diff --git a/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
index ef4f8d14d9..af6890f84b 100644
--- a/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
-2026-07-31-claimed-pre-step-inbox-lifecycle.md: ed56723ab8dcaaba9fbcd8e4c52ecc264141df2e
-2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: aecfece99c49a31d429fc3229432911dc4b83f82
+2026-07-31-claimed-pre-step-inbox-lifecycle.md: 6aa829ee8489ee0d19c4af16e527774f8751eea8
+2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md: 03dc58fed3ecc3842faab3727286c6ceda9399a5
diff --git a/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md b/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
index ed56723ab8..6aa829ee84 100644
--- a/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
+++ b/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.md
@@ -20,7 +20,7 @@ The durable inbox remains two `UserMessage[]` lists addressed by `MessageId`. `a
The two event surfaces have separate consumers. Observers following one message use `agent/inbox/inserted`, `claimed`, and `discarded`. Whole-queue consumers, including the Web queue projection and reconnect baseline, use the durable `agent/inbox/spliced` stream; UI edits and removals route through `Inbox.splice()` or another Inbox mutation method so the same projection records every change.
-Plugins that need current-step atomic rewriting return messages from `agent/pre-step`. Plugins that only need later context may mutate `agent.inbox` directly. Workspace context uses that weaker timing deliberately: it prepends its current sourced context to `next-step`, replaces an exact still-pending predecessor, and does not alter the current pre-step decision.
+Plugins that need current-step atomic rewriting return messages from `agent/pre-step`. Plugins that only need later context may mutate `agent.inbox` directly. Workspace context uses both paths: asynchronous filesystem projections stage one replaceable `next-step` item, while the next entering pre-step folds that item or a newly composed baseline into its final batch and removes the pending copy. Rejection keeps the item queued.
The archived [addressable queue occurrence decision](../../archived/feature/2026-07-29-addressable-queue-operations.md) describes the superseded occurrence-wrapper design. `MessageId` now owns addressability, while the retained Host queue mirror derives its snapshots from the durable splice projection.
@@ -34,7 +34,7 @@ The archived [addressable queue occurrence decision](../../archived/feature/2026
## Verification
-Agent-loop coverage pins turn-start-before-claim-before-pre-step ordering, exact live event payloads, balanced no-step rejection, final-batch rewriting, input inserted after a claim, listener failure, and cancellation. Inbox and consumer tests pin pure claim deletions, canceled ordinary removals, workspace-context delayed insertion and replacement, plan/goal/hook behavior, UI cleanup, compaction, checkpointing, and resumed durable projection. Generated event and type catalogs expose only the new seam and payloads.
+Agent-loop coverage pins turn-start-before-claim-before-pre-step ordering, exact live event payloads, balanced no-step rejection, final-batch rewriting, input inserted after a claim, listener failure, and cancellation. Inbox and consumer tests pin pure claim deletions, canceled ordinary removals, workspace-context staging, replacement, and same-step entry, plan/goal/hook behavior, UI cleanup, compaction, checkpointing, and resumed durable projection. Generated event and type catalogs expose only the new seam and payloads.
## Consequences
diff --git a/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md b/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
index aecfece99c..03dc58fed3 100644
--- a/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
@@ -20,7 +20,7 @@ Status: implemented
两类事件表面服务不同消费方。跟踪单条消息的观察方使用 `agent/inbox/inserted`、`claimed` 与 `discarded`。包括 Web 队列投影和重连基线在内的整体队列消费方使用持久 `agent/inbox/spliced` 流;UI 编辑与移除经 `Inbox.splice()` 或其他 Inbox 变更方法进入,从而让同一投影记录所有变化。
-必须对当前步骤进行原子改写的插件从 `agent/pre-step` 返回消息。只需要稍后上下文的插件可以直接修改 `agent.inbox`。Workspace context 刻意采用较弱的时序:它把当前带来源上下文 prepend 到 `next-step`,替换仍在等待的确切前序消息,并且不修改当前 pre-step 决策。
+必须对当前步骤进行原子改写的插件从 `agent/pre-step` 返回消息。只需要稍后上下文的插件可以直接修改 `agent.inbox`。Workspace context 同时使用两条路径:异步文件系统投影会暂存一条可替换的 `next-step` 消息,而下一次进入步骤的 pre-step 会把该消息或新组合的基线折入最终批次,并移除仍待处理的副本。reject 会让该条目继续排队。
已归档的[可寻址队列项决策](../../archived/feature/2026-07-29-addressable-queue-operations.md)描述了已被取代的单次出现 wrapper 设计。现在由 `MessageId` 负责寻址,而保留的 Host 队列镜像根据持久 splice 投影派生快照。
@@ -34,7 +34,7 @@ Status: implemented
## 验证
-Agent-loop 覆盖固定先 `turn/start`、再领取、后 pre-step 的顺序、实时事件的确切载荷、边界平衡的无步骤 reject、最终批次改写、领取后插入的输入、监听器失败与取消。Inbox 和消费方测试固定纯领取删除、普通删除的 canceled 结果、workspace-context 的延后插入与替换、plan/goal/hook 行为、UI 清理、压缩、检查点以及恢复后的持久投影。生成的事件与类型目录只公开新的 seam 与载荷。
+Agent-loop 覆盖固定先 `turn/start`、再领取、后 pre-step 的顺序、实时事件的确切载荷、边界平衡的无步骤 reject、最终批次改写、领取后插入的输入、监听器失败与取消。Inbox 和消费方测试固定纯领取删除、普通删除的 canceled 结果、workspace-context 的暂存、替换与同一步骤进入、plan/goal/hook 行为、UI 清理、压缩、检查点以及恢复后的持久投影。生成的事件与类型目录只公开新的 seam 与载荷。
## 后果
diff --git a/.agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml b/.agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml
index 0d099c19b4..5f045324dd 100644
--- a/.agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-24-workspace-context.md
-2026-06-24-workspace-context.md: 5b55f9173d11ad248c29fc736df092cb3600d7ef
-2026-06-24-workspace-context.zh.md: c6f3ce9d52e537f04765123124a1d3e5cd81249d
+2026-06-24-workspace-context.md: df7ae58f8a42a8c1aac8113a429ef0f50b2fb795
+2026-06-24-workspace-context.zh.md: aecc57a5c63b4c6282260863f4183944c14b5cf8
diff --git a/.agents/notes/implemented/feature/2026-06-24-workspace-context.md b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md
index 5b55f9173d..df7ae58f8a 100644
--- a/.agents/notes/implemented/feature/2026-06-24-workspace-context.md
+++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md
@@ -28,9 +28,9 @@ The user-global file is fixed at `$DSH_HOME/AGENTS.md`, is not affected by eithe
### Baseline Injection
-At the first `agent/pre-step` of an agent-loop instance, the plugin composes one sourced user-role baseline and prepends it directly to the agent's next-step inbox without changing the current decision. It loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads the configured candidates from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root. Because the loop claims before pre-step, the baseline may enter a later request.
+At the first `agent/pre-step` of an agent-loop instance, the plugin composes one sourced user-role baseline. When the downstream decision enters a nonempty first-step batch, the plugin folds the baseline into that final batch right after the claimed prompt, so it becomes durable with the direct prompt and reaches the first request. Rejection or an empty first-step decision leaves the baseline in the next-step inbox for a later wakeup. The plugin loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads the configured candidates from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root.
-When a later pre-step claims and enters it, the baseline becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes the complete startup or resume baseline from later deltas, and its change list persists the included scopes and content digests. If a previously queued workspace baseline is still pending, the plugin removes that exact message and prepends its replacement instead of accumulating duplicates.
+The baseline becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes the complete startup or resume baseline from later deltas, and its change list persists the included scopes and content digests. If a previously queued workspace baseline is still pending, the plugin removes that exact message and prepends its replacement instead of accumulating duplicates.
A resumed agent creates a new loop instance and injects a baseline composed from current files before its first request. This permits current baseline content on resume without mutating an earlier history event. A resume and a hot plugin remount both face a log that may already hold a baseline; they are told apart by `agent/session-start`, which a startup or resume emits before the first step while a remount attaches to an already-live session and never sees it. A remount retains the existing baseline only when its typed event remains in the current visible surface, and still rebuilds scope and provider-version tracking from current files. If compaction has shadowed that event, the remount injects a current baseline. A resume always re-composes.
@@ -68,7 +68,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc
**Use a global `ctx.systemPrompt.section()`.** Rejected because one Cordis context can host sessions with different cwd values, while repository-owned text is lower-authority context rather than top-authority provider system content.
-**Return the baseline in every `agent/pre-step` decision.** Rejected because workspace context needs only eventual next-step delivery. Direct inbox replacement keeps one pending baseline without coupling it to the request already claimed before the listener ran.
+**Always leave prepared workspace context in the inbox.** Rejected because context prepared during pre-step would then survive the current request and start a second model step by itself. The inbox remains the staging and rejection fallback, while an entering pre-step owns atomic delivery with its final batch.
**Load both `AGENTS.md` and `CLAUDE.md` in one directory.** Rejected because repositories in transition commonly duplicate guidance across both files. Ordered candidates make precedence explicit and configurable.
diff --git a/.agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md b/.agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md
index c6f3ce9d52..aecc57a5c6 100644
--- a/.agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md
+++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.zh.md
@@ -28,9 +28,9 @@ Status: implemented
### 基线注入
-在 agent loop(智能体循环)实例的第一个 `agent/pre-step`,插件会组合一条带来源的 user 角色基线,将它直接 prepend 到 agent 的 next-step inbox,且不修改当前决策。它先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录加载已配置候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。由于循环先领取、后运行 pre-step,该基线可能进入后续请求。
+在 agent loop(智能体循环)实例的第一个 `agent/pre-step`,插件会组合一条带来源的 user 角色基线。当下游决策让非空的第一步批次进入时,插件会将基线折入最终批次、紧随已领取的直接提示词之后,使其与直接提示词一同成为持久记录并抵达第一次请求。reject 或空的第一步决策会将基线留在 next-step inbox,等待后续唤醒。插件先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录加载已配置候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。
-后续 pre-step 领取并让它进入步骤时,该基线成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待,插件会删除该确切消息并 prepend 替代值,而不会累积副本。
+该基线会成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待,插件会删除该确切消息并 prepend 替代值,而不会累积副本。
恢复 agent 会创建新的循环实例,并在其第一次请求前注入由当前文件组合的基线。这样,恢复时可以使用当前基线内容,而无需修改先前的历史事件。恢复与插件热重挂都会面对日志中可能已存在基线的情况;二者通过 `agent/session-start` 区分:启动或恢复会在第一步前发出该事件,而热重挂附着到一个已存活的会话、永远不会看到它。只有当基线的类型化事件仍在当前可见表层中时,热重挂才保留既有基线,同时仍会根据当前文件重建 scope 与提供方版本跟踪。如果压缩(compaction)已遮蔽该事件,热重挂会注入当前基线。恢复则始终重新组合。
@@ -68,7 +68,7 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell,
**使用全局 `ctx.systemPrompt.section()`。** 不予采纳,因为同一个 Cordis 上下文可以承载 cwd 不同的多个会话,而仓库自身拥有的文本属于低权威用户上下文,不是最高权威的提供方系统内容。
-**在每次 `agent/pre-step` 决策中返回基线。** 不予采纳,因为 workspace context 只需要最终进入后续步骤。直接替换 inbox 可以只保留一条待处理基线,也不会把它耦合到监听器运行前已经领取的请求。
+**始终把准备好的 workspace 上下文留在 inbox 中。** 不予采纳,因为在 pre-step 中准备的上下文会因此在当前请求结束后继续留存,并自行启动第二个模型步骤。inbox 仍作为暂存区以及 reject 时的后备,而进入步骤的 pre-step 负责随最终批次原子投递。
**在一个目录中同时加载 `AGENTS.md` 和 `CLAUDE.md`。** 不予采纳,因为正在迁移的仓库通常会在两个文件中重复指引。按顺序排列的候选项让优先级显式且可配置。
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index 7b44a0d3e0..31546a444f 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
-architecture.md: e852506d94fa67da92819879dba4dc546fce540b
-architecture.zh.md: b4b34b29c75996dc79188943dc1999db71e1710c
+architecture.md: bbf13606861ad6995fd5a8b2c64e323eabc869c6
+architecture.zh.md: bea98a9d5936af9e11be6dffcb4d911e07665419
diff --git a/docs/architecture.md b/docs/architecture.md
index e852506d94..bbf1360686 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -158,7 +158,7 @@ A swappable capability usually has **interface / implementation / consumer** lay
Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children ([subagent.md](core-data-structures/subagent.md)).
-`dsh-workspace-context` prepends its baseline to the next-step inbox on first `agent/pre-step`, replacing a pending predecessor, and adds `ctx.fs` changes after tools. Because claim precedes pre-step, the baseline may miss the current request ([decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)). `dsh-paths` owns shared paths.
+`dsh-workspace-context` composes its baseline on the first `agent/pre-step` and folds it into the final entering batch right after the claimed prompt, so it reaches the first request with the direct prompt; rejection keeps it in the next-step inbox. Filesystem changes projected after tools are likewise folded into the next entering pre-step instead of creating a later context-only step ([decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)). `dsh-paths` owns shared paths.
### Bundles And Apps
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index b4b34b29c7..bea98a9d59 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -158,7 +158,7 @@ idle inject:
例外情况包括 LLM(大语言模型)合并接口和消费方、文件系统整合策略、web 使用注册表、skill 和 subagent 使用具名提供方。subagent 可以通过 spawn 创建全新实例、fork 一个已完成轮次的前缀,或使用 ACP(Agent Client Protocol)子 agent([subagent.md](core-data-structures/subagent.md))。
-`dsh-workspace-context` 在第一次 `agent/pre-step` 将基线 prepend 到 next-step inbox,替换仍在等待的前序消息,并在工具执行后添加 `ctx.fs` 变更。由于领取发生在 pre-step 之前,该基线可能赶不上当前请求([决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md))。`dsh-paths` 负责共享路径。
+`dsh-workspace-context` 在第一次 `agent/pre-step` 组合基线并将它折入最终进入的批次、紧随已领取的直接提示词之后,使其与直接提示词一同抵达第一次请求;reject 则将它留在 next-step inbox。工具执行后投影的文件系统变更也会折入下一次进入步骤的 pre-step,而不会另外创建稍后的纯上下文步骤([决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md))。`dsh-paths` 负责共享路径。
### 组合包与应用
diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/replay.override.json b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/replay.override.json
index 86b1e185f9..192a2bdcac 100644
--- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/replay.override.json
+++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/replay.override.json
@@ -9,13 +9,6 @@
{ "type": "finish", "reason": { "kind": "tool-calls" } }
]
},
- {
- "kind": "chunks",
- "chunks": [
- { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 1 } },
- { "type": "finish", "reason": { "kind": "stop" } }
- ]
- },
{
"kind": "chunks",
"chunks": [
diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
index f7da13d67f..2efe69428a 100644
--- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
@@ -1,12 +1,12 @@
{"type":"session","version":0,"id":"b1e35a14-a592-44e6-bf23-b2496ad2bf7b","createdAt":1785014475001,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498825884,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"433d493a-0b85-4b92-8063-cfa19b8f7d10"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498825884,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"3b04578e-7b22-4b44-b4cd-ef9d4d26fe8b"}]}}
{"type":"turn/start","seq":1,"time":1785821441771,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785498825916,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":3,"time":1785821441803,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"5578ed99-ced0-42af-b8f6-651e8aaf72e5"}]}}
-{"type":"step/start","seq":4,"time":1785122256264,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785498825917,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"433d493a-0b85-4b92-8063-cfa19b8f7d10"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730478198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"adca0c36-3b6e-4d58-9076-c0b4e6681030"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730478198,"data":{"title":"Using ONE run_code program, call","messageSeqs":[5],"source":{"kind":"fallback"}}}
+{"type":"step/start","seq":3,"time":1785901435161,"data":{"turn":1,"step":1}}
+{"type":"user/message","seq":4,"time":1785498825917,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"3b04578e-7b22-4b44-b4cd-ef9d4d26fe8b"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785901435161,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"ac92e76e-4861-47a6-87f8-4e9ca904eb24"},"surfaceOp":"append"}
+{"type":"user/message","seq":6,"time":1785730478198,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"d6d78330-05c0-4ebd-9e29-595df6440250"},"surfaceOp":"append"}
+{"type":"session/title","seq":7,"time":1785730478198,"data":{"title":"Using ONE run_code program, call","messageSeqs":[4],"source":{"kind":"fallback"}}}
{"type":"request/header","seq":8,"time":1785498825920,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","seq":9,"time":1785730478199,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
{"type":"assistant/chunk","seq":10,"time":1785014475638,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -14,28 +14,21 @@
{"type":"assistant/chunk","seq":12,"time":1785014475639,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}}}}
{"type":"assistant/chunk","seq":13,"time":1785014475639,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":14,"time":1785014475639,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":15,"time":1785733131056,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b6030d5a-27fb-42a4-b1a4-5e2893e4dfe2"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
+{"type":"assistant/message","seq":15,"time":1785733131056,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"b9402d85-58bd-4881-b890-0b186f661671"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}
{"type":"tool/call","seq":16,"time":1785733131056,"data":{"turn":1,"step":1,"callId":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}}
{"type":"tool/code-dispatch-start","seq":17,"time":1785733131109,"data":{"parentCallId":"call_workspace_read","subCallId":"call_workspace_read:code:1","name":"read","arguments":{"file_path":"nested/task.txt"}}}
{"type":"tool/code-dispatch","seq":18,"time":1785733131110,"data":{"parentCallId":"call_workspace_read","subCallId":"call_workspace_read:code:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}]}}
-{"type":"tool/result","seq":19,"time":1785733131112,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"{\n \"path\": \"{{cwd}}/nested/task.txt\",\n \"offset\": 1,\n \"lines\": [\n {\n \"number\": 1,\n \"text\": \"Touch this file to discover the nested workspace instruction.\"\n }\n ],\n \"totalLines\": 1\n}"}],"isError":false}],"role":"user","id":"10d0cae5-8ed4-41a3-9696-04069be40100"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
+{"type":"tool/result","seq":19,"time":1785733131112,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"{\n \"path\": \"{{cwd}}/nested/task.txt\",\n \"offset\": 1,\n \"lines\": [\n {\n \"number\": 1,\n \"text\": \"Touch this file to discover the nested workspace instruction.\"\n }\n ],\n \"totalLines\": 1\n}"}],"isError":false}],"role":"user","id":"bde1c12e-44d1-44f7-ba7e-868349ed2b05"}},"sourceEventSeqs":[16],"surfaceOp":"append"}
{"type":"step/end","seq":20,"time":1785733131112,"data":{"turn":1,"step":1}}
-{"type":"agent/inbox/spliced","seq":21,"time":1785733131112,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":22,"time":1785733131116,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"},{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"},{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"9324febc-9fee-483f-b70e-d0d52e90ac20"}]}}
+{"type":"agent/inbox/spliced","seq":21,"time":1785733131112,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"29b0eb87-92d5-4915-ba64-7bd8133ed011"}]}}
+{"type":"agent/inbox/spliced","seq":22,"time":1785733131116,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
{"type":"step/start","seq":23,"time":1785733131123,"data":{"turn":1,"step":2}}
-{"type":"user/message","seq":24,"time":1785733131123,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"5578ed99-ced0-42af-b8f6-651e8aaf72e5"},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":25,"time":1785014475805,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":1}}}}
-{"type":"assistant/chunk","seq":26,"time":1785014475806,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":27,"time":1785733131123,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4e3a19b8-1c6f-4005-a336-272245160fa3"},"usage":{"inputTokens":10,"outputTokens":1}},"sourceEventSeqs":[25,26],"surfaceOp":"append"}
-{"type":"step/end","seq":28,"time":1785733131123,"data":{"turn":1,"step":2}}
-{"type":"agent/inbox/spliced","seq":29,"time":1785733131123,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":30,"time":1785733131134,"data":{"turn":1,"step":3}}
-{"type":"user/message","seq":31,"time":1785733131134,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"},{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"},{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"9324febc-9fee-483f-b70e-d0d52e90ac20"},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":32,"time":1785014475887,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":33,"time":1785014475887,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}}}
-{"type":"assistant/chunk","seq":34,"time":1785014475887,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}}}}
-{"type":"assistant/chunk","seq":35,"time":1785014475888,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
-{"type":"assistant/chunk","seq":36,"time":1785014475930,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":37,"time":1785733131134,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"2cf342f3-fa45-4472-8304-f8c947e825f8"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"}
-{"type":"step/end","seq":38,"time":1785733131135,"data":{"turn":1,"step":3}}
-{"type":"turn/end","seq":39,"time":1785733131135,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"user/message","seq":24,"time":1785733131123,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"29b0eb87-92d5-4915-ba64-7bd8133ed011"},"surfaceOp":"append"}
+{"type":"assistant/chunk","seq":25,"time":1785014475805,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":26,"time":1785014475806,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}}}
+{"type":"assistant/chunk","seq":27,"time":1785901435233,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}}}}
+{"type":"assistant/chunk","seq":28,"time":1785901435233,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
+{"type":"assistant/chunk","seq":29,"time":1785901435233,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/message","seq":30,"time":1785901435233,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"**Code Mode workspace handshake:** `CODE_MODE_CONTEXT_OK`"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"add632ac-e646-4e50-84d3-96a084427a01"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[25,26,27,28,29],"surfaceOp":"append"}
+{"type":"step/end","seq":31,"time":1785901435233,"data":{"turn":1,"step":2}}
+{"type":"turn/end","seq":32,"time":1785901435234,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/workspace-context/replay.override.json b/examples/acp-agent/tests/snapshots/workspace-context/replay.override.json
index d198c8f7aa..a8ba5d718f 100644
--- a/examples/acp-agent/tests/snapshots/workspace-context/replay.override.json
+++ b/examples/acp-agent/tests/snapshots/workspace-context/replay.override.json
@@ -19,16 +19,6 @@
{ "type": "finish", "reason": { "kind": "tool-calls" } }
]
},
- {
- "kind": "chunks",
- "chunks": [
- { "type": "block-start", "index": 0, "blockType": "text" },
- { "type": "text-delta", "index": 0, "text": "DONE" },
- { "type": "block-end", "index": 0, "block": { "type": "text", "text": "DONE" } },
- { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 2 } },
- { "type": "finish", "reason": { "kind": "stop" } }
- ]
- },
{
"kind": "chunks",
"chunks": [
diff --git a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl
index 3e885e8cb7..9fffa3e640 100644
--- a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl
+++ b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl
@@ -1,12 +1,12 @@
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
-{"type":"agent/inbox/spliced","seq":0,"time":1785498790330,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Read nested/task.txt, then read scope/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"c692a4e6-491f-49e2-8e50-9019b7c34ee7"}]}}
+{"type":"agent/inbox/spliced","seq":0,"time":1785498790330,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Read nested/task.txt, then read scope/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"22938d3b-c065-46c8-acb7-18f758285842"}]}}
{"type":"turn/start","seq":1,"time":1785821400350,"data":{"turn":1}}
{"type":"agent/inbox/spliced","seq":2,"time":1785498790356,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":3,"time":1785821400378,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"ee54ddf0-5cfd-46f6-b7ab-465184a4a43b"}]}}
-{"type":"step/start","seq":4,"time":1784903339799,"data":{"turn":1,"step":1}}
-{"type":"user/message","seq":5,"time":1785498790356,"data":{"content":[{"type":"text","text":"Read nested/task.txt, then read scope/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"c692a4e6-491f-49e2-8e50-9019b7c34ee7"},"surfaceOp":"append"}
-{"type":"user/message","seq":6,"time":1785730689193,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"0b6bc3b0-57bc-4a60-bc46-992b6a29b361"},"surfaceOp":"append"}
-{"type":"session/title","seq":7,"time":1785730689193,"data":{"title":"Read nested/task.txt, then read scope/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"22938d3b-c065-46c8-acb7-18f758285842"},"surfaceOp":"append"}
+{"type":"user/message","seq":5,"time":1785901433982,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"ec039e95-6864-49ef-ad23-4f65b331dc29"},"surfaceOp":"append"}
+{"type":"user/message","seq":6,"time":1785730689193,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"cbea9bd9-3e08-48bf-951f-fe3e5aa4b5d9"},"surfaceOp":"append"}
+{"type":"session/title","seq":7,"time":1785730689193,"data":{"title":"Read nested/task.txt, then read scope{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"c03ad3ab-6cf8-404a-bf96-c676ae92401b"},"meta":{"path":"{{cwd}}/nested/task.txt","offset":1,"lines":[{"number":1,"text":"snapshot task"}],"totalLines":1}},"sourceEventSeqs":[16],"surfaceOp":"append"}
+{"type":"tool/result","seq":17,"time":1785730689204,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"260bbd5d-4496-40cf-b987-d1d944d93cf1"},"meta":{"path":"{{cwd}}/nested/task.txt","offset":1,"lines":[{"number":1,"text":"snapshot task"}],"totalLines":1}},"sourceEventSeqs":[16],"surfaceOp":"append"}
{"type":"step/end","seq":18,"time":1785498790369,"data":{"turn":1,"step":1}}
-{"type":"agent/inbox/spliced","seq":19,"time":1785498790369,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":20,"time":1785730689207,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"},{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"},{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"7d9270dd-ec9a-4a15-95b0-75745512476b"}]}}
+{"type":"agent/inbox/spliced","seq":19,"time":1785498790369,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"}]}}
+{"type":"agent/inbox/spliced","seq":20,"time":1785730689207,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
{"type":"step/start","seq":21,"time":1785730689212,"data":{"turn":1,"step":2}}
-{"type":"user/message","seq":22,"time":1785498790377,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"ee54ddf0-5cfd-46f6-b7ab-465184a4a43b"},"surfaceOp":"append"}
+{"type":"user/message","seq":22,"time":1785498790377,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"10cdaa4b-9654-420e-afca-3cfb07e26754"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":23,"time":1784903339821,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","seq":24,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_delimiter_read","name":"read","argumentsDelta":"{\"file_path\":\"scope/task.txt\"}"}}}
{"type":"assistant/chunk","seq":25,"time":1785498790377,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope/task.txt\"}"}}}}
{"type":"assistant/chunk","seq":26,"time":1785498790378,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
{"type":"assistant/chunk","seq":27,"time":1785498790378,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/message","seq":28,"time":1785498790378,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ee9963f1-174b-4d7d-96b6-c7ea70000d99"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[23,24,25,26,27],"surfaceOp":"append"}
+{"type":"assistant/message","seq":28,"time":1785498790378,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"43385fc1-54b4-4a8a-82ca-d9111c766f48"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[23,24,25,26,27],"surfaceOp":"append"}
{"type":"tool/call","seq":29,"time":1785498790378,"data":{"turn":1,"step":2,"callId":"call_workspace_delimiter_read","name":"read","arguments":"{\"file_path\":\"scope/task.txt\"}"}}
-{"type":"tool/result","seq":30,"time":1785498790388,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_workspace_delimiter_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_delimiter_read","content":[{"type":"text","text":"{{cwd}}/scope/task.txt\nfile\n\n1: delimiter path snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"b2ca34bb-6d9d-480b-a38f-ea3ce5b27948"},"meta":{"path":"{{cwd}}/scope/task.txt","offset":1,"lines":[{"number":1,"text":"delimiter path snapshot task"}],"totalLines":1}},"sourceEventSeqs":[29],"surfaceOp":"append"}
+{"type":"tool/result","seq":30,"time":1785498790388,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"call_workspace_delimiter_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_delimiter_read","content":[{"type":"text","text":"{{cwd}}/scope/task.txt\nfile\n\n1: delimiter path snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false}],"role":"user","id":"3a62b23b-d165-4c6d-a028-e171c4b2d7fc"},"meta":{"path":"{{cwd}}/scope/task.txt","offset":1,"lines":[{"number":1,"text":"delimiter path snapshot task"}],"totalLines":1}},"sourceEventSeqs":[29],"surfaceOp":"append"}
{"type":"step/end","seq":31,"time":1785730689220,"data":{"turn":1,"step":2}}
-{"type":"agent/inbox/spliced","seq":32,"time":1785730689220,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
-{"type":"agent/inbox/spliced","seq":33,"time":1785498790389,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"},{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"7b7f8187-8c22-442a-ae8c-1c1f392e250b"}]}}
+{"type":"agent/inbox/spliced","seq":32,"time":1785730689220,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"}]}}
+{"type":"agent/inbox/spliced","seq":33,"time":1785498790389,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}}
{"type":"step/start","seq":34,"time":1785498790396,"data":{"turn":1,"step":3}}
-{"type":"user/message","seq":35,"time":1785498790396,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"},{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"},{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]},"role":"user","id":"7d9270dd-ec9a-4a15-95b0-75745512476b"},"surfaceOp":"append"}
+{"type":"user/message","seq":35,"time":1785498790396,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"01c3af61-4567-4be2-b776-21f04ddc9cba"},"surfaceOp":"append"}
{"type":"assistant/chunk","seq":36,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","seq":37,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
{"type":"assistant/chunk","seq":38,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
{"type":"assistant/chunk","seq":39,"time":1785498790396,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
{"type":"assistant/chunk","seq":40,"time":1785498790397,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":41,"time":1785498790397,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"811740d5-6b8b-4073-84d3-c1e2465dd04d"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"}
+{"type":"assistant/message","seq":41,"time":1785498790397,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"f82f5200-090f-4e68-a016-963ab7166d4f"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[36,37,38,39,40],"surfaceOp":"append"}
{"type":"step/end","seq":42,"time":1785498790397,"data":{"turn":1,"step":3}}
-{"type":"agent/inbox/spliced","seq":43,"time":1785730689228,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
-{"type":"step/start","seq":44,"time":1785730689236,"data":{"turn":1,"step":4}}
-{"type":"user/message","seq":45,"time":1785730689236,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n\nAdditional instructions from: scope<\\/system-reminder>/AGENTS.md\n\nThese instructions apply to work under `scope<\\/system-reminder>`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nDelimiter path snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"},{"action":"set","scope":"scope\u0000AGENTS.md","path":"scope/AGENTS.md","digest":"38803cd13e2dff9105ba5fbbc703fe27e989e26e"}]},"role":"user","id":"7b7f8187-8c22-442a-ae8c-1c1f392e250b"},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":46,"time":1785730689236,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":47,"time":1785730689236,"data":{"turn":1,"step":4,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}}
-{"type":"assistant/chunk","seq":48,"time":1785730689236,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
-{"type":"assistant/chunk","seq":49,"time":1785730689236,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}}
-{"type":"assistant/chunk","seq":50,"time":1785730689236,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
-{"type":"assistant/message","seq":51,"time":1785730689236,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4082d4e8-9244-45e6-b71d-cd6728f372a3"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[46,47,48,49,50],"surfaceOp":"append"}
-{"type":"step/end","seq":52,"time":1785730689237,"data":{"turn":1,"step":4}}
-{"type":"turn/end","seq":53,"time":1785730689237,"data":{"turn":1,"reason":{"kind":"completed"}}}
+{"type":"turn/end","seq":43,"time":1785901434023,"data":{"turn":1,"reason":{"kind":"completed"}}}
diff --git a/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl
index 6e66b486ff..82ae8907ca 100644
--- a/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl
+++ b/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl
@@ -1,5 +1,4 @@
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}}
{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}}
{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
-{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"DONE"}}}}
{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}
diff --git a/packages/context/workspace-context/README.i18n.yaml b/packages/context/workspace-context/README.i18n.yaml
index 52a3ede0de..bd8c55d78c 100644
--- a/packages/context/workspace-context/README.i18n.yaml
+++ b/packages/context/workspace-context/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/context/workspace-context/README.md
-README.md: 954a032e600579dda7f11ce3d11224aff7f7666e
-README.zh.md: ba945cfbf83398466c7888c5e95300dd8570870d
+README.md: 7add269c7a1b38e9624cf6d0368151661bc72a6b
+README.zh.md: 3456c5c36275e8e521b312b1b0b7e1dd102ca23c
diff --git a/packages/context/workspace-context/README.md b/packages/context/workspace-context/README.md
index 954a032e60..7add269c7a 100644
--- a/packages/context/workspace-context/README.md
+++ b/packages/context/workspace-context/README.md
@@ -6,7 +6,7 @@ Per-session workspace instruction loading for `AGENTS.md`-compatible files. The
## Lifecycle
-The first eligible `agent/pre-step` of each live session composes the baseline and prepends it to the agent's `next-step` inbox without changing the current `PreStepDecision`. It reads `$DSH_HOME/AGENTS.md` followed by, in each directory from the project root to `agent.session.header.cwd`, every existing base candidate and then every existing local-overlay candidate. Within one directory, candidates whose content is byte-identical after trimming leading and trailing whitespace collapse to the earliest candidate in configured order, so a `CLAUDE.md` that merely duplicates its sibling `AGENTS.md` is rendered once. Because the loop claims before running pre-step, the new context may enter a later request. If a previously queued workspace context is still pending, the plugin removes and replaces that exact inbox item instead of accumulating duplicates.
+The first eligible `agent/pre-step` of each live session composes the baseline. When the downstream decision enters a nonempty first-step batch, the plugin folds the baseline into that final batch right after the claimed prompt, so the direct prompt and the durable baseline enter step 1 and reach the first request together. A rejected or empty first-step decision leaves the baseline in the agent's `next-step` inbox for a later wakeup. The loader reads `$DSH_HOME/AGENTS.md` followed by, in each directory from the project root to `agent.session.header.cwd`, every existing base candidate and then every existing local-overlay candidate. Within one directory, candidates whose content is byte-identical after trimming leading and trailing whitespace collapse to the earliest candidate in configured order, so a `CLAUDE.md` that merely duplicates its sibling `AGENTS.md` is rendered once. If a previously queued workspace context is still pending, the plugin removes and replaces that exact inbox item instead of accumulating duplicates.
The plugin also listens on `tools/post-execute` for successful first-party `read`, `write`, and `edit` calls. Each touch checks newly reached descendant scopes and every previously loaded scope. Each configured candidate name is an independent scope in its directory: a newly present file is attached through the result's `additionalContexts`; a changed file appends a replacement; a file that disappears or becomes a per-directory duplicate of an earlier candidate appends a removal notice. Native calls and Code Mode sub-dispatches share this path: `run_code` defers each nested context until its outer result, so the loop still appends updates after tool-call/result adjacency is complete. This follows structured filesystem activity rather than shell `cd`, because each local bash call starts a fresh shell and parsing arbitrary shell syntax would be unreliable.
@@ -48,11 +48,11 @@ The plugin owns the complete `` framing, and every injected `us
## State And Refresh
-Model-visible text contains no hidden state markers. Each baseline or dynamic context event instead carries a typed `workspace-instructions` source with a list of `{ action, scope, path, digest? }` changes; the complete startup or resume baseline also carries `baseline: true`. A matching durable `user/message` confirms a queued baseline and its candidate versions. If pre-step rejects or rewrites away a claimed workspace message, a later boundary recomposes and queues the current context again. On every relevant tool touch, the plugin reconstructs loaded state from its visible session events and overlays a short in-memory pending window for context present on the immutable top-level `tools/result` but not yet appended by the loop. If the owning `step/end` arrives before a matching dynamic context reaches the log, the plugin clears that pending transition and its version fast path so the next successful touch can load it again. Nested Code Mode results stage pending changes under the outer execution token for same-run duplicate suppression; the outer result rolls that state back and recommits only contexts that survived outer policy.
+Model-visible text contains no hidden state markers. Each baseline or dynamic context event instead carries a typed `workspace-instructions` source with a list of `{ action, scope, path, digest? }` changes; the complete startup or resume baseline also carries `baseline: true`. A matching durable `user/message` confirms a queued baseline and its candidate versions. An entering pre-step folds newly composed context into its final batch immediately after the claimed messages and removes the pending inbox copy; rejection keeps the current context queued. If a listener rewrites away a claimed workspace message without entering its replacement, a later boundary recomposes the current context. On every relevant tool touch, the plugin reconstructs loaded state from its visible session events and overlays a short in-memory pending window for context present on the immutable top-level `tools/result` but not yet appended by the loop. If the owning `step/end` arrives before a matching dynamic context reaches the log, the plugin clears that pending transition and its version fast path so the next successful touch can load it again. Nested Code Mode results stage pending changes under the outer execution token for same-run duplicate suppression; the outer result rolls that state back and recommits only contexts that survived outer policy.
An unchanged path and SHA-1 content digest is not injected again. A per-session, per-scope provider cache stores only `{ path, version, digest, trimmedDigest }`: when the provider's opaque `FsVersion` and the effective visible state both match, reconciliation skips the content read; a changed version triggers a bounded read and SHA-1 confirmation before any model-visible update. The `trimmedDigest` — SHA-1 over the whitespace-trimmed content — is the per-directory duplicate key, so an unchanged file can still be removed when an earlier candidate converges on its content. Resume works because SHA-1 state is persisted in the typed source, while an empty in-memory version cache merely causes one confirming read. Compaction re-arms a scope after its context event leaves the visible surface even when the cached version is unchanged. A removal is a tombstone, so a later candidate reappearance is loaded again. Only model-visible changes actually rendered within the byte budget enter the source, pending state, and version cache; an omitted change remains eligible for a later touch, while a same-digest version refresh updates only the provider cache.
-The initial baseline event itself is not rewritten. Its typed changes remain authoritative only while that event is in the visible session surface; the next successful filesystem touch re-adds an unchanged baseline scope after compaction, or appends its replacement or removal. The in-memory scope marker and provider-version cache only select and accelerate probes. A hot plugin remount retains a baseline only when its typed event remains visible, while rebuilding current scope and version tracking; otherwise it queues a current baseline. A resumed loop always recomposes the current baseline and also reconciles still-visible dynamic scopes at its first pre-step, but the resulting inbox context need not enter that same request. There is no file watcher, so an on-disk change becomes visible at the next successful `read`, `write`, or `edit` touch, or when a resumed loop prepares its baseline.
+The initial baseline event itself is not rewritten. Its typed changes remain authoritative only while that event is in the visible session surface; the next successful filesystem touch re-adds an unchanged baseline scope after compaction, or appends its replacement or removal. The in-memory scope marker and provider-version cache only select and accelerate probes. A hot plugin remount retains a baseline only when its typed event remains visible, while rebuilding current scope and version tracking; otherwise it queues a current baseline. A resumed loop always recomposes the current baseline and also reconciles still-visible dynamic scopes at its first pre-step; an entering first request records that context in the same step. There is no file watcher, so an on-disk change becomes visible at the next successful `read`, `write`, or `edit` touch, or when a resumed loop prepares its baseline.
## Configuration
diff --git a/packages/context/workspace-context/README.zh.md b/packages/context/workspace-context/README.zh.md
index ba945cfbf8..3456c5c362 100644
--- a/packages/context/workspace-context/README.zh.md
+++ b/packages/context/workspace-context/README.zh.md
@@ -6,7 +6,7 @@
## 生命周期
-每个实时会话第一次符合条件的 `agent/pre-step` 会组合基线,将它 prepend 到 agent 的 `next-step` inbox,但不改变当前 `PreStepDecision`。它先读取 `$DSH_HOME/AGENTS.md`,随后针对项目根目录到 `agent.session.header.cwd` 的每个目录,先读取每个现有基础候选文件,再读取每个现有本地 overlay 候选文件。同一目录中,如果候选文件在去除首尾空白后字节完全一致,就会按已配置顺序折叠到最早候选文件,因此 `CLAUDE.md` 若只是复制同级 `AGENTS.md`,只会渲染一次。因为循环在运行 pre-step 前已经领取消息,新上下文可能要到后续请求才会进入。若之前排队的 workspace 上下文仍在等待,插件会删除并替换该确切 inbox 条目,而不会不断累积副本。
+每个实时会话第一次符合条件的 `agent/pre-step` 会组合基线。当下游决策让非空的第一步批次进入时,插件会将基线折入最终批次、紧随已领取的直接提示词之后,使直接提示词与持久基线一同进入步骤 1,并共同抵达第一次请求。reject 或空的第一步决策会将基线留在 agent 的 `next-step` inbox,等待后续唤醒。loader 先读取 `$DSH_HOME/AGENTS.md`,随后针对项目根目录到 `agent.session.header.cwd` 的每个目录,先读取每个现有基础候选文件,再读取每个现有本地 overlay 候选文件。同一目录中,如果候选文件在去除首尾空白后字节完全一致,就会按已配置顺序折叠到最早候选文件,因此 `CLAUDE.md` 若只是复制同级 `AGENTS.md`,只会渲染一次。若之前排队的 workspace 上下文仍在等待,插件会删除并替换该确切 inbox 条目,而不会不断累积副本。
该插件还会监听 `tools/post-execute` 中成功的第一方 `read`、`write` 和 `edit` 调用。每次 touch 都会检查新达到的后代 scope 以及之前加载的每个 scope。每个已配置候选名称都是所在目录中的独立 scope:新出现的文件通过结果的 `additionalContexts` 附加;已改变文件追加替换;文件消失或成为同一目录中较早候选文件的重复项时,追加移除通知。原生调用与 Code Mode 子分派共享该路径:`run_code` 将每个嵌套上下文延迟到外层结果,因此 loop 仍会在工具调用/结果相邻关系完成后追加更新。这种发现跟随结构化文件系统活动,而不是 shell `cd`,因为每次本地 bash 调用都启动新 shell,解析任意 shell 语法也不可靠。
@@ -48,11 +48,11 @@ These instructions apply to work under `packages/app`. Use them as guidance when
## 状态与刷新
-模型可见文本不含隐藏状态标记。每个基线或动态上下文事件改为携带带类型的 `workspace-instructions` 来源,其中包含 `{ action, scope, path, digest? }` 变更列表;完整的启动或恢复基线还会携带 `baseline: true`。匹配的持久 `user/message` 会确认已排队基线及其候选版本。若 pre-step reject 或改写掉已领取的 workspace 消息,后续边界会重新组合并排队当前上下文。每次相关工具 touch 时,插件会从可见会话事件重建已加载状态,并叠加一个短暂内存 pending 窗口,用于不可变顶层 `tools/result` 上存在但 loop 尚未追加的上下文。如果所属 `step/end` 在匹配的动态上下文进入日志之前到达,插件会清除该 pending 转换及其版本快速路径,使下一次成功 touch 可以重新加载。嵌套 Code Mode 结果会在外层执行 token 下暂存 pending 变更,用于抑制同次运行中的重复项;外层结果会回滚该状态,再只重新提交经过外层策略的上下文。
+模型可见文本不含隐藏状态标记。每个基线或动态上下文事件改为携带带类型的 `workspace-instructions` 来源,其中包含 `{ action, scope, path, digest? }` 变更列表;完整的启动或恢复基线还会携带 `baseline: true`。匹配的持久 `user/message` 会确认已排队基线及其候选版本。进入步骤的 pre-step 会把新组合的上下文折入最终批次,位置紧随已领取的消息,并移除 inbox 中仍待处理的副本;reject 则让当前上下文继续排队。若监听器改写掉已领取的 workspace 消息,又没有让替代消息进入,后续边界会重新组合当前上下文。每次相关工具 touch 时,插件会从可见会话事件重建已加载状态,并叠加一个短暂内存 pending 窗口,用于不可变顶层 `tools/result` 上存在但 loop 尚未追加的上下文。如果所属 `step/end` 在匹配的动态上下文进入日志之前到达,插件会清除该 pending 转换及其版本快速路径,使下一次成功 touch 可以重新加载。嵌套 Code Mode 结果会在外层执行 token 下暂存 pending 变更,用于抑制同次运行中的重复项;外层结果会回滚该状态,再只重新提交经过外层策略的上下文。
路径与 SHA-1 内容 digest 都未变时,不会重复注入。每会话、每 scope 提供方 cache 只存储 `{ path, version, digest, trimmedDigest }`:当提供方的不透明 `FsVersion` 与有效可见状态都匹配时,对账会跳过内容读取;版本改变会在任何模型可见更新之前触发有界读取与 SHA-1 确认。`trimmedDigest` 是针对去除空白后内容的 SHA-1,也是每目录重复 key,因此较早候选文件与某个未更改文件的内容收敛后,后者仍可被移除。恢复可行,因为 SHA-1 状态持久化在带类型的来源中,而空的内存版本 cache 只会导致一次确认读取。压缩(compaction)会在 scope 的上下文事件离开可见表层后重新启用它,即使缓存版本未变。移除是 tombstone,因此候选文件之后重新出现时会重新加载。只有在字节预算内实际渲染的模型可见变更才会进入来源、pending 状态和版本 cache;已省略变更仍可在后续 touch 处理,而相同 digest 的版本刷新只更新提供方 cache。
-初始基线事件自身不会被改写。其带类型的变更仅在该事件仍位于可见会话表层时才是权威状态;下一次成功的文件系统 touch 会在压缩后重新添加未变的基线 scope,或追加其替换或移除。内存中的 scope 标记和提供方版本 cache 只负责选择探测对象并加速探测。插件热重挂只有在其带类型的事件仍然可见时才保留基线,同时会重建当前 scope 与版本跟踪状态;否则会排队当前基线。恢复的 loop 始终在第一次 pre-step 重新组合当前基线,并对账仍可见的动态 scope,但产生的 inbox 上下文不保证进入同一次请求。没有文件 watcher,因此磁盘变更会在下一次成功 `read`、`write` 或 `edit` touch 时可见,也会在恢复 loop 准备基线时可见。
+初始基线事件自身不会被改写。其带类型的变更仅在该事件仍位于可见会话表层时才是权威状态;下一次成功的文件系统 touch 会在压缩后重新添加未变的基线 scope,或追加其替换或移除。内存中的 scope 标记和提供方版本 cache 只负责选择探测对象并加速探测。插件热重挂只有在其带类型的事件仍然可见时才保留基线,同时会重建当前 scope 与版本跟踪状态;否则会排队当前基线。恢复的 loop 始终在第一次 pre-step 重新组合当前基线,并对账仍可见的动态 scope;首次请求若进入步骤,就会在同一步骤记录该上下文。没有文件 watcher,因此磁盘变更会在下一次成功 `read`、`write` 或 `edit` touch 时可见,也会在恢复 loop 准备基线时可见。
## 配置
diff --git a/packages/context/workspace-context/src/index.ts b/packages/context/workspace-context/src/index.ts
index c1a6e19b54..be9e2aa806 100644
--- a/packages/context/workspace-context/src/index.ts
+++ b/packages/context/workspace-context/src/index.ts
@@ -214,13 +214,31 @@ export function apply(ctx: Context, config: Config): void {
ctx.on('agent/pre-step', async (
agent: Agent,
messages,
- { signal },
+ { step, signal },
next,
): Promise => {
const decision = await next()
await waitForProjections(agent)
- await composeAndSync(agent, signal, messages)
- return decision
+ const pending = agent.inbox.nextStep.filter(isWorkspaceContext)
+ const desired = await compose(agent, signal, messages, pending)
+ signal.throwIfAborted()
+ // An empty first entry owns a no-step turn; keep context pending instead
+ // of turning it into a standalone request. Later entries may be tool continuations.
+ if (decision.kind === 'reject' || (step === 1 && decision.messages.length === 0)) {
+ syncInbox(agent, messages, desired)
+ return decision
+ }
+ // A proceeding step settles the pending context: it either enters below as
+ // `desired`, or its payload is already covered by the batch, so nothing stays pending.
+ for (const message of pending) agent.inbox.remove(message.id)
+ if (desired === undefined || decision.messages.some(message => sameContextPayload(message, desired))) {
+ return decision
+ }
+ // Fold the context right after the claimed batch, so the direct prompt
+ // precedes it and the driver-appended runtime context follows it.
+ const lastClaimedIndex = decision.messages.findLastIndex(message => messages.includes(message))
+ const entered = decision.messages.toSpliced(lastClaimedIndex + 1, 0, desired)
+ return { kind: 'enter', messages: entered }
})
ctx.on('tools/result', (exec: ToolExecution, result: ToolExecutionResult) => {
diff --git a/packages/context/workspace-context/tests/workspace-context.spec.ts b/packages/context/workspace-context/tests/workspace-context.spec.ts
index 661767f4e4..03d9e38bd6 100644
--- a/packages/context/workspace-context/tests/workspace-context.spec.ts
+++ b/packages/context/workspace-context/tests/workspace-context.spec.ts
@@ -40,7 +40,7 @@ import {
type InstructionVersionCache,
} from '../src/state.ts'
import { resolveConfig } from '../src/config.ts'
-import { candidateScopeKey, renderInstructionChanges } from '../src/render.ts'
+import { candidateScopeKey, renderInstructionChanges, USER_GLOBAL_DIRECTORY, USER_GLOBAL_FILE } from '../src/render.ts'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
/** Per-candidate reconciliation scope key: directory paired with the file name. */
@@ -1035,26 +1035,20 @@ describe('workspace context request injection', () => {
() => Promise.resolve({ kind: 'enter' as const, messages: staleClaim }),
)
- expect(staleDecision).toEqual({ kind: 'enter', messages: staleClaim })
- expect(resumed.inbox.nextStep).toHaveLength(1)
- const replacement = resumed.inbox.nextStep[0]
+ if (staleDecision.kind !== 'enter') throw new Error('recovered baseline was rejected')
+ expect(staleDecision.messages).toHaveLength(2)
+ expect(staleDecision.messages[0]).toBe(staleClaim[0])
+ const replacement = staleDecision.messages[1]
expect(replacement?.id).not.toBe(stale?.id)
expect(blocksText(replacement?.content)).toContain('new repo rule')
expect(blocksText(replacement?.content)).not.toContain('old repo rule')
+ expect(resumed.inbox.nextStep).toHaveLength(0)
- const replacementClaim = resumed.inbox.claim('next-step')
- const replacementDecision = await agentEvents(ctx, resumed).waterfall(
- 'agent/pre-step',
- replacementClaim,
- { turn: 1, step: 2, signal: AbortSignal.timeout(1000) },
- () => Promise.resolve({ kind: 'enter' as const, messages: replacementClaim }),
- )
- if (replacementDecision.kind !== 'enter') throw new Error('replacement baseline was rejected')
- for (const message of replacementDecision.messages) {
+ for (const message of staleDecision.messages) {
const event = resumed.session.append('user/message', message, { surfaceOp: 'append' })
ctx.emit('session/event', resumed.session, event)
}
- expect(baselineEvents(resumed)).toHaveLength(0)
+ expect(baselineEvents(resumed)).toHaveLength(1)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1177,7 +1171,7 @@ describe('workspace context request injection', () => {
}
})
- it('leaves the current pre-step batch unchanged while queuing the baseline', async () => {
+ it('enters the baseline right after the claimed prompt in the first pre-step without queuing another step', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
@@ -1199,9 +1193,40 @@ describe('workspace context request injection', () => {
() => Promise.resolve(downstream),
)
- expect(decision).toBe(downstream)
- expect(agent.inbox.nextStep).toHaveLength(1)
- expect(blocksText(agent.inbox.nextStep[0]?.content)).toContain('Instructions from: AGENTS.md')
+ expect(decision).toMatchObject({ kind: 'enter' })
+ if (decision.kind !== 'enter') throw new Error('workspace baseline was rejected')
+ expect(decision.messages).toHaveLength(2)
+ expect(decision.messages[0]).toBe(prompt)
+ expect(decision.messages[1]?.source).toMatchObject({ kind: 'workspace-instructions', baseline: true })
+ expect(blocksText(decision.messages[1]?.content)).toContain('Instructions from: AGENTS.md')
+ expect(agent.inbox.nextStep).toHaveLength(0)
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ await rm(home, { recursive: true, force: true })
+ }
+ })
+
+ it('reports a changed user-global instruction through the visible-scope reconcile path', async () => {
+ const root = await tempRepo()
+ const home = await tempRepo()
+ try {
+ await mkdir(join(root, '.git'), { recursive: true })
+ await write(join(root, 'AGENTS.md'), 'repo rule')
+ await write(join(home, 'AGENTS.md'), 'global rule')
+ const ctx = new Context()
+ await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
+ const agent = stubAgent(root)
+ await composeBaselinePrefix(ctx, agent)
+
+ await write(join(home, 'AGENTS.md'), 'updated global rule')
+ await syncWorkspaceContext(ctx, agent)
+
+ const pending = await workspaceContextOf(agent)
+ expect(pending?.source).toMatchObject({
+ kind: 'workspace-instructions',
+ changes: [{ action: 'replace', scope: sk(USER_GLOBAL_DIRECTORY, USER_GLOBAL_FILE) }],
+ })
+ expect(blocksText(pending?.content)).toContain('updated global rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -2693,6 +2718,51 @@ describe('dynamic nested workspace context injection', () => {
},
)
+ it('skips visible baseline scopes when baseline scopes are excluded from reconciliation', async () => {
+ const root = join(await tempRepo(), 'virtual-repo')
+ const home = join(await tempRepo(), 'virtual-home')
+ const ctx = new Context()
+ try {
+ await ctx.plugin(RecordingFileSystem)
+ const fs = ctx.fs as RecordingFileSystem
+ fs.entries.set(join(root, '.git'), { type: 'directory' })
+ fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'repo rule' })
+ const agent = stubAgent(root)
+ const rootScope = sk('.', 'AGENTS.md')
+ const loaded = baselineInstructionState([{
+ absolutePath: join(root, 'AGENTS.md'),
+ displayPath: 'AGENTS.md',
+ content: 'repo rule',
+ version: FsVersion('loaded-agents'),
+ }])
+ const previous = loaded.changes.get(rootScope)
+ if (previous === undefined) throw new Error('missing AGENTS.md baseline state')
+ const authoritative = createUserMessage({
+ content: [{ type: 'text', text: 'repo rule' }],
+ source: { kind: 'workspace-instructions', changes: [previous] },
+ })
+ agent.session.append('user/message', authoritative, { surfaceOp: 'append' })
+ const resolved = resolveConfig({ dshHome: home, maxBytes: 65536, localInstructionFileCandidates: [] })
+ const cache: InstructionVersionCache = new WeakMap()
+ cache.set(agent.session, new Map(loaded.versions))
+ const options = {
+ authorityMessages: [],
+ scopeMessages: [],
+ touchedPaths: [],
+ includeBaselineScopes: false,
+ signal: testToolSignal,
+ }
+
+ const result = await reconcileInstructionContext(agent, resolved, cache, fs, options)
+
+ expect(result).toBeUndefined()
+ } finally {
+ await ctx.fiber.dispose()
+ await rm(dirname(root), { recursive: true, force: true })
+ await rm(dirname(home), { recursive: true, force: true })
+ }
+ })
+
it('removes a previously rendered sibling once its content becomes a duplicate of an earlier candidate', async () => {
const root = await tempRepo()
const home = await tempRepo()
@@ -3885,7 +3955,7 @@ describe('workspace context inbox synchronization', () => {
}
})
- it('keeps a claimed batch unchanged and queues an offline correction', async () => {
+ it('enters an offline correction immediately after its claimed stale context', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
@@ -3906,10 +3976,12 @@ describe('workspace context inbox synchronization', () => {
async () => downstream,
)
- expect(decision).toBe(downstream)
- expect(blocksText(decision.kind === 'enter' ? decision.messages[0]?.content : [])).toContain('old claimed rule')
- expect(agent.inbox.nextStep).toHaveLength(1)
- expect(blocksText(agent.inbox.nextStep[0]?.content)).toContain('new claimed rule with more detail')
+ if (decision.kind !== 'enter') throw new Error('offline correction was rejected')
+ expect(decision.messages).toHaveLength(2)
+ expect(decision.messages[0]).toBe(claimed[0])
+ expect(blocksText(decision.messages[0]?.content)).toContain('old claimed rule')
+ expect(blocksText(decision.messages[1]?.content)).toContain('new claimed rule with more detail')
+ expect(agent.inbox.nextStep).toHaveLength(0)
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })