refactor(schedule): simplify request zone authority

This commit is contained in:
pku-xht
2026-08-06 19:33:19 +08:00
committed by Tianyi Cui
parent a667ec55d6
commit cd59acd6f6
25 changed files with 466 additions and 1007 deletions

View File

@@ -10,19 +10,19 @@ A request-only clock can tell the model the current time, but replacing that val
A process-local refresh cache makes displayed time depend on state that cannot survive resume or be reconstructed from the durable session. Durable interval scheduling can reduce append frequency without introducing that hidden state.
Local calendar work also needs to distinguish two authorities: the immutable zone captured by the Session and the zone attached to each browser-originated request. Process state or a mutable connection default cannot represent travel, concurrent tabs, or old headerless Sessions without silently reinterpreting a request.
Local calendar work also needs to distinguish two owned facts: the immutable zone captured by the Session and the zone attached to each browser-originated request. Process state or a mutable connection default cannot represent travel, concurrent tabs, or old headerless Sessions without silently reinterpreting a request.
## Decision
`@deepseek-ai/dsh-time-context` is an opt-in function plugin in `packages/context/time-context/`. The `context/` group holds bounded request-context enrichments that define neither a tool nor a service. Default compositions leave its disclosure and token cost disabled; the explicit Schedule Web overlay mounts it because local `at` interpretation consumes its authority.
`@deepseek-ai/dsh-time-context` is an opt-in function plugin in `packages/context/time-context/`. The `context/` group holds bounded request-context enrichments that define neither a tool nor a service. Default compositions leave its disclosure and token cost disabled; the explicit Schedule Web overlay mounts it because local `at` interpretation needs request-zone context.
When a reading is due, a prepended `system-prompt/assemble` listener opens a narrow authority envelope in the ordinary next-step inbox. It captures the already-claimed messages, and each user steering insertion admitted during asynchronous assembly synchronously stages a superseding authority. AgentLoop includes non-authority messages inside the closed envelope in the downstream `agent/pre-step` proposal, so ordinary guards, edits, discards, and filtering see the late input.
The plugin prepends an `agent/pre-step` listener and delegates first. When the downstream decision enters a request step and a reading is due, time-context derives client zones from that decision's final messages plus user-rpc messages already entered in the open turn, then appends one reading to the decision. Steering inserted after AgentLoop claims the current batch keeps ordinary next-step ownership and receives a new reading when that step enters.
After downstream pre-step transformations settle, time-context derives one final authority from the returned messages. An entering step appends those messages and only the final authority after `step/start`, before request derivation. An empty decision consumes the envelope without opening a request. A rejection, throw, or cancellation removes the envelope before the failed turn closes and may settle an already-sampled final authority inside that turn; append rejection drops it instead of leaking it. Disposal removes pending authorities and prevents an in-flight listener from contributing after disposal.
An entering step appends its returned messages followed by the time reading after `step/start`, before request derivation. A first-step decision rewritten to empty opens no request, while an empty tool continuation can still enter a later step and receive a reading. Rejection, failure, or cancellation before `step/start` appends nothing. Disposal prevents an in-flight listener from contributing after it wins, without adding inbox state or an AgentLoop lifecycle path.
Each reading's strict source is `{ kind: 'plugin', plugin: 'time-context', authority }`. The authority identifies the proposed turn and step, reports the immutable `SessionHeader.timeZone` as `resolved` or `unavailable`, and folds the final request chain's browser provenance into `resolved`, sorted `mixed`, or `missing`. The rendered clock uses the Session zone when available. A headerless Session uses the configured fallback, or the Node process zone resolved once at plugin load when config is omitted, while its machine Session authority remains `unavailable`. Every explicit or Session-owned IANA zone is validated through `Intl.DateTimeFormat`.
Each reading has the simple source `{ kind: 'plugin', plugin: 'time-context' }`. The immutable `SessionHeader.timeZone` and each original user-rpc message's `clientTimeZone` remain the only machine-readable owners. Time-context renders those facts for the model, while Schedule derives directly from the same header and current-turn sources instead of consuming a copy. The rendered clock uses the Session zone when available. A headerless Session uses the configured fallback, or the Node process zone resolved once at plugin load when config is omitted, while still reporting the Session zone as `unavailable`. Every explicit or Session-owned IANA zone is validated through `Intl.DateTimeFormat`.
The optional `refreshIntervalMs` config is manually validated at plugin load as a non-negative safe integer. Omission or `0` injects on every eligible preparation attempt. A positive value scans the raw session events for the most recent `user/message` with this plugin's source and injects when none exists, wall time moved backward, or the event is at least the configured age. The raw event timestamp governs even after compaction shadows the message, so scheduling persists across turns and process resume without a timer or process-local cache.
The optional `refreshIntervalMs` config is manually validated at plugin load as a non-negative safe integer. Omission or `0` injects on every entered request step. A positive value scans the raw session events for the most recent `user/message` with this plugin's source and injects when none exists, wall time moved backward, or the event is at least the configured age. The raw event timestamp governs even after compaction shadows the message, so scheduling persists across turns and process resume without a timer or process-local cache.
### Text and elapsed baselines
@@ -50,13 +50,13 @@ Their baseline is the durable event timestamp of the preceding time-context mess
### Durability and request reconstruction
Each reading remains a normal surface node until compaction shadows it; positive interval scheduling never removes existing readings. A later request therefore sees the cumulative unshadowed readings that affected earlier preparation and steps, rather than a system-prompt value rewritten in place. The strict source makes the same Session and request-zone authority available to typed consumers such as Schedule without parsing model-facing text.
Each reading remains a normal surface node until compaction shadows it; positive interval scheduling never removes existing readings. A later request therefore sees the cumulative unshadowed readings that affected earlier preparation and steps, rather than a system-prompt value rewritten in place. The simple source identifies the reading without duplicating the Session or request-zone facts that Schedule can derive from their original durable owners.
The plugin uses system-prompt assembly only as the bounded preparation window; it does not add a system-prompt section. `request/header` contains no time-context text, and request reconstruction obtains the complete durable surface prefix at each `step/start`. Readings and requests need not map one-to-one because interval suppression can enter a request without appending a reading, while a failed no-step preparation may retain its already-sampled authority without transmitting a request.
The plugin does not add a system-prompt section. `request/header` contains no time-context text, and request reconstruction obtains the complete durable surface prefix at each `step/start`. Readings and requests need not map one-to-one because interval suppression can enter a request without appending a reading, while a failure after step entry may retain a reading without transmitting a request. A failure before step entry retains none.
## Testing
Unit and real-loop tests pin formatting, Session/fallback display zones, resolved/mixed/missing client authority, both elapsed baselines, interval omission and zero, threshold boundaries, cross-turn and per-session scheduling, backward-clock behavior, invalid config, resumed raw-event lookup after compaction, late steering, edit and discard, empty suppression, append rejection, default and keep-inbox cancellation, in-flight disposal, source decoding, cumulative multi-step visibility, and absence from request headers. A keyless subprocess e2e boots the real Loader with the Headless composition, drives two ordered one-shot turns, and verifies the persisted plugin-attributed messages externally; the Schedule Web scenario verifies the authority through the assembled browser path.
Unit and real-loop tests pin formatting, Session/fallback display zones, unique/mixed/missing client-zone derivation, both elapsed baselines, interval omission and zero, threshold boundaries, cross-turn and per-session scheduling, backward-clock behavior, invalid config, resumed raw-event lookup after compaction, post-claim steering ownership, empty suppression, cancellation, in-flight disposal, simple source validation, cumulative multi-step visibility, and absence from request headers. A keyless subprocess e2e boots the real Loader with the Headless composition, drives two ordered one-shot turns, and verifies the persisted plugin-attributed messages externally; the Schedule Web scenario verifies the same source facts through the assembled browser path.
## Alternatives considered
@@ -66,13 +66,14 @@ Unit and real-loop tests pin formatting, Session/fallback display zones, resolve
- **Expose time only through a tool** — rejected because ordinary temporal reasoning would require an avoidable tool round trip and would not guarantee a reading before every step.
- **Use `agent/session-prefix`** — rejected because one loop-instance prefix cannot represent distinct step timestamps and does not accumulate historically attributable readings.
- **Mutate assembled requests or register independent prompt variables** — rejected because request-local insertion bypasses the durable surface and separate providers can sample different instants. One attributed context message records the timestamp and elapsed baseline atomically.
- **Use the process zone or most recent browser as request authority** — rejected because deployment state cannot infer a remote user's zone, while a mutable connection default lets travel or concurrent tabs reinterpret another request. The process or configured zone remains only a display fallback for headerless Sessions.
- **Mount the plugin in default compositions or place it in `core/`** — rejected because disclosure, freshness, and history cost are deployment choices for an optional context leaf. A feature-specific overlay may opt in when it has a current authority consumer.
- **Copy request zones into a durable authority and absorb post-claim steering into the current step** — rejected because the immutable Session header and entered user-rpc sources already own those facts, while no current production assembly boundary requires inbox reentry. Copying them would add validation and AgentLoop lifecycle solely for a second representation; post-claim steering already receives fresh context in its ordinary next step.
- **Use the process zone or most recent browser as request state** — rejected because deployment state cannot infer a remote user's zone, while a mutable connection default lets travel or concurrent tabs reinterpret another request. The process or configured zone remains only a display fallback for headerless Sessions.
- **Mount the plugin in default compositions or place it in `core/`** — rejected because disclosure, freshness, and history cost are deployment choices for an optional context leaf. A feature-specific overlay may opt in when it has a current consumer.
## Consequences
- Omission or `0` records every eligible preparation attempt; a positive interval reduces append frequency and history growth while preserving durable scheduling across resume.
- Timing context remains append-only until compaction shadows older surface nodes, including an already-sampled preparation reading settled inside a turn that opens no step.
- Omission or `0` records every entered request step; a positive interval reduces append frequency and history growth while preserving durable scheduling across resume.
- Timing context remains append-only until compaction shadows older surface nodes; a turn that opens no step records no reading.
- First-step duration measures from the previous durable model-visible event, while later-step duration measures model and tool processing since the preceding step context.
- Session authority is immutable and request authority is message-bound, so travel or concurrent tabs expose disagreement instead of changing shared state.
- A headerless Session renders through the configured or deployment-process fallback but remains machine-readable as `unavailable`; elapsed time still uses durable harness append boundaries rather than client-origin timestamps.
- The Session zone is immutable and each browser zone is message-bound, so travel or concurrent tabs expose disagreement instead of changing shared state.
- A headerless Session renders through the configured or deployment-process fallback but remains reported as `unavailable`; elapsed time still uses durable harness append boundaries rather than client-origin timestamps.

View File

@@ -33,15 +33,15 @@ An Agent-scoped FIFO serializes each accepted management transaction and the liv
Every successful management preflight also asks the live owner to recompute. This closes the recovery path where create appended successfully but its post-append barrier rejected: a later list can confirm the coordinator's retained batch, return the active record, and arm its timer without a Schedule-specific retry loop.
### Session and request time-zone authority
### Session and request time-zone ownership
The official Web create path requires the browser's IANA zone, validates and canonicalizes it at the Host boundary, and stores it once as immutable `SessionHeader.timeZone`. Resume preserves that value, fork copies it, and another create for the same id and cwd conflicts when its canonical zone differs. Session core keeps the field optional so pre-zone Sessions remain readable but explicitly `unavailable`; a legacy header is never backfilled from a later browser request. JSONL preserves the optional header, while SQLite schema v14 adds nullable `time_zone` and upgrades an owned v13 database atomically without guessing values for existing rows.
Every Web prompt samples its own `clientTimeZone`, which the Host validates before Agent entry and binds to that immutable `user-rpc` message source. This is request provenance, not a mutable property of the connection or Session, so concurrent tabs cannot overwrite one another and queue, steering, edit, retry, and persisted history retain the originating zone.
Time-context opens a request-authority envelope at system-prompt assembly. Its model-visible reading uses the Session zone for the current date, local time, and offset, while its machine source names the proposed turn and step plus Session `resolved`/`unavailable` and client `resolved`/`mixed`/`missing` state. Steering admitted during asynchronous assembly is followed synchronously by a same-step superseding authority; the model and Schedule tool both consume the last authority for that turn and step. AgentLoop drains only the closed envelope that begins and ends with those authority messages. If the proposed step exits before `step/start`, it settles appendable authority inside the failed turn or removes authority that cannot be appended, while preserving the existing steering policy, so an old turn/step authority cannot leak into a later request.
Time-context delegates through `agent/pre-step`, derives the final entered request's zones from the immutable Session header and message-bound browser sources, and appends one model-visible reading to an entered step. Its source remains the simple plugin marker; it does not copy those facts into another durable authority. Steering inserted after AgentLoop claims the current batch keeps ordinary next-step ownership and receives fresh context when that step enters. Rejection, cancellation, or failure before `step/start` records no reading, and this feature adds no inbox or AgentLoop lifecycle state.
An implicit local `at` is accepted only when the final authority has one resolved client zone equal to the resolved Session zone. A headerless Session, missing or mixed client provenance, or a client/Session mismatch returns `timezone_confirmation_required` with the known zones. An explicit `time_zone` bypasses that ambiguity check but still passes the same IANA validation.
Schedule requires a current-step time-context marker, then derives request zones directly from the open turn's original `user-rpc` sources. An implicit local `at` is accepted only when that derivation has one client zone equal to the Session zone. A headerless Session, missing or mixed client provenance, or a client/Session mismatch returns `timezone_confirmation_required` with the known zones. An explicit `time_zone` bypasses that ambiguity check but still passes the same IANA validation.
### Absolute-time normalization
@@ -107,7 +107,7 @@ The design does not recognize or migrate any unmerged Schedule implementation or
Package tests pin strict decoding, transitions, fork suffixes, id reuse, offset and local-calendar profiles, IANA validation, gap rejection, overlap-first selection, mismatch confirmation, time bounds, bounded waits, wall-clock movement, overdue admission, fixed framing, enqueue and append failures, barrier recovery, registration rollback, and quiescent disposal at 100% per-file coverage. Persistence tests cover new, fork, and resumed initialization failures against the actual durable cursor, optional header round-trips, a real SQLite v13-to-v14 migration, and a production JSONL restart. The assembled Loader/Web restart lane proves pending recovery, fork isolation, one durable dispatch, cold-history rendering without Agent activation, and no redelivery after another restart. Host/client tests cover zone identity across live, stored, and concurrent-create paths; per-operation prompt provenance; commit gating; reversed watermarks; semantic header identity; per-event prefix matching; same-seq upgrades; every window merge exit; and reconnect generations.
Time-context and AgentLoop lifecycle tests cover queued, edited, discarded, cancelled, and retried input; mixed tabs; delayed assembly with late steering; pre-step hook, assembly, checkpoint, append, and disposal failures; same-step last-authority selection; and non-leakage into the next turn. The opt-in Loader composition boots the source and built packages. Keyless real-browser scenarios execute `schedule_create` through the complete tool pipeline for the existing short `after` case and one absolute-time case, observe the identity-matched persisted prefix, and render the durable reminder card from attached history. The deliberately absent model adapter closes the turn with an error after dispatch, proving that model failure does not remove the receipt.
Time-context tests cover final pre-step messages, current-turn unique/mixed/missing zone derivation, post-claim steering entering the next step, cancellation, empty suppression, retry, simple source validation, and in-flight disposal. Schedule tests independently derive the same request zones from durable `user-rpc` sources and fail closed without a current-step marker. The opt-in Loader composition boots the source and built packages. Keyless real-browser scenarios execute `schedule_create` through the complete tool pipeline for the existing short `after` case and one absolute-time case, observe the identity-matched persisted prefix, and render the durable reminder card from attached history. The deliberately absent model adapter closes the turn with an error after dispatch, proving that model failure does not remove the receipt.
## Consequences

View File

@@ -33,15 +33,15 @@ Status: implemented
每次成功的管理 preflight 也会要求 live owner 重新计算。这闭合了 create 已成功追加、但 post-append barrier 拒绝时的恢复路径:后续 list 可以确认 coordinator 保留的 batch、返回活动 record并在没有 Schedule 私有重试循环的情况下 arm timer。
### Session 与请求时区权威
### Session 与请求时区归属
官方 Web create 路径要求浏览器提供 IANA 时区,在 Host 边界校验并规范化后,将其一次性存为不可变的 `SessionHeader.timeZone`。resume 保留该值fork 复制该值;若针对相同 id 与 cwd 的另一次 create 得到的规范化时区不同则发生冲突。Session core 保持该字段可选,使时区支持前的 Session 仍可读取,但其时区明确为 `unavailable`;绝不会用后续浏览器请求回填 legacy header。JSONL 保留该可选 headerSQLite schema v14 增加 nullable `time_zone`,并以原子方式升级自有 v13 数据库,不为既有行猜测值。
每条 Web 提示词都会单独采样自己的 `clientTimeZone`Host 在进入 Agent 前校验该值,并把它绑定到不可变的 `user-rpc` 消息来源。它是请求 provenance而不是连接或 Session 的可变属性,因此并发 tab 无法相互覆盖排队、steering中途引导、编辑、重试和持久化 history 都会保留来源时区。
Time-context 在系统提示词组装时打开请求权威包络。它向模型显示的读数按照 Session 时区给出当前日期、本地时间和 offset机器源则标明拟议的轮次与步骤以及 Session 的 `resolved``unavailable` 状态和 client 的 `resolved``mixed``missing` 状态。异步组装期间获准进入的 steering 后面,会同步追加同一步骤的取代权威;模型与 Schedule 工具都使用该轮次和步骤的最后一条权威。AgentLoop 只排空以这些权威消息为首尾的闭合包络。如果拟议步骤在 `step/start` 前退出AgentLoop 会在失败轮次内结算可追加的权威消息,或移除无法追加的权威消息,同时保留既有 steering 政策,从而防止旧轮次/步骤的权威泄漏到后续请求
Time-context 会委托 `agent/pre-step`,从不可变 Session header 和与消息绑定的浏览器来源派生最终进入请求的时区再向已经进入的步骤追加一条模型可见读数。其来源仍是简单插件标记不会把这些事实复制成另一份持久权威。AgentLoop 领取当前批次后才插入的 steering中途引导保留常规 next-step 归属,并在该步骤进入时获得新上下文。`step/start` 之前发生 reject、取消或失败时不会记录读数本功能也不增加 inbox 或 AgentLoop 生命周期状态
只有最终权威包含一个已解析的 client 时区,且它等于已解析的 Session 时区,才会接受隐式 local `at`。无 header 的 Session、client provenance 缺失或 mixed或 clientSession 不匹配,都会返回 `timezone_confirmation_required` 及已知时区。显式 `time_zone` 可绕过这项歧义检查,但仍要通过相同的 IANA 校验。
Schedule 要求当前步骤存在 time-context 标记,然后直接从 open turn 的原始 `user-rpc` 来源派生请求时区。只有派生结果包含一个与 Session 时区相等的 client 时区,才会接受隐式 local `at`。无 header 的 Session、client provenance 缺失或 mixed或 clientSession 不匹配,都会返回 `timezone_confirmation_required` 及已知时区。显式 `time_zone` 可绕过这项歧义检查,但仍要通过相同的 IANA 校验。
### 绝对时间规范化
@@ -69,7 +69,7 @@ Host 在 append 时继续发送所有 raw event。它在 `WeakMap` 中按 exact
已附加 history 会独立 inspect persistence只有 stored event prefix 的 header identity 与每个 event 都和 live Session 匹配时才添加 view。persistence 会把顶层缺失的 `delegationDepth` 规范写成零因此两种形式在身份上等价cwd、lineage、origin、时间戳、版本、id 与每个 event 仍必须精确匹配。inspect 缺失、失败、分歧或比 live 更长时,只会省略 viewraw history 仍然返回。已分离 history 本身就是持久前缀。因此复制进 fork seed 的 parent dispatch 只有在 child storage 证明该前缀后才会显示。
浏览器 Session 只有在 durable event 深度一致时才接受重复 seq随后立即升级 sidecar不再追加 event。只有尾部加载与真正的 gap repair 会将尚未覆盖的事件保留在既有 `liveBuffer` 中;已接受的 repair 快照在推进 tail 但仍留下后续已缓冲的 gap 时会启动另一次 pull身份冲突则会触发全量重新同步。普通旧页分页会让当前数组继续接收 live tail 事件,当前 window 以下的 sidecar 则由 in-flight page 自身暂存,只有该页返回身份完全相同的事件时才附着。重连 generation 会阻止陈旧的 page 或 repair 结果以及 `finally` 块触碰重建后的 window。`TranscriptAdapter` 创建按持久事件类型键控的通用 `PresentedEventNode``ui-conversation` 通过 `conversation.chat.eventview` 分发,并保留可展开 JSON fallback`ui-schedule` 则拥有双语 `schedule/change` 提醒行。
浏览器 Session 只有在 durable event 深度一致时才接受重复 seq随后立即升级 sidecar不再追加 event。尾部加载与真正的 gap repair 会将尚未覆盖的事件保留在既有 `liveBuffer` 中;已接受的 repair 快照在推进 tail 但仍留下后续已缓冲的 gap 时会启动另一次 pull身份冲突则会触发全量重新同步。普通旧页分页会让当前数组继续接收 live tail 事件,当前 window 以下的 sidecar 则由 in-flight page 自身暂存,只有该页返回身份完全相同的事件时才附着。重连 generation 会阻止陈旧的 page 或 repair 结果以及 `finally` 块触碰重建后的 window。`TranscriptAdapter` 创建按持久事件类型键控的通用 `PresentedEventNode``ui-conversation` 通过 `conversation.chat.eventview` 分发,并保留可展开 JSON fallback`ui-schedule` 则拥有双语 `schedule/change` 提醒行。
```text
schedule_create → Session create event → persistence
@@ -107,7 +107,7 @@ due → admission → followup → dispatch → flush(true) → session/flushed
package 测试以逐文件 100% coverage 固定严格 decoding、transition、fork suffix、id 不复用、offset 与 local-calendar profile、IANA 校验、gap 拒绝、overlap-first 选择、mismatch confirmation、时间边界、有界等待、墙钟变化、overdue 准入、固定 framing、入队与 append 失败、barrier 恢复、注册 rollback 和完全停稳 dispose。persistence 测试依据实际 durable cursor 覆盖 new、fork 与 resumed 初始化失败、可选 header round-trip、一次真实 SQLite v13 到 v14 migration以及 production JSONL restart。组装后的 Loader/Web restart lane 证明 pending 恢复、fork 隔离、单次 durable dispatch、无需激活 agent 的 cold-history rendering以及再次 restart 后不重投。Host/client 测试覆盖 live、stored 与 concurrent-create 路径中的 zone identity、逐操作提示词 provenance、commit gating、反序 watermark、语义 header identity、逐 event 前缀匹配、same-seq 升级、每个 window merge 出口和 reconnect generation。
Time-context 与 AgentLoop 生命周期测试覆盖已排队、已编辑、已丢弃、已取消和已重试的输入;混合 tab带有晚到 steering 的延迟组装pre-step 钩子、组装、检查点、追加与处置阶段的失败;同一步骤内选择最后一条权威;以及权威不会泄漏到下一轮次。显式 Loader 组合可以启动 source 与 built package。无密钥真实浏览器场景会通过完整工具 pipeline针对既有的短 `after` case 和一个 absolute-time case 执行 `schedule_create`,观察 identity-matched 持久前缀,并从已附加 history 渲染 durable reminder card。刻意缺少的模型 adapter 会在 dispatch 后以错误关闭 turn从而证明模型失败不会移除回执。
Time-context 测试覆盖最终 pre-step 消息、当前 turn 的唯一/混合/缺失时区派生、领取后 steering 进入下一步骤、取消、空值抑制、重试、简单来源校验和执行中释放。Schedule 测试会从持久 `user-rpc` 来源独立派生同一组请求时区,并在缺少当前步骤标记时 fail closed。显式 Loader 组合可以启动 source 与 built package。无密钥真实浏览器场景会通过完整工具 pipeline针对既有的短 `after` case 和一个 absolute-time case 执行 `schedule_create`,观察 identity-matched 持久前缀,并从已附加 history 渲染 durable reminder card。刻意缺少的模型 adapter 会在 dispatch 后以错误关闭 turn从而证明模型失败不会移除回执。
## 后果