Merge branch 'master' into sdk/ts-client-and-subagent

This commit is contained in:
Tianyi Cui
2026-07-27 22:41:23 +08:00
committed by GitHub
386 changed files with 7849 additions and 9915 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-06-18-compaction-capability-seam.md: a263b5e7d0245bd1279024a50e05b2f33edad521
2026-06-18-compaction-capability-seam.zh.md: df0cf9d9131978e608d47124ba0f0db0343ee12a
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
2026-06-18-compaction-capability-seam.md: 3c219b734e148b963fb5857de89c16f28c2bd402
2026-06-18-compaction-capability-seam.zh.md: b2c7e9720b596705b60a284e6ccf1448a782b7fc

View File

@@ -39,7 +39,7 @@ An earlier draft put the full algorithm (the retention walk, token-summing, text
Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, optional `ctx.toolResultPrune` rewriting runs before summary selection; compact-basic remeasures the durable surface and skips summarization if pruning restores safe pressure.
Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic prunes before forcing one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists; the loop then opens a new numbered step and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
Canonical provider context overflow takes a separate path. The failed step closes and `agent/request-error` receives the original request error. Compact-basic owns its per-agent overflow count, prunes before forcing one useful balanced reduction, and returns `{ kind: 'retry' }` only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists. The loop then closes the failed turn, opens a new numbered retry turn, and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
```
assistant/message → tool/result/context/steering

View File

@@ -39,7 +39,7 @@ Status: implemented
成功调用的压力检查不能在步骤前运行,因为最终的 `agent/request` 路由、提供方输出、工具结果、缓冲上下文与 steering 当时尚不存在。串行的 `agent/post-step(agent, turn, step, signal)` 会在这些事实持久化后、`step/end` 之前触发。`dsh-compact-basic` 通过 `ctx.tokenMeter` 测量规范的已记录请求,因此下一个请求无需推测性覆盖信封即可看到任何替换。压力达到条件后,可选的 `ctx.toolResultPrune` 重写在摘要范围选择前运行compact-basic 重新测量持久 surface如果修剪恢复到安全压力便跳过摘要生成。
规范的提供方上下文溢出走另一条路径。失败步骤先关闭,`agent/request-error` 接收原始请求错误与连续重试次数,compact-basic 在强制执行一次有效且平衡的缩减前先修剪仅当 `session.surface.replaceGeneration` 增加时,它才返回 retry;这包括没有摘要范围时仅修剪取得的进展。随后循环开启新的编号步骤,并从持久日志重建请求。没有替换、任何替换前的恢复失败、取消、耗尽的上限或无关错误都会保留原始提供方失败。如果修剪已经推进 generation而后续摘要工作失败恢复会从该持久的已修剪 surface 重试,除非取消或资源释放胜出。完整生命周期决策见[调用后恢复 Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)。
规范的提供方上下文溢出走另一条路径。失败步骤先关闭,`agent/request-error` 接收原始请求错误compact-basic 自行持有按 agent 计的溢出次数,在强制执行一次有效且平衡的缩减前先修剪,且仅当 `session.surface.replaceGeneration` 增加时才返回 `{ kind: 'retry' }`;这包括没有摘要范围时仅修剪取得的进展。随后循环关闭失败轮次,开启新的编号重试轮次,并从持久日志重建请求。没有替换、任何替换前的恢复失败、取消、耗尽的上限或无关错误都会保留原始提供方失败。如果修剪已经推进 generation而后续摘要工作失败恢复会从该持久的已修剪 surface 重试,除非取消或资源释放胜出。完整生命周期决策见[调用后恢复 Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)。
```
assistant/message → tool/result/context/steering

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-06-24-workspace-context.md: 6acdb6241bcc57250e217cfc8856e8b0598d4622
2026-06-24-workspace-context.zh.md: f165d08108931df21697c7895523f10ef816297f
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-24-workspace-context.md
2026-06-24-workspace-context.md: f86e227be615c9b54e2a9013d3c7dca75d3975f0
2026-06-24-workspace-context.zh.md: 154b5260955570e2de3c88d98286c5ea6afaa3b5

View File

@@ -10,13 +10,13 @@ Repository guidance such as `AGENTS.md` belongs in a coding session's effective
Neighboring products establish useful conventions but differ in details. Codex treats `AGENTS.md` as native, Claude Code uses `CLAUDE.md` and familiar system-reminder-style user context, and opencode supports both names with one winner per directory plus lazy nested discovery. The harness needs cross-tool compatibility without loading duplicate or contradictory files from the same scope.
The lifecycle has two distinct classes of content. The initial applicable chain is stable enough to live in the request prefix and benefit from provider prefix caching. Nested files, edits, candidate switches, and removals happen after the session starts and belong in durable append-only history rather than the frozen prefix.
The lifecycle has two distinct classes of content. The initial applicable chain is injected once before the first request. Nested files, edits, candidate switches, and removals happen later and join the same durable append-only history.
## Decision
The implementation lives in `packages/context/workspace-context` as `@deepseek-ai/dsh-workspace-context`. It is a request-context extension, not a core service or a filesystem backend. The shared demo spine and Host Runtime mount it from an explicit `{ maxBytes } | false` deployment choice; `dsh web` enables a 65,536-byte budget while the Host Runtime's headless consumer disables it. The plugin consumes `agent/session-prefix`, `tools/post-execute`, and the optional `ctx.fs` capability.
The implementation lives in `packages/context/workspace-context` as `@deepseek-ai/dsh-workspace-context`. It is a request-context extension, not a core service or a filesystem backend. The shared demo spine and Host Runtime mount it from an explicit `{ maxBytes } | false` deployment choice; `dsh web` enables a 65,536-byte budget while the Host Runtime's headless consumer disables it. The plugin consumes `agent/step`, `tools/post-execute`, and the optional `ctx.fs` capability.
The plugin does not statically inject `fs`. Providerless product trees therefore boot normally and the plugin no-ops until a filesystem provider exists. All production reads go through that provider. Candidate probes resolve each path and stat the result, so a final-component symlink is followed to its target: a link to a regular file loads, while a missing path or a non-file target is a confirmed absence. Following repository-owned links across the trust boundary is a deliberate reversal of the original no-follow probe; the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns that decision and its residual risk. The session-prefix signal and dynamic tool execution signal propagate through resolution, metadata probes, and streaming reads, so cancellation does not wait for an unrelated filesystem scan. A resolve or stat exception is classified as unavailable: it skips only that candidate and is never interpreted as the deletion of an already-loaded scope.
The plugin does not statically inject `fs`. Providerless product trees therefore boot normally and the plugin no-ops until a filesystem provider exists. All production reads go through that provider. Candidate probes resolve each path and stat the result, so a final-component symlink is followed to its target: a link to a regular file loads, while a missing path or a non-file target is a confirmed absence. Following repository-owned links across the trust boundary is a deliberate reversal of the original no-follow probe; the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns that decision and its residual risk. The step signal and dynamic tool execution signal propagate through resolution, metadata probes, and streaming reads, so cancellation does not wait for an unrelated filesystem scan. A resolve or stat exception is classified as unavailable: it skips only that candidate and is never interpreted as the deletion of an already-loaded scope.
### File Names And Precedence
@@ -26,13 +26,13 @@ Candidate entries are same-directory file names. Empty entries, `.`/`..`, and en
The user-global file is fixed at `$DSH_HOME/AGENTS.md`, is not affected by either candidate list, and has no local overlay. `$DSH_HOME` defaults to `~/.dsh`, matching the harness-level home role of `~/.codex` or `~/.claude` rather than introducing a plugin-specific home. Tilde expansion and the default live in `dsh-paths` so future harness features share the same convention.
### Baseline Prefix
### Baseline Injection
On the first request of an agent-loop instance, the plugin contributes one user-role message through `agent/session-prefix`. 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 one candidate 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.
At the first `agent/step` of an agent-loop instance, the plugin injects one sourced user-role message before the request is derived. 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 one candidate 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.
The plugin prepends its contribution before `await next()` returns, so session-prefix contributions appear in plugin registration order. In the product spine workspace instructions are registered before a skills catalog and therefore appear first. The loop deep-freezes the composed prefix, logs it in `EpochHeader.messagePrefix`, and reuses it verbatim for that instance. It is request state, not `Session.deriveMessages()` history.
The injection 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. In the product spine workspace instructions are registered before the skills catalog, so their `agent/step` listener injects first. The loop drains both messages before deriving the first request.
A resumed agent creates a new loop instance and recomposes the baseline from current files, with the new prefix anchored by the resume request header. This permits current baseline content on resume without mutating a prefix already used by an earlier instance.
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.
The baseline is a user-role `<system-reminder>` with `Instructions from: <path>` sections and explicit authority and precedence language. This familiar model-facing frame avoids a harness-specific XML vocabulary. Project paths are root-relative and the user-global path is `~/.dsh/AGENTS.md` for the default home or `$DSH_HOME/AGENTS.md` for a configured home. A literal `</system-reminder>` inside file content is escaped. The package README owns the exact current [prompt shape](../../../../packages/context/workspace-context/README.md#prompt-shape).
@@ -42,21 +42,21 @@ After a successful first-party `read`, `write`, or `edit` call, the `tools/post-
A content edit appends `Updated instructions from: <path>`, states that the new content replaces the previous content, and includes the complete current file. If precedence changes from one candidate to another, the message also names the previous path and says it no longer applies. If no candidate remains, the plugin appends `Instructions removed: <path>` and states that the previously loaded instructions no longer apply.
Dynamic messages carry their complete system-reminder framing in `content`, and every `context/message` reaches the model verbatim as a user-role message (there is no core wrapper to opt out of). `context/message.meta` carries opaque JSON state that is persisted but never rendered to the model.
Baseline and dynamic messages carry their complete system-reminder framing in `content`, and every sourced `user/message` reaches the model verbatim (there is no core wrapper to opt out of). The typed `workspace-instructions` source carries persisted state that is never rendered to the model.
Shell commands are not discovery triggers. Local bash calls start fresh shells, and inferring reached paths from arbitrary command strings would require shell semantics the prompt plugin does not own.
### Duplicate Suppression And Change Detection
Every dynamic workspace context event stores versioned metadata with `{ action, scope, path, digest? }`, where `digest` is SHA-1 over the loaded content. The model-facing prompt has no HTML comments, hidden markers, or headings that are parsed back into state.
Every workspace context event stores versioned metadata with `{ action, scope, path, digest? }`, where `digest` is SHA-1 over the loaded content. Baselines additionally carry `baseline: true`. The model-facing prompt has no HTML comments, hidden markers, or headings that are parsed back into state.
At reconciliation time the plugin scans plugin-owned `context/message` events and derives the latest state for each visible scope. A short per-session pending map begins only after the immutable top-level `tools/result` proves an `additionalContexts` entry survived every post-execute listener, then covers the interval before the loop appends that context to the log. Each entry records the open `{ turn, step }`: an equal durable `context/message` at or after its sequence boundary confirms and removes it, while a matching `step/end` arriving first means the loop discarded its context buffer, so the plugin removes both the pending entry and its version-cache fast path. A nested Code Mode result stages its changes under the parent's opaque execution token so repeated sub-dispatches in one run do not duplicate them; the parent result rolls that provisional state back and commits only contexts retained by outer policy.
At reconciliation time the plugin scans workspace-sourced `user/message` events and derives the latest state for each visible scope. A short per-session pending map begins only after the immutable top-level `tools/result` proves an `additionalContexts` entry survived every post-execute listener, then covers the interval before the loop appends that context to the log. Each entry records the open `{ turn, step }`: an equal durable `user/message` at or after its sequence boundary confirms and removes it, while a matching `step/end` arriving first means the loop discarded its context buffer, so the plugin removes both the pending entry and its version-cache fast path. A nested Code Mode result stages its changes under the parent's opaque execution token so repeated sub-dispatches in one run do not duplicate them; the parent result rolls that provisional state back and commits only contexts retained by outer policy.
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata. If compaction removes an instruction event from the visible surface, that state no longer suppresses a later load, matching the fact that the model can no longer see it. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch.
The frozen baseline keeps an in-memory path/digest map for comparison. A later successful filesystem touch appends baseline edits or removals as dynamic messages; it never rewrites the prefix. During resumed prefix composition the plugin also reconciles visible dynamic scopes, so nested changes made while the agent was offline can append an update before the first resumed request.
The initial baseline's typed changes are comparison state only while its event remains in the visible session surface. A later successful filesystem touch re-adds an unchanged baseline scope after compaction, or appends baseline edits or removals as dynamic messages; it never rewrites the original event. The in-memory scope marker and provider-version cache only select and accelerate probes, so neither can suppress context the model no longer sees. During resumed baseline preparation the plugin also reconciles visible dynamic scopes, so nested changes made while the agent was offline can append an update before the first resumed request.
There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch or resumed prefix composition. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully.
There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch or resumed baseline preparation. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully.
### Byte Budget And Bounded Reads
@@ -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.
**Inject the baseline on every `agent/pre-step`.** Rejected because repeated history injection wastes tokens, complicates duplicate state, and prevents a structurally stable provider prefix. Prefix composition gives a frozen, logged, per-instance baseline while dynamic append-only messages handle changes.
**Inject the baseline on every `agent/step`.** Rejected because repeated history injection wastes tokens and complicates duplicate state. A per-mount session guard gives one visible baseline event while it remains on the surface; dynamic append-only messages handle changes and compaction re-arming.
**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.
@@ -78,7 +78,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc
## Consequences
Workspace guidance is isolated per session and shared by the demo front doors, Web Host, and every tool presentation mode. Initial instructions benefit from stable prefix caching, while nested and changed content remains durable and replayable. The generic session/agent context contract carries JSON metadata propagated through prompt-submit and post-tool `additionalContexts` arrays without flattening entries.
Workspace guidance is isolated per session and shared by the demo front doors, Web Host, and every tool presentation mode. Initial, nested, and changed instructions are durable and replayable. The generic session/agent context contract carries typed source data through injected messages and post-tool `additionalContexts` arrays without flattening entries.
Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, and delimiter escaping reduce risk but do not eliminate prompt injection. Following a candidate symlink to its target widens that surface to off-tree content, so the permission and sandbox layers that confine `ctx.fs` to trusted roots are the boundary that treats workspace files as data rather than authority (the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns the residual risk).

View File

@@ -10,13 +10,13 @@ Status: implemented
相邻产品形成了值得借鉴的约定但具体做法各不相同。Codex 原生使用 `AGENTS.md`Claude Code 使用 `CLAUDE.md`,并采用熟悉的 system-reminder 风格用户上下文opencode 同时支持这两个名称每个目录只选一个胜出者并延迟发现嵌套文件。harness 需要跨工具兼容,同时避免从同一作用域加载重复或互相矛盾的文件。
生命周期中有两类截然不同的内容。初始适用文件链足够稳定,可以放入请求前缀并受益于提供方前缀缓存。嵌套文件、编辑、候选项切换和移除发生在会话启动后,进入持久的仅追加历史,而不是被冻结的前缀
生命周期中有两类截然不同的内容。初始适用文件链在第一次请求前一次性注入。嵌套文件、编辑、候选项切换和移除发生在后,进入同一份持久的仅追加历史。
## 决策
该实现在 `packages/context/workspace-context`package名为 `@deepseek-ai/dsh-workspace-context`。它是请求上下文扩展,不是核心服务或文件系统后端。共享 demo 主干与 Host Runtime 根据显式的 `{ maxBytes } | false` 部署选择挂载它;`dsh web` 启用 65,536 字节预算Host Runtime 的 headless 消费方则禁用它。该插件使用 `agent/session-prefix``tools/post-execute` 和可选的 `ctx.fs` 功能。
该实现在 `packages/context/workspace-context`package名为 `@deepseek-ai/dsh-workspace-context`。它是请求上下文扩展,不是核心服务或文件系统后端。共享 demo 主干与 Host Runtime 根据显式的 `{ maxBytes } | false` 部署选择挂载它;`dsh web` 启用 65,536 字节预算Host Runtime 的 headless 消费方则禁用它。该插件使用 `agent/step``tools/post-execute` 和可选的 `ctx.fs` 功能。
插件不会静态注入 `fs`。因此,不带提供方的产品树仍能正常启动;在文件系统提供方出现之前,插件保持无操作。所有生产读取都通过该提供方完成。候选项探测会解析每个路径并对结果执行 stat因此会跟随最终路径组件的符号链接至其目标指向普通文件的链接会被加载缺失路径或非文件目标则确认为不存在。允许仓库拥有的链接跨越信任边界是对最初不跟随探测方式的刻意反转[跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明该决策及其残余风险。会话前缀信号与动态工具执行信号会贯穿解析、元数据探测和流式读取,因此取消不会等待无关的文件系统扫描。解析或 stat 异常归类为不可用:它只跳过该候选项,绝不被解释为已经加载的作用域被删除。
插件不会静态注入 `fs`。因此,不带提供方的产品树仍能正常启动;在文件系统提供方出现之前,插件保持无操作。所有生产读取都通过该提供方完成。候选项探测会解析每个路径并对结果执行 stat因此会跟随最终路径组件的符号链接至其目标指向普通文件的链接会被加载缺失路径或非文件目标则确认为不存在。允许仓库拥有的链接跨越信任边界是对最初不跟随探测方式的刻意反转[跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明该决策及其残余风险。步骤信号与动态工具执行信号会贯穿解析、元数据探测和流式读取,因此取消不会等待无关的文件系统扫描。解析或 stat 异常归类为不可用:它只跳过该候选项,绝不被解释为已经加载的作用域被删除。
### 文件名与优先级
@@ -26,13 +26,13 @@ Status: implemented
用户全局文件固定为 `$DSH_HOME/AGENTS.md`,不受任一候选列表影响,也没有本地覆盖层。`$DSH_HOME` 默认为 `~/.dsh`,与 `~/.codex``~/.claude` 在 harness 层的 home 角色一致,而不会引入插件专用 home。波浪号展开与默认值位于 `dsh-paths` 中,以便未来的 harness 功能共享同一约定。
### 基线前缀
### 基线注入
agent loop智能体循环实例的第一次请求会让插件通过 `agent/session-prefix` 提供一条 user 角色消息。它先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录各加载一个候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时cwd 本身就是根目录。
agent loop智能体循环实例的第一 `agent/step`,插件会在派生请求前注入一条带来源的 user 角色消息。它先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录各加载一个候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时cwd 本身就是根目录。
插件会在 `await next()` 返回前前置其贡献,因此会话前缀贡献按插件注册顺序出现。在产品主干中,工作区指令的注册先于 skill 目录,所以它排在前面。循环会深度冻结组合后的前缀,将其记录在 `EpochHeader.messagePrefix` 中,并在该实例内逐字复用。它是请求状态,不是 `Session.deriveMessages()` 历史
该注入成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。在产品主干中工作区指令的注册先于 skill 目录,所以其 `agent/step` 监听器先注入。循环会在派生第一次请求前 drain 这两条消息
恢复 agent 会创建新的循环实例,并使用当前文件重新组合基线;新的前缀由恢复请求 header 锚定。这样,恢复时可以使用当前基线内容,而无需修改先前实例已经使用过的前缀
恢复 agent 会创建新的循环实例,并在其第一次请求前注入由当前文件组合基线。这样,恢复时可以使用当前基线内容,而无需修改先前的历史事件。恢复与插件热重挂都会面对日志中可能已存在基线的情况;二者通过 `agent/session-start` 区分:启动或恢复会在第一步前发出该事件,而热重挂附着到一个已存活的会话、永远不会看到它。只有当基线的类型化事件仍在当前可见表层中时,热重挂才保留既有基线,同时仍会根据当前文件重建 scope 与提供方版本跟踪。如果压缩compaction已遮蔽该事件热重挂会注入当前基线。恢复则始终重新组合
基线是一条 user 角色的 `<system-reminder>`,包含 `Instructions from: <path>` 章节,以及明确的权威性与优先级说明。这种熟悉的模型可见框架避免引入 harness 专用的 XML 词汇。项目路径相对于根目录;使用默认 home 时,用户全局路径为 `~/.dsh/AGENTS.md`,使用已配置 home 时则为 `$DSH_HOME/AGENTS.md`。文件内容中的字面量 `</system-reminder>` 会被转义。包 README 负责规定当前准确的[提示词形态](../../../../packages/context/workspace-context/README.md#prompt-shape)。
@@ -42,21 +42,21 @@ agent loop智能体循环实例的第一次请求会让插件通过 `agent
内容编辑会追加 `Updated instructions from: <path>`,说明新内容取代先前内容,并包含当前的完整文件。如果优先级从一个候选项变为另一个,消息还会指出先前路径并说明它不再适用。如果没有候选项保留,插件会追加 `Instructions removed: <path>`,并说明先前加载的指令不再适用。
动态消息在 `content` 中携带完整的 system-reminder 框架;每`context/message` 都作为 user 角色消息逐字抵达模型,核心层不会再添加可选择退出的包装。`context/message.meta` 携带不透明 JSON 状态,该状态会持久化,但绝不会渲染给模型。
基线消息和动态消息`content` 中携带完整的 system-reminder 框架;每条带来源的 `user/message` 都逐字抵达模型,核心层不会再添加可选择退出的包装。带类型的 `workspace-instructions` 来源携带持久化状态,该状态绝不会渲染给模型。
shell 命令不会触发发现。本地 bash 调用会启动全新的 shell而从任意命令字符串推断已到达路径需要实现提示词插件并不拥有的 shell 语义。
### 重复抑制与变更检测
每个动态工作区上下文事件都会存储带版本的元数据,其形态为 `{ action, scope, path, digest? }``digest` 是对已加载内容计算的 SHA-1。模型可见提示词中没有 HTML 注释、隐藏标记,也没有会被解析回状态的标题。
每个工作区上下文事件都会存储带版本的元数据,其形态为 `{ action, scope, path, digest? }``digest` 是对已加载内容计算的 SHA-1。基线还会额外携带 `baseline: true`模型可见提示词中没有 HTML 注释、隐藏标记,也没有会被解析回状态的标题。
协调时,插件扫描自身拥有的 `context/message` 事件,并派生每个可见作用域的最新状态。一个简短的逐会话待处理映射只会在不可变的顶层 `tools/result` 证明某个 `additionalContexts` 条目经过所有 post-execute 监听器后仍然保留时开始记录;随后,它覆盖循环将该上下文追加到日志之前的间隔。每个条目记录开启状态的 `{ turn, step }`:如果相同的持久 `context/message` 出现在其序列边界或之后,该条目得到确认并被移除;如果匹配的 `step/end` 先到达,则说明循环丢弃了上下文缓冲区,插件会同时移除待处理条目及其版本缓存快速路径。嵌套的 Code Mode 结果会把变更暂存在父级的不透明执行 token 下,确保一次运行中的重复子分发不会产生重复项;父级结果会回滚这份临时状态,并且只提交外层策略保留的上下文。
协调时,插件扫描带工作区来源的 `user/message` 事件,并派生每个可见作用域的最新状态。一个简短的逐会话待处理映射只会在不可变的顶层 `tools/result` 证明某个 `additionalContexts` 条目经过所有 post-execute 监听器后仍然保留时开始记录;随后,它覆盖循环将该上下文追加到日志之前的间隔。每个条目记录开启状态的 `{ turn, step }`:如果相同的持久 `user/message` 出现在其序列边界或之后,该条目得到确认并被移除;如果匹配的 `step/end` 先到达,则说明循环丢弃了上下文缓冲区,插件会同时移除待处理条目及其版本缓存快速路径。嵌套的 Code Mode 结果会把变更暂存在父级的不透明执行 token 下,确保一次运行中的重复子分发不会产生重复项;父级结果会回滚这份临时状态,并且只提交外层策略保留的上下文。
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作。如果压缩compaction从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作。如果压缩从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
被冻结的基线会保留一个内存中的 pathdigest map 以供比较。后续成功的文件系统触碰会把基线编辑或移除操作追加为动态消息绝不重写前缀。恢复时重新组合前缀的过程中,插件还会协调可见的动态作用域,因此 agent 离线期间发生的嵌套变更可以在第一次恢复请求前追加更新。
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。后续成功的文件系统触碰会在压缩后重新添加未变化的基线 scope把基线编辑或移除操作追加为动态消息;它绝不重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。恢复时准备基线的过程中,插件还会协调可见的动态作用域,因此 agent 离线期间发生的嵌套变更可以在第一次恢复请求前追加更新。
系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰或恢复时的前缀组合。提供方失败不会产生移除;只有该作用域中的全部已配置候选项都成功完成探测后,系统才接受「不存在」这一结论。
系统刻意不使用文件监视器。检测发生在下一次成功的结构化文件系统触碰或恢复时的基线准备。提供方失败不会产生移除;只有该作用域中的全部已配置候选项都成功完成探测后,系统才接受「不存在」这一结论。
### 字节预算与有界读取
@@ -68,7 +68,7 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell
**使用全局 `ctx.systemPrompt.section()`。** 不予采纳,因为同一个 Cordis 上下文可以承载 cwd 不同的多个会话,而仓库所有的文本属于低权威用户上下文,不是最高权威的提供方系统内容。
**在每次 `agent/pre-step` 时注入基线。** 不予采纳,因为重复注入历史会浪费 token使重复状态复杂化,并妨碍提供方前缀保持结构稳定。前缀组合提供冻结、已记录且逐实例的基线,动态仅追加消息负责变更
**在每次 `agent/step` 时注入基线。** 不予采纳,因为重复注入历史会浪费 token,并使重复状态复杂化。逐挂载会话防护会在基线事件仍留在表面期间提供一条可见基线事件;动态仅追加消息负责处理变更和压缩后的重新启用
**在一个目录中同时加载 `AGENTS.md` 和 `CLAUDE.md`。** 不予采纳,因为正在迁移的仓库通常会在两个文件中重复指引。按顺序排列的候选项让优先级显式且可配置。
@@ -78,7 +78,7 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell
## 影响
工作区指引按会话隔离,并由 demo 前端、Web Host 与每一种工具展示模式共享。初始指令受益于稳定的前缀缓存,嵌套与变更内容则保持持久且可回放。通用的 sessionagent 上下文契约通过 prompt-submit 与工具执行后的 `additionalContexts` 数组携带 JSON 元数据,而不会把条目展平。
工作区指引按会话隔离,并由 demo 前端、Web Host 与每一种工具展示模式共享。初始嵌套与变更指令都保持持久且可回放。通用的 sessionagent 上下文契约通过注入消息与工具执行后的 `additionalContexts` 数组携带带类型的来源数据,而不会把条目展平。
仓库文本仍是不受信任的输入。低权威 user 角色框架、显式优先级说明和分隔符转义可以降低风险,但无法消除提示词注入。跟随候选符号链接到目标,会把该接口扩大至树外内容;因此,把 `ctx.fs` 限制在可信根目录内的权限与沙箱层才是真正的边界,它们让系统把工作区文件当作数据而不是权威([跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明残余风险)。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-06-30-hook-bridges.md: c207b6901155925215548676364e903f5de2f29b
2026-06-30-hook-bridges.zh.md: d396279d7ed1991536da2cea39e2aec5e50960c2
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-hook-bridges.md
2026-06-30-hook-bridges.md: 99c6b1941a10e198ec3028f5fe505dabfff9abbe
2026-06-30-hook-bridges.zh.md: 11ed3a5d177661271b30f1a58d034caa577b5348

View File

@@ -6,7 +6,7 @@ English | [中文](2026-06-30-hook-bridges.zh.md)
## Problem
The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`, `subagent/start`, `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)).
The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)).
The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome onto a seam Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols.
@@ -27,7 +27,7 @@ Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto the se
| `agent/prompt-submit` | `deny``block`; context-only→delegate+fold | `block``block`; context-only→delegate+fold |
| `tools/pre-execute` | `deny``deny`; `ask``ask` | `block``deny` (no allow/ask) |
| `tools/post-execute` | `deny``block`+feedback; context-only→delegate+fold | same |
| `agent/turn-continuation` | blocking Stop → `continue` (reason = next-step steering) | same |
| `agent/turn-stopping` | blocking Stop → next-step steering | same |
| `subagent/start` (emit) | additionalContext → inject into a live in-process child; a remote child has no local injection target | unsupported by this bridge |
| `subagent/end` (emit) | observe-only | unsupported by this bridge |
@@ -35,7 +35,9 @@ The CC bridge's `ask` result is a real permission path, not a terminal bridge de
### Context source is always the plugin (the mislabel guard)
`agent.inject()` defaults a missing `MessageSource` to `{ kind: 'user' }`, so every bridge `inject()` and `HookContext` passes `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`. Unit coverage pins the resulting `context/message.source` as the plugin rather than the user.
Every bridge `inject()` and additional-context input explicitly passes `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`. Unit coverage pins the resulting `user/message.source` as the plugin rather than the user.
`UserPromptSubmit` runs during admission, before any turn opens. It therefore writes no turn-scoped `hook/invoked` / `hook/result` pair: a block leaves no transcript, while allowed additional context is durably represented by its sourced `user/message`. The Codex payload still receives the candidate next `turn_id`; rejection does not consume that number.
### Adding context is not a veto — delegate, then prepend
@@ -57,13 +59,13 @@ Hooks run in the agent's session workspace, so relative paths target the user's
- **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + tool presentation, so an honest rewrite is a design unit, not a field.
- **Stop loop-guard** (`TODO(stop-loop-guard)`). Claude Code supplies `stop_hook_active` and overrides a hook after eight consecutive blocks; Codex supplies `stop_hook_active` but documents no equivalent cap. Both bridges always report `false`, so a Stop hook that unconditionally blocks force-continues every step — a hook author must self-limit until state tracking lands.
- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; the halt request is recorded in the `hook/result` log, and the hook keeps its per-point effect (decision/context) meanwhile.
- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; mid-turn requests record the halt in `hook/result`, and the hook keeps its per-point effect (decision/context) meanwhile.
- **Config discovery.** The path is explicit in `cordis.yml` and process-level (see above); the full multi-layer CC/Codex precedence walk, per-session project-local discovery, and the trust/hash model are not reimplemented (`TODO(per-session-hook-config)`).
- **Session-start / subagent-start context is best-effort (`TODO(session-start-gating)`).** Both hooks run detached from startup, so their context is injected when ready but may miss the first request or a short-lived child. Guaranteeing first-request delivery requires an awaited startup seam.
## Alternatives considered
**Concurrent per-point hook execution.** The reference engines run a point's matched hooks concurrently and fold the results. These bridges run them **serially** (`await` per hook inside the match loop) and fold with the same most-restrictive merge. Serial is deliberate: it keeps each hook's `hook/invoked`/`hook/result` pair adjacent and in a deterministic order in the session log, and the fold is order-independent for the decision (`deny > ask > allow`) so the outcome matches. The cost is latency (hook *N* waits for hook *N1*) and that per-hook timeouts are not overlapped — acceptable for the hook counts real configs use; revisit if a config ever fans out enough for the wall-clock to matter.
**Concurrent per-point hook execution.** The reference engines run a point's matched hooks concurrently and fold the results. These bridges run them **serially** (`await` per hook inside the match loop) and fold with the same most-restrictive merge. Serial is deliberate: for turn-scoped points it keeps each hook's `hook/invoked`/`hook/result` pair adjacent and in deterministic order, and the fold is order-independent for the decision (`deny > ask > allow`) so the outcome matches. The cost is latency (hook *N* waits for hook *N1*) and that per-hook timeouts are not overlapped — acceptable for the hook counts real configs use; revisit if a config ever fans out enough for the wall-clock to matter.
## Consequences

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
harness 的扩展面是其类型化的拦截 seam见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start``agent/prompt-submit``tools/pre-execute``tools/post-execute``agent/turn-continuation``subagent/start``subagent/end`。但用户带着**既有的** Claude CodeCC和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上构建于共享的协议格式wire format库之上见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。
harness 的扩展面是其类型化的拦截 seam见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start``agent/prompt-submit``tools/pre-execute``tools/post-execute``agent/turn-stopping``subagent/start``subagent/end`。但用户带着**既有的** Claude CodeCC和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上构建于共享的协议格式wire format库之上见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。
贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为 seam Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。
@@ -27,7 +27,7 @@ harness 的扩展面是其类型化的拦截 seam见[拦截 seam Agent Note](
| `agent/prompt-submit` | `deny``block`仅上下文→delegate+fold | `block``block`仅上下文→delegate+fold |
| `tools/pre-execute` | `deny``deny``ask``ask` | `block``deny`(无 allow/ask |
| `tools/post-execute` | `deny``block`+feedback仅上下文→delegate+fold | 同上 |
| `agent/turn-continuation` | 阻塞的 Stop → `continue`reason = 下一步 steering中途引导 | 同上 |
| `agent/turn-stopping` | 阻塞的 Stop → 下一步 steering中途引导 | 同上 |
| `subagent/start`emit | additionalContext → 注入到存活的进程内 subagent远程 subagent 无本地注入目标 | 本桥接不支持 |
| `subagent/end`emit | 仅观察 | 本桥接不支持 |
@@ -35,7 +35,9 @@ CC 桥接的 `ask` 结果是一条真正的权限路径,而非终态桥接决
### 上下文来源始终是插件(误标签防护)
`agent.inject()` 在缺少 `MessageSource` 时默认为 `{ kind: 'user' }`,因此每个桥接的 `inject()``HookContext`传入 `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`。单元测试覆盖率固定验证结果中的 `context/message.source` 为插件而非用户。
每个桥接的 `inject()` 和 additional-context 输入都显式传入 `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`。单元测试覆盖率固定验证结果中的 `user/message.source` 为插件而非用户。
`UserPromptSubmit` 在准入阶段运行,早于任何轮次开启。因此它不写入任何轮次范围的 `hook/invoked` / `hook/result` 对:阻止不会留下 transcript文本记录而被允许的额外上下文由其带来源的 `user/message` 持久呈现。Codex payload 仍会收到候选的下一个 `turn_id`;拒绝不会消耗该编号。
### 添加上下文不是否决——先 delegate再 prepend
@@ -57,13 +59,13 @@ Claude Code 始终导出 `CLAUDE_PROJECT_DIR`,常见的未修改钩子引用 `
- **工具输入重写。** CC/Codex 的 `updatedInput` 被记录日志并发出警告,但不予执行——输入重写是一个推迟的一致性设计问题(见 [pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)),因为 pre-execution 参数被 `tool/call` 审计、`assistant/message` 历史和工具展示共同读取,诚实的重写是一个设计单元,而非一个字段。
- **Stop 循环防护**`TODO(stop-loop-guard)`。Claude Code 提供 `stop_hook_active` 并在连续八次阻塞后覆盖钩子Codex 提供 `stop_hook_active` 但未记录等效上限。两个桥接始终报告 `false`,因此一个无条件阻塞的 Stop 钩子会在每一步强制继续——在状态追踪落地之前,钩子作者必须自行限制。
- **钩子 `continue:false`(硬停止)。** 钩子可以请求终止整个运行CC/Codex `continue:false`);共享合并将其折叠为 `MergedHookOutcome.stop`/`stopReason`,但没有桥接对其采取行动(`TODO(hook-continue-false)`)——拦截 seam 尚无「硬停止 agent」原语Decision 阻塞/引导的是单个点,而非整个运行)。与循环防护工作一同推迟;停止请求记录在 `hook/result` 日志中,钩子在此期间保留其逐点效果(决策/上下文)。
- **钩子 `continue:false`(硬停止)。** 钩子可以请求终止整个运行CC/Codex `continue:false`);共享合并将其折叠为 `MergedHookOutcome.stop`/`stopReason`,但没有桥接对其采取行动(`TODO(hook-continue-false)`)——拦截 seam 尚无「硬停止 agent」原语Decision 阻塞/引导的是单个点,而非整个运行)。与循环防护工作一同推迟;轮中请求会将停止请求记录在 `hook/result` 中,钩子在此期间保留其逐点效果(决策/上下文)。
- **配置发现。** 路径在 `cordis.yml` 中显式指定且为进程级(见上文);完整的多层 CC/Codex 优先级遍历、按会话的项目本地发现以及信任/hash 模型未被重新实现(`TODO(per-session-hook-config)`)。
- **Session-start / subagent-start 上下文为尽力而为(`TODO(session-start-gating)`)。** 两个钩子以 detached 方式运行于启动过程之外,因此其上下文在就绪时注入,但可能错过首个请求或短命的 subagent。要保证首请求送达需要一个 awaited 的启动 seam。
## 曾考虑的替代方案
**每点钩子并发执行。** 参考引擎对一个点匹配到的钩子并发运行并折叠结果。本桥接**串行**运行(匹配循环内每个钩子 `await`),并以相同的最严格合并策略折叠。串行是刻意的:它使每个钩子的 `hook/invoked`/`hook/result`在会话日志中相邻且顺序确定,而折叠对决策是顺序无关的(`deny > ask > allow`),因此结果一致。代价是延迟(钩子 *N* 等待钩子 *N1*)以及每钩子超时不重叠——对真实配置中的钩子数量可以接受;如果某配置的扇出大到影响总耗时,再重新评估。
**每点钩子并发执行。** 参考引擎对一个点匹配到的钩子并发运行并折叠结果。本桥接**串行**运行(匹配循环内每个钩子 `await`),并以相同的最严格合并策略折叠。串行是刻意的:对轮次范围的拦截点,它使每个钩子的 `hook/invoked`/`hook/result` 对相邻且顺序确定,而折叠对决策是顺序无关的(`deny > ask > allow`),因此结果一致。代价是延迟(钩子 *N* 等待钩子 *N1*)以及每钩子超时不重叠——对真实配置中的钩子数量可以接受;如果某配置的扇出大到影响总耗时,再重新评估。
## 后果

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-06-30-interception-seams.md: 81799f4c6e3e7a4c6b9605cd97f5728b99d11995
2026-06-30-interception-seams.zh.md: 65ae16842c632641e7ac65908162f4784dc6e1e0
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-interception-seams.md
2026-06-30-interception-seams.md: 4658983e1f098ecd199eecec4408e7c2f134cbf7
2026-06-30-interception-seams.zh.md: 13b7c56829412773111fcf6d75cc717c51d49c7b

View File

@@ -16,9 +16,9 @@ The canonical surface separates transformable policy, around-dispatch control, a
**Agent events** (`dsh-agent`):
- `agent/session-start(agent, source)` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`.
- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` — waterfall, fired for the turn's single claimed queued message before the `user/message` append. The explicit turn signal is placed before the final `next`; `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`, while `block` appends a durable `prompt/blocked` and rejects that zero-step turn.
- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` — waterfall, fired for one claimed queued message before the loop opens a turn or appends `user/message`. The explicit admission signal is placed before the final `next`; `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`, while `block` discards the candidate without creating session history.
**`agent/turn-continuation`** receives and returns a `ContinuationDecision`. A `{action:'continue', reason?}` may carry model-facing content and source recorded as next-step steering in the same turn — the typed twin of the `/goal` step-end-steer pattern. It is not a `context/message`, so its type does not offer durable context metadata.
**`agent/turn-stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn.
### The tool pipeline gives each phase one kind of authority
@@ -33,15 +33,13 @@ Every call follows `tools/pre-execute` → guards → `tools/execute` → dispat
Core dispatch and the tool body sit inside normalization boundaries, so tool, listener, invalid canonical value, renderer/projector, non-JSON presentation, and identity-shape failures resolve as JSON-safe `isError` results rather than escaping the turn. A post-execute listener can therefore inspect a thrown tool; definition-owned final content invariants also cover outer pipeline and candidate-materialization failures; and a final observer sees the execution-local canonical value beside exactly the presentation fields the session log can persist. The [canonical tool-output contract](../architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/projection and durability rules.
**`TurnEndReason.rejected`** (`dsh-session`): a zero-step turn whose claimed prompt was blocked by `prompt-submit`.
### Three load-bearing loop decisions
1. **Open the turn before prompt policy.** A blocked prompt becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. The veto records `prompt/blocked` with the original prompt and reason, while every allowed `additionalContexts` entry is injected into the open turn. Each claimed ordinary-send item is the sole message in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md); a pre-start drop creates no turn.
1. **Run prompt policy before opening the turn.** A blocked prompt creates no turn or durable event. On allow, the loop stages the rewritten prompt followed by every returned `additionalContexts` entry, opens the turn, and drains that outbox before the first step. Each claimed ordinary-send item is the sole direct prompt in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md).
2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate `context/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision.
2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate sourced `user/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision.
3. **A forced `continue` `reason` is enqueued through the steering channel**, so the next step's top-of-loop drain records it as steering for the continued turn — next-*step* steering within the SAME turn, not a next-*turn* prompt (matching the existing `hasSteering` force-continue override).
3. **A stopping listener requests continuation through the steering channel**, so the next step's top-of-loop drain records it as steering for the continued turn — next-*step* steering within the SAME turn, not a next-*turn* prompt.
### Pre-tool input rewrite is a separate consistency decision
@@ -49,7 +47,7 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li
### Boundaries
The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`, while terminal monotonic stopping is owned separately by `agent/turn-stop`.
The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`; terminal monotonic stopping is expressed by tool-result data, while `agent/turn-stopping` is the last chance to steer another step.
## Alternatives considered
@@ -58,4 +56,4 @@ The seam package does **not** declare `hook/*` session events (the durable hook-
## Consequences
The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, prompt-submit, post-tool context buffering, and continuation; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge maps `rejected` turns to its `cancelled` codec value, while hook-driven snapshots verify the observable bridge behavior end to end.
The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-turn prompt admission, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an admission rejection as `cancelled` after the agent becomes idle with no owned turn, while hook-driven snapshots verify the observable bridge behavior end to end.

View File

@@ -16,9 +16,9 @@ harness 需要一套钩子子系统:用户像 Claude CodeCC和 Codex 那
**Agent 事件**`dsh-agent`
- `agent/session-start(agent, source)` ——emit在第 1 轮次之前触发一次,携带 `SessionStartSource``startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。
- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` ——waterfall在轮次唯一取得所有权的排队消息追加 `user/message` 之前触发。显式轮次 signal 位于最后的 `next` 之前;`allow` 可以重写提示词 `content` 或附加来源各自独立的 `additionalContexts[]`,而 `block`追加一条持久的 `prompt/blocked`,并拒绝这个零步骤轮次
- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` ——waterfall针对一条取得所有权的排队消息触发,早于循环开启轮次或追加 `user/message`。显式准入 signal 位于最后的 `next` 之前;`allow` 可以重写提示词 `content` 或附加来源各自独立的 `additionalContexts[]`,而 `block`丢弃该候选消息,不产生会话历史
**`agent/turn-continuation`** 接收并返回一个 `ContinuationDecision``{action:'continue', reason?}` 可携带面向模型的内容和来源,记录为同一轮次内的下一步 steering中途引导)——与 `/goal` step-end-steer 模式互为类型化孪生。它不是 `context/message`,因此其类型不提供持久上下文元数据
**`agent/turn-stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的面向模型的内容(steering中途引导);循环随后重新读取 outbox继续执行或关闭轮次
### 工具流水线为每个阶段赋予一种权限
@@ -33,15 +33,13 @@ harness 需要一套钩子子系统:用户像 Claude CodeCC和 Codex 那
核心调度与工具体位于规范化边界内部,因此工具、监听器、无效规范值、渲染器/投影器、非 JSON 呈现和身份形状错误均解析为 JSON 安全的 `isError` 结果而非逃逸出轮次。post-execute 监听器因此可以检查一个抛出异常的工具;由定义拥有的最终内容不变式也会覆盖外层流水线与候选结果实体化失败;最终观测者会同时看到执行期间的规范值,以及会话日志能够持久化的确切呈现字段。[规范工具输出契约](../architecture/2026-07-20-canonical-tool-output-contract.md)定义值/投影与持久性规则。
**`TurnEndReason.rejected`**`dsh-session`):取得所有权的提示词被 `prompt-submit` 阻止的零步骤轮次。
### 三个承重的循环决策
1. **在提示词策略之前开启轮次** 被阻止的提示词成为零步骤的 `rejected` 轮次,保持封闭性并为 ACPAgent Client Protocol提供持久的终结事件。否决记录 `prompt/blocked`(含原始提示词和原因),而每个允许`additionalContexts` 条目都注入到已开启轮次。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个取得所有权的 ordinary-send 条目都是其轮次中唯一消息;启动前丢弃不会创建轮次
1. **在开启轮次之前运行提示词策略。** 被阻止的提示词不会创建轮次,也不产生持久事件。允许时,循环先暂存重写后的提示词,再暂存每个返回`additionalContexts` 条目,然后开启轮次并在第一个步骤之前排空该 outbox。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个取得所有权的 ordinary-send 条目都是其轮次中唯一的直接提示词
2. **工具执行后的 `additionalContexts` 与异步注入进入活跃批次 FIFO并在该批次结算时追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但每项上下文都是独立的 `context/message`,而单个步骤或组合工具可以产生许多上下文。立即追加上下文会产生 `result(c1) → context → result(c2)` 的交错,或把嵌套上下文放在外层结果之前,破坏工具调用/结果邻接性。因此 `ToolRunContext.deferContext()` 会在失败路径上也收集嵌套调度上下文,`execute()``ToolExecutionResult` 上暴露有序数组,循环再把它接纳到与执行期间 `agent.inject()` 调用相同的 FIFO 中。FIFO 在批次结算时,于每个已记录结果之后追加,其中也包括被中断轮次关闭之前。被接受的外层调用将 deferred contexts 保留在 decision contexts 之前;被外层阻止时则丢弃 deferred contexts只暴露阻止 decision 显式提供的上下文。
2. **工具执行后的 `additionalContexts` 与异步注入进入活跃批次 FIFO并在该批次结算时追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但每项上下文都是一条独立的带来源 `user/message`,而单个步骤或组合工具可以产生许多上下文。立即追加上下文会产生 `result(c1) → context → result(c2)` 的交错,或把嵌套上下文放在外层结果之前,破坏工具调用/结果邻接性。因此 `ToolRunContext.deferContext()` 会在失败路径上也收集嵌套调度上下文,`execute()``ToolExecutionResult` 上暴露有序数组,循环再把它接纳到与执行期间 `agent.inject()` 调用相同的 FIFO 中。FIFO 在批次结算时,于每个已记录结果之后追加,其中也包括被中断轮次关闭之前。被接受的外层调用将 deferred contexts 保留在 decision contexts 之前;被外层阻止时则丢弃 deferred contexts只暴露阻止 decision 显式提供的上下文。
3. **强制 `continue` 的 `reason` 通过 steering 通道入队**,使得下一步骤在循环顶部排空时将其记录为当前轮次的 steering——同一轮次内的下一*步骤* steering而非下一*轮次*的提示词(与现有的 `hasSteering` 强制继续覆盖一致)
3. **stopping 监听器通过 steering 通道请求继续执行**,使得下一步骤在循环顶部排空时将其记录为当前轮次的 steering——同一轮次内的下一*步骤* steering而非下一*轮次*的提示词。
### 工具执行前输入重写是一个独立的一致性决策
@@ -49,7 +47,7 @@ harness 需要一套钩子子系统:用户像 Claude CodeCC和 Codex 那
### 边界
seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam不涉及 `hook/*` 协议。压缩compaction`PreCompact`/`PostCompact`、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision,而终结性的单调停止由 `agent/turn-stop` 独立负责
seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam不涉及 `hook/*` 协议。压缩compaction`PreCompact`/`PostCompact`、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision终结性的单调停止由工具结果数据表达,而 `agent/turn-stopping` 是引导再执行一步的最后机会
## 曾考虑的替代方案
@@ -58,4 +56,4 @@ seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);
## 后果
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision执行包装层做包装终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、prompt-submit、工具执行后上下文缓冲和 continuation`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP 桥接`rejected` 轮次映射为其 `cancelled` 编解码值,而钩子驱动的快照端到端验证可观测的桥接行为。
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision执行包装层做包装终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、轮次前的提示词准入、工具执行后上下文缓冲和 stopping`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP 桥接在 agent 空闲且不再拥有轮次后,将准入拒绝结算为 `cancelled`,而钩子驱动的快照端到端验证可观测的桥接行为。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-07-06-sandbox.md: c6883873192f15ba2982436e156d8795396c0148
2026-07-06-sandbox.zh.md: d84df9b06b15dd296801073d381603f34cfd2878
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
2026-07-06-sandbox.md: b9c410e4498a565a7414085fddfb74856592da66
2026-07-06-sandbox.zh.md: 876870a12f1205b4f2cc2c8c13b5e7ec815a1569

View File

@@ -165,7 +165,7 @@ What shipped pins — the tiers in Testing hold each:
- A resumed session's overrides apply with no catch-up state; a default changed while the process was down is narrated before the session's first new request, attributed to the operator.
- Two concurrent sessions never see each other's state or notices.
- Two concurrent project sessions in one Cordis context resolve independent workspace roots; bash and fs writes succeed inside the calling session's cwd and fail against its neighbor's cwd.
- `agent-loop` is untouched — everything rides `systemPrompt.section`, `SessionEventMap` merging, `agent.inject()`, `agent/pre-step`, `agent/prompt-submit`, and capability-owned policy resolution.
- `agent-loop` is untouched — everything rides `systemPrompt.section`, `SessionEventMap` merging, `agent.inject()`, `agent/step`, `agent/prompt-submit`, and capability-owned policy resolution.
Costs and accepted limits:

View File

@@ -128,7 +128,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- **第二个消费方**——`subagent-acp` 可选地约束子 agent按调用策略默认无约束——子 agent 必须写入自己的持久化)。
- **更多环境**——环境一致的能力组示例(如 bash+fs 对一个容器)。
- **Windows 链**——`PLATFORM_CHAINS.win32` 保留为空(失败关闭);填充它意味着来自 AppContainer/restricted-token 家族的约束 runner从其自己的仓库按 `node-addon-landlock-run` 模板交付,加上其 profile 方言和拒绝/runner 失败签名。改为包装第三方 landstrip runner 的方案[经考虑后已驳回](../../rejected/feature/2026-07-26-evaluate-landstrip-for-windows-sandbox-rung.md)——对安全不变式而言,它还远未经过实战检验
- **Windows 链**——`PLATFORM_CHAINS.win32` 保留为空(失败关闭);填充它意味着来自 AppContainer/restricted-token 家族的约束 runner从其自己的仓库按 `node-addon-landlock-run` 模板交付,加上其 profile 方言和拒绝/runner 失败签名。改为包装第三方 landstrip runner 的方案[经考虑后已驳回](../../rejected/feature/2026-07-26-evaluate-landstrip-for-windows-sandbox-rung.md)——它所经受的实战检验还不足以承载安全不变式
## 曾考虑的替代方案
@@ -165,7 +165,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
- 恢复的会话的覆盖直接生效,无需追赶状态;进程停止期间变更的默认值在会话的首个新请求前被叙述,归因于运维人员。
- 两个并发会话永远看不到彼此的状态或通知。
- 同一个 Cordis 上下文中的两个并发项目会话解析各自独立的工作区根目录bash 和 fs 写入在调用方会话的 cwd 内成功,对其相邻会话的 cwd 则失败。
- `agent-loop` 未被触及——一切搭载 `systemPrompt.section``SessionEventMap` 合并、`agent.inject()``agent/pre-step``agent/prompt-submit` 和由能力拥有的策略解析。
- `agent-loop` 未被触及——一切搭载 `systemPrompt.section``SessionEventMap` 合并、`agent.inject()``agent/step``agent/prompt-submit` 和由能力拥有的策略解析。
代价与已接受的限制:

View File

@@ -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
2026-07-07-session-prefix.md: 75113952fc5f3df8da1580d42ed2a385b6135fe8
2026-07-07-session-prefix.zh.md: e38bf09298296203b275d6d66a62ef17be7c045d
2026-07-07-session-prefix.md: df007012165da2da9b7de4bd9ae83534e45a439d
2026-07-07-session-prefix.zh.md: d0c574706352bfaad9d6b462a7866e509976f84e

View File

@@ -2,6 +2,8 @@
Status: implemented
The request-only prefix seam described below was later removed by the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md). Current producers inject durable sourced `user/message` context at `agent/step`; this record preserves the earlier design and its trade-offs.
English | [中文](2026-07-07-session-prefix.zh.md)
## Problem
@@ -12,7 +14,7 @@ The obvious third option — let a plugin edit the request's `messages` on the w
## Decision
`agent/session-prefix` is a waterfall on the agent event map ([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)): listeners receive a frozen empty seed and return an extension (the canonical contribution is a prepend, `[mine, ...await next()]`, which yields registration order on the wire). The loop ([`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts)) fires it once per loop instance, lazily before the instance's first `agent/pre-step`; the composed list is deep-cloned, deep-frozen, cached on the instance, and placed in front of the ENTIRE derived history — directly after the provider's system slot — on every request the instance sends ([wire order](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header)).
`agent/session-prefix` is a waterfall on the agent event map ([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)): listeners receive a frozen empty seed and return an extension (the canonical contribution is a prepend, `[mine, ...await next()]`, which yields registration order on the wire). The loop ([agent-loop source](../../../../packages/core/agent-loop/src/)) fires it once per loop instance, lazily before the instance's first `agent/pre-step`; the composed list is deep-cloned, deep-frozen, cached on the instance, and placed in front of the ENTIRE derived history — directly after the provider's system slot — on every request the instance sends ([wire order](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header)).
Three properties carry the design:

View File

@@ -2,6 +2,8 @@
Status: implemented
下文所述的仅请求前缀 seam 后来已被[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)移除。当前的生产方在 `agent/step` 时注入持久的带来源 `user/message` 上下文;本记录保留了早先的设计及其权衡。
[English](2026-07-07-session-prefix.md) | 中文
## 问题
@@ -12,7 +14,7 @@ Status: implemented
## 决策
`agent/session-prefix` 是 agent 事件映射上的一个 waterfall瀑布式事件[`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)):监听器接收一个冻结的空种子并返回扩展(规范的贡献方式是前置插入 `[mine, ...await next()]`在协议格式上产生注册顺序。agent loop智能体循环[`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts))在每个循环实例中触发一次,惰性地在实例首次 `agent/pre-step` 之前执行;组合后的列表被深拷贝、深冻结、缓存在实例上,并在该实例发出的每个请求中置于整个派生历史之前——紧接在提供方的 system 槽位之后([协议格式顺序](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header))。
`agent/session-prefix` 是 agent 事件映射上的一个 waterfall瀑布式事件[`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)):监听器接收一个冻结的空种子并返回扩展(规范的贡献方式是前置插入 `[mine, ...await next()]`在协议格式上产生注册顺序。agent loop智能体循环[agent-loop 源码](../../../../packages/core/agent-loop/src/))在每个循环实例中触发一次,惰性地在实例首次 `agent/pre-step` 之前执行;组合后的列表被深拷贝、深冻结、缓存在实例上,并在该实例发出的每个请求中置于整个派生历史之前——紧接在提供方的 system 槽位之后([协议格式顺序](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header))。
三个属性承载了这一设计:

View File

@@ -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
2026-07-19-model-facing-goal-tools.md: 2c53a7658e213dee4fecc93709244f97b821aca0
2026-07-19-model-facing-goal-tools.zh.md: aa7b5ea14b7afff88819f0efa35c5f2d5e2e933e
2026-07-19-model-facing-goal-tools.md: bc4305af80bb13ceeff1888d489dcd8a00132f94
2026-07-19-model-facing-goal-tools.zh.md: b07f62aa526902c4b2e9c081777a76ca53783d31

View File

@@ -22,13 +22,13 @@ The prompt tells the model that it may infer goal intent from a direct human req
All three tools use exclusive execution so a model-ordered batch observes prior mutations and their new revisions. Results are compact JSON. UI presentation is a pure function of arguments and uses generic read or mutation cards; mutation cards select meaningful action values before the goal id, so accepted fillers cannot blank their input. Activation is reported only as live observation and is never written into replay state.
An autonomous goal round that successfully reports completion or blocking contributes the existing terminal `agent/turn-stop` decision for that physical turn, preventing an unnecessary follow-up request. Direct-human mutations do not contribute a terminal stop: the assistant can acknowledge the change, and concurrent human steering remains available to ordinary continuation folding.
An autonomous goal round that successfully reports completion or blocking marks its tool result as concluding the physical turn, preventing an unnecessary follow-up request. Direct-human mutations do not conclude the turn: the assistant can acknowledge the change, and concurrent human steering remains available to ordinary stopping checks.
### Execution authority
Every call requires an `exec.agent` that is the exact running object in `AgentRegistry`, is the current inherited driver initiator, and has an open turn. These are execution-time checks and cannot be bypassed by prompt injection or hand-authored tool arguments.
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: `Agent.send()` and `steer()` default an omitted source to `{ kind: 'user' }`, so non-human producers must label their own content. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: every `Agent.send()` or `steer()` input requires an explicit source, so the host labels direct human content `{ kind: 'user' }` and non-human producers label their own provenance. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
Complete and blocked accept either direct-human authority or the exact current goal round. Goal-round authority requires a goal-sourced `user/message` whose goal id, revision, and round all equal the folded current goal. It grants only the two terminal reports. Direct human authority may stop a goal immediately.

View File

@@ -22,13 +22,13 @@ Status: implemented
三个工具都采用独占执行,使模型排序的批次可以观察此前变更及其新修订号。结果为紧凑 JSON。UI 展示是参数的纯函数,使用通用读取或变更卡片;变更卡片选择输入时,先取有实际意义的操作值,再取目标 id因此允许的占位值不会使卡片输入留空。激活态仅作为实时观察返回绝不会写入回放状态。
自主目标回合成功报告完成或阻塞后,插件会为该物理轮次贡献现有的终止型 `agent/turn-stop` 决策,避免再发起一次不必要的模型请求。直接人类发起的变更不会贡献终止决策:智能体可以确认该变更,并且并发的人类 steering转向)仍可参与普通的继续执行折叠
自主目标回合成功报告完成或阻塞后,其工具结果会被标记为结束该物理轮次,避免再发起一次不必要的模型请求。直接人类发起的变更不会结束轮次agent 可以确认该变更,并且并发的人类 steering中途引导)仍可参与普通的停止检查
### 执行权限
每次调用都要求存在 `exec.agent`,且它必须是 `AgentRegistry` 中完全相同的运行中对象、当前继承的驱动发起者,并处于开放轮次内。这些检查在执行时进行,不能通过提示词注入或手写工具参数绕过。
创建、编辑、暂停与恢复还要求运行时根智能体的当前轮次已经接纳一条用户消息或用户 steering(转向)事件。根所有权来自实时智能体图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子智能体则仍然是子智能体,不能改变这些状态。用户来源是宿主的证明:`Agent.send()` `steer()` 会把省略的来源默认`{ kind: 'user' }`因此非人类生产者必须标注自己的内容。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
创建、编辑、暂停与恢复还要求运行时根 agent 的当前轮次已经接纳一条用户消息或用户 steering 事件。根所有权来自实时 agent 图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子级则仍是 subagent,不能改变这些状态。用户来源是宿主的证明:每个 `Agent.send()` `steer()` 输入都必须显式提供来源,因此宿主把直接人类内容标`{ kind: 'user' }`,非人类生产者标注自己的来源信息。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
完成与阻塞既接受直接人类权限,也接受准确的当前目标回合。目标回合权限要求存在一条来源为目标的 `user/message`,其中目标 id、修订号和回合都与折叠后的当前目标相等。它只授予这两种终止报告权限。直接人类权限可以立即停止目标。

View File

@@ -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
2026-07-19-same-session-goal-round-driver.md: d23af9a9b05d60d2dccad095455524844f1185b9
2026-07-19-same-session-goal-round-driver.zh.md: f4f0cd6fd575d14427025bdbd8d10bc90e25f780
2026-07-19-same-session-goal-round-driver.md: 0e6be9fe3109336d47867ab52c585dc267309fb4
2026-07-19-same-session-goal-round-driver.zh.md: cfd9d1aa8cbc3c17cd046df4f57a8f79a6877f5c

View File

@@ -24,7 +24,7 @@ When an agent is idle, has no competing queued work, and its current goal is `ac
The `agent/prompt-submit` waterfall is the admission fence. A positive goal source is allowed only when it exactly matches the driver's pending identity and content, the live goal still has that id and revision, activation remains armed, and the round is still the next number. The plugin checks once before delegating and again after downstream hooks return. This second check prevents an async hook from editing or pausing the goal while still admitting the old prompt.
Only the resulting `user/message` is an admitted round and advances the goal fold. A stale reservation becomes a durable `prompt/blocked` plus zero-step rejected turn, but the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy.
Only the resulting `user/message` is an admitted round and advances the goal fold. A stale reservation is discarded before a turn opens; the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy.
### Human work and revision races
@@ -43,7 +43,6 @@ The driver classifies one closed goal-owned turn as follows:
| `error` with code `RATE_LIMIT` or `QUOTA` | block with code `usage-limited` |
| other `error` | block with code `turn-error` |
| `max-tokens` | block with code `max-tokens` |
| non-stale `rejected` | block with code `prompt-rejected` |
| failed durability checkpoint | disarm without changing durable phase |
| `disposed` or `interrupted` | disarm |
| plugin-added unknown result | block for inspection |

View File

@@ -24,7 +24,7 @@ Status: implemented
`agent/prompt-submit` 瀑布是接纳栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed并且该回合仍是下一个编号时才会获准。插件在委托下游监听器前检查一次在下游返回后再检查一次。第二次检查防止异步钩子编辑或暂停目标后旧提示词仍被接纳。
只有最终产生的 `user/message` 才是已接纳目标回合,并推进目标折叠。过期预留会生成持久的 `prompt/blocked` 和零步骤 rejected 轮次,但驱动器会把它标记为过期,不消耗回合数。若下游策略拒绝并非由过期导致,目标会进入 blocked而不会绕过该策略自动重试。
只有最终产生的 `user/message` 才是已接纳目标回合,并推进目标折叠。陈旧预留会在轮次打开前被丢弃;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked而不会绕过该策略自动重试。
### 人类工作与修订竞争
@@ -43,7 +43,6 @@ Status: implemented
| 代码为 `RATE_LIMIT``QUOTA``error` | 以 `usage-limited` 代码阻塞 |
| 其他 `error` | 以 `turn-error` 代码阻塞 |
| `max-tokens` | 以 `max-tokens` 代码阻塞 |
| 非过期的 `rejected` | 以 `prompt-rejected` 代码阻塞 |
| 持久检查点失败 | 解除激活,但不改变持久阶段 |
| `disposed``interrupted` | 解除激活 |
| 插件新增的未知结果 | 阻塞并等待检查 |

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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
2026-07-21-cross-session-references.md: fc084b36e7920a72efff0f363278d24eaebc4c69
2026-07-21-cross-session-references.zh.md: fe4a876b5265fa7ad298adf3b829bcec70e878e8
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-cross-session-references.md
2026-07-21-cross-session-references.md: 18dab5fb85f2201258e2f15c3c069d2668ae80d3
2026-07-21-cross-session-references.zh.md: 44e33ab1ed762b08b6be30d66b82396a5b519c02

View File

@@ -10,7 +10,7 @@ TUI users need to bring relevant work from another conversation into one new mes
## Decision
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferences`. Hosts normalize their protocol into `SessionReferenceInput[]`, call `prepare()` before enqueue, and pass the returned contexts through the generic `SendOptions.contexts` boundary. Core agent packages know only that one queued message may carry frozen `HookContext[]`; they do not parse session URIs or read another log.
`@deepseek-ai/dsh-session-reference` is one context consumer service at `ctx.sessionReferences`. Hosts normalize their protocol into `SessionReferenceInput[]` and call `prepare()` before delivery. The service returns detached readable content plus an optional sourced `UserMessageData` snapshot; core agent packages do not parse session URIs or read another log.
`dsh-session:<base64url(JSON.stringify(sessionId))>` is the canonical host-independent identifier. JSON string encoding precedes base64url so quotes, slashes, backslashes, Unicode, newlines, and every other JavaScript string value round-trip without delimiter ambiguity. TUI renders that URI inside `@[label](uri)`; text-only clients may use the same inline mention. Explicit Markdown mentions reject malformed URIs. Bare text becomes a reference only for a non-empty base64url-shaped payload, whose decode must still be canonical; empty or punctuation-only uses remain ordinary discussion text.
@@ -22,17 +22,17 @@ Preparation deduplicates in first-appearance order, rejects the target id, enfor
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compact`. That marker is part of the compaction capability contract rather than a backend package name. When a source prompt already contains baked prefix context, projection reads only its model-hidden display content, so referencing that target later does not recursively propagate an earlier snapshot. Projection excludes shadowed pre-compaction nodes, tools and results, reasoning, injected context, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags. The `## My request:` text is a routing cue rather than the trust boundary: referenced data may spell those words inside a JSON string, but it cannot forge the closing `</referenced-sessions>` tag or escape the data region. The same serializer drives each source's independent byte accounting. The context declares `prompt-prefix` placement, so AgentLoop persists one `user/message` or `steering/message` containing the snapshot, `## My request:` delimiter, and effective direct prompt. Its model-hidden envelope retains the direct display content and source/retention metadata. Target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type or a separate user-role context message.
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags or escape the data region. The same serializer drives each source's independent byte accounting. AgentLoop persists the snapshot as a sourced `user/message` immediately before the direct `user/message` or `steering/message`; target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type, placement mode, or prompt envelope.
## Message ownership
`send()` and `steer()` snapshot content, resolved source, and contexts together as one deeply frozen lossless-JSON inbox record. Synthetic `inject()` accepts source and model-hidden metadata but not attached contexts, which belong to inbox messages. A claimed ordinary message exposes its attached contexts as the default `agent/prompt-submit` additional contexts; a block writes neither user message nor contexts. The waterfall's returned allow is authoritative, so a listener wrapping `next()` preserves downstream content and contexts unless it intentionally replaces them. After admission, absent or `separate` placement writes an independent `context/message`, while `prompt-prefix` placement bakes context and the effective request into one prompt event. Drained steering bypasses `agent/prompt-submit` but applies the same placement split. Late steering retains the same record when converted to queued input, while cancellation, disposal, and terminal discard drop message and contexts together. `agent/queued` reports the frozen contexts so the observation event describes the complete retained item.
TUI owns the snapshot/direct-message transaction without extending the generic inbox record. Outside the next-step acceptance window, it installs a one-shot outer `agent/prompt-submit` listener before `followup()`; an allowed decision receives the snapshot as `additionalContexts`, while a blocked or discarded prompt releases the listener and writes neither message. During prompt admission or an open turn, TUI calls `inject(snapshot)` then `steer(prompt)`, and AgentLoop stages both for the same safe boundary. If admission fails before that boundary, both remain staged for retry or a later admitted prompt; cancellation or disposal may discard them. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this generic delivery boundary.
This preserves host driving semantics: TUI decides `send()` versus `steer()` from the agent state after preparation, so only its queued path dispatches UserPromptSubmit hooks. Reference preparation is not a new steering protocol and does not create a turn by itself.
Reference preparation is not a new steering protocol and does not create a turn by itself. A `followup()` outside the next-step acceptance window dispatches prompt admission; steering inside the window bypasses it while retaining snapshot order through the shared outbox.
## Host adapters
TUI combines session candidates with the existing `@` file provider. Each candidate displays the latest folded session title and falls back to the session id; lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the prompt envelope's display content as the user message, and renders its session-reference metadata as a compact source list instead of exposing the complete JSON in the terminal.
TUI combines session candidates with the existing `@` file provider. Each candidate displays the latest folded session title and falls back to the session id; lookup follows the editor's cancellation signal, and session id, cwd, and mention labels escape external terminal controls while the canonical URI retains the original id. TUI prepares only submissions containing structured mentions, disables duplicate submit while awaiting snapshots, restores failed input, renders the readable direct content as the user message, and renders session-reference source metadata as a compact source list instead of exposing the complete JSON in the terminal.
The [automation-only ACP transport](../simplification/2026-07-23-acp-automation-only-protocol.md) deliberately does not mount session-query or session-reference services.
@@ -45,15 +45,15 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
- **Wait for SQLite FTS5** — rejected because snapshot correctness requires exact id reads and canonical surface folding, not content search. FTS improves discovery only.
- **Put mention syntax in `Agent.send()`** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
- **Implement references separately in each host** — rejected because projection, security warning, retention, and persistence would drift across hosts.
- **Place a separate user-role context message beside the prompt** — rejected because two adjacent user messages weaken the prompt's deictic binding: in `@foo what does this session discuss?`, the model may resolve this session” as the current conversation instead of the referenced snapshot.
- **Bake the prefix host-side before `send()`** — rejected because `agent/prompt-submit` must inspect and rewrite only the direct prompt. The effective prompt and attached contexts meet only after admission in AgentLoop, which can apply an `allow.content` rewrite consistently to both combined model content and `envelope.displayContent`; earlier host assembly would expose snapshot bytes to the hook or let those two views diverge.
- **Attach context to `SendOptions` and the inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. A domain-specific admission wrapper and the existing next-step outbox preserve the required pairing without enlarging every message.
- **Bake the prefix host-side before `send()`** — rejected because `agent/prompt-submit` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets TUI hide background bytes from the direct user bubble.
- **Replay the raw source log or restore shadowed events** — rejected because compact defines the current model surface and may intentionally retire sensitive or expensive history.
- **Resume or fork the source** — rejected because the feature supplies read-only background for one target message, not identity or lifecycle continuity.
- **Inject at request time by rereading the source** — rejected because the reference would become nondeterministic, cancellation races could alter its bytes, and target replay would depend on external mutable state.
## Verification
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, title-aware candidate ranking, terminal-control escaping, projection exclusions, non-recursive prompt-envelope projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, frozen message ownership, prompt blocking, send/steer placement, title isolation, missing capability, and compact TUI replay. A keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains one user message ordered as snapshot, request delimiter, and current prompt, without either shadowed string.
Unit and integration coverage pins URI round-trips and text-boundary punctuation, explicit malformed references, title-aware candidate ranking, terminal-control escaping, projection exclusions, non-recursive snapshot projection, backend-independent compact checkpoints, tag-safe framing, deduplication, self-reference, count limits, all-or-nothing reads, prompt cancellation against a non-settling storage read, independent per-source byte retention, prompt blocking, admission-time staging, send/steer placement, title isolation, missing capability, and compact TUI replay. A keyless TUI snapshot runs the real agent loop: the source surface replaces old user/assistant history with a compact checkpoint, the target submits a mention, and the captured model request contains a sourced snapshot message followed by the readable current prompt, without either shadowed string.
## Consequences

View File

@@ -10,7 +10,7 @@ TUI 用户需要把另一场对话中的相关工作带入一条新消息,但
## 决策
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferences` 上的单一上下文消费服务。宿主先把各自的协议规范化为 `SessionReferenceInput[]`在入队前调用 `prepare()`,再通过通用的 `SendOptions.contexts` 边界传递返回的上下文。核心 agent 包只知道一条排队消息可以携带已冻结的 `HookContext[]`;它们既不解析会话 URI也不读取其他日志。
`@deepseek-ai/dsh-session-reference` 是注册在 `ctx.sessionReferences` 上的单一上下文消费服务。宿主先把各自的协议规范化为 `SessionReferenceInput[]`并在交付前调用 `prepare()`。该服务返回分离的可读内容和一份可选的、带来源信息的 `UserMessageData` 快照;核心 agent 包既不解析会话 URI也不读取其他日志。
`dsh-session:<base64url(JSON.stringify(sessionId))>` 是与宿主无关的规范标识符。系统先执行 JSON 字符串编码,再执行 base64url 编码因此引号、正斜杠、反斜杠、Unicode、换行符以及其他任意 JavaScript 字符串值都能无损往返不会因分隔符产生歧义。TUI 把该 URI 渲染到 `@[label](uri)` 中;纯文本客户端可以使用同一种行内提及标记。显式 Markdown 提及标记会拒绝格式错误的 URI。裸文本只有在负载非空且形状符合 base64url 时才会成为引用,而且解码过程仍须通过规范性校验;空负载或只含标点符号的用法仍按普通讨论文本处理。
@@ -22,17 +22,17 @@ TUI 用户需要把另一场对话中的相关工作带入一条新消息,但
投影会保留直接用户消息与 steering中途引导、已完成的 assistant 文本,以及携带由 `dsh-compact` 导出的规范来源标记的检查点用户消息。该标记属于压缩功能契约的一部分而非某个后端包名称。当源提示词已包含合并写入的前缀上下文时投影只读取其模型不可见的显示内容因此后续引用该目标不会递归传播先前的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理reasoning、注入的上下文、其他插件用户消息、仅用于日志的记录以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
系统把一个聚合上下文序列化为 JSON并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签`## My request:` 文本只是路由提示,不是信任边界:被引用数据可以在 JSON 字符串中包含这些词,但无法伪造闭合的 `</referenced-sessions>` 标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。该上下文声明 `prompt-prefix` 放置方式,因此 AgentLoop 会持久化一条 `user/message``steering/message`,其中包含快照、`## My request:` 分隔符和最终生效的直接提示词。其模型不可见封套保留直接显示内容以及来源与保留元数据。因此,目标回放无需新增事件类型或单独的用户角色上下文消息,也能满足「模型可见/日志可重建」不变量。
系统把一个聚合上下文序列化为 JSON并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。AgentLoop 会把快照持久化一条带来源信息的 `user/message`,紧接在直接 `user/message``steering/message` 之前。因此,目标回放无需新增事件类型、放置模式或提示词封套,也能满足「模型可见/日志可重建」不变量。
## 消息所有权
`send()``steer()` 会把内容、解析后的来源和上下文一起快照为一条深度冻结、无损 JSON 的收件箱记录。合成的 `inject()` 接受来源和模型不可见的元数据,但不接受附加上下文,因为上下文属于收件箱消息。普通消息被认领后,其附带的上下文会作为 `agent/prompt-submit` 的默认附加上下文公开提示词被阻止时系统既不写入用户消息也不写入上下文。waterfall瀑布式事件返回的 allow 结果具有最终权威性,因此监听器包装 `next()` 时会保留下游内容与上下文,除非它有意替换这些值。消息被接纳后,未指定放置方式或指定为 `separate` 时会写入独立的 `context/message`;指定为 `prompt-prefix` 时则会把上下文与最终生效的请求合并写入同一个提示词事件。排空 steering 消息时会绕过 `agent/prompt-submit`,但采用相同的放置方式分流。延迟到达的 steering 转换为排队输入时保留同一条记录取消、dispose资源释放和到达终止态后的丢弃则会同时丢弃消息与上下文。`agent/queued` 会报告已冻结的上下文,使观察事件能够描述完整的保留项
TUI 负责快照/直接消息事务,不扩展通用收件箱记录。在 next-step 接受窗口之外,它会在调用 `followup()` 前安装一次性的外层 `agent/prompt-submit` 监听器;获准决策会把快照作为 `additionalContexts` 接收而被阻止或丢弃的提示词会释放监听器并且不写入任何消息。提示词准入期间或轮次打开时TUI 会依次调用 `inject(snapshot)``steer(prompt)`AgentLoop 则将两者暂存到同一个安全边界。如果准入在抵达该边界前失败,两者都会保留暂存状态,供重试或后续获准提示词使用;取消或资源释放可能丢弃它们。这一通用交付边界由[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定
这保留了宿主的驱动语义TUI 在准备完成后根据 agent 状态决定调用 `send()` 还是 `steer()`,因此只有它的排队路径才会分派 UserPromptSubmit 钩子。引用准备过程不是新的 steering 协议,本身也不会创建轮次。
引用准备过程不是新的 steering 协议,本身也不会创建轮次。在 next-step 接受窗口之外调用 `followup()` 会分派提示词准入;窗口内的 steering 会绕过它,同时通过共享 outbox 保持快照顺序
## 宿主适配器
TUI 把会话候选与现有 `@` 文件提供方组合在一起。每个候选项显示最新折叠后的会话标题,没有标题时回退到 session id。候选查询遵循编辑器的取消信号session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把提示词封套的显示内容渲染为用户消息,并把其中的会话引用元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
TUI 把会话候选与现有 `@` 文件提供方组合在一起。每个候选项显示最新折叠后的会话标题,没有标题时回退到 session id。候选查询遵循编辑器的取消信号session id、cwd 和提及标签中的外部终端控制字符会被转义,但规范 URI 仍保留原始 id。TUI 只准备包含结构化提及标记的提交;等待快照时禁用重复提交;失败时恢复输入;它把可读的直接内容渲染为用户消息,并把会话引用来源元数据渲染为精简的来源列表,不在终端中暴露完整 JSON。
[仅面向自动化的 ACPAgent Client Protocol传输层](../simplification/2026-07-23-acp-automation-only-protocol.md)有意不挂载会话查询或会话引用服务。
@@ -45,15 +45,15 @@ TUI 把会话候选与现有 `@` 文件提供方组合在一起。每个候选
- **等待 SQLite FTS5**:不予采纳,因为快照正确性依赖按准确 id 读取和规范表层折叠而不是内容搜索。FTS 只改进候选发现。
- **把提及标记语法放入 `Agent.send()`**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
- **在每个宿主中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
- **在提示词旁放置单独的用户角色上下文消息**:不予采纳,因为相邻的两条用户消息会削弱提示词的指示语绑定:在 `@foo what does this session discuss?`模型可能把「this session」解析为当前对话而不是被引用的快照
- **在调用 `send()` 前由宿主合并前缀**:不予采纳,因为 `agent/prompt-submit` 必须只检查和改写直接提示词。最终生效的提示词与附加上下文只有在 AgentLoop 接纳后才汇合;此时 AgentLoop 可以把 `allow.content` 改写一致应用于合并后的模型内容和 `envelope.displayContent`。若由宿主更早组装,就会向该钩子暴露快照字节,或使这两个视图发生偏离
- **把上下文附加到 `SendOptions` 和收件箱记录**不予采纳因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域专用的准入包装层和现有 next-step outbox 可以保持所需配对,而无需扩大每条消息
- **在调用 `send()` 前由宿主合并前缀**:不予采纳,因为 `agent/prompt-submit` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 TUI 从直接用户气泡中隐藏背景字节
- **回放原始源日志或恢复被遮蔽的事件**:不予采纳,因为压缩定义了当前模型表层,并且可能有意淘汰敏感或开销高昂的历史内容。
- **恢复或 fork 源会话**:不予采纳,因为本功能只为一条目标消息提供只读背景,不提供身份或生命周期连续性。
- **在请求时重新读取源会话并注入**:不予采纳,因为这会让引用变得不确定,取消竞态可能改变其字节内容,目标回放也会依赖可变的外部状态。
## 验证
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、会考虑标题的候选排序、终端控制字符转义、投影排除规则、提示词封套的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、冻结的消息所有权、提示词阻止、sendsteer 放置方式、标题隔离、功能缺失和精简的 TUI 回放。无密钥 TUI 快照会运行真实的 agent loop智能体循环源表层用一个压缩检查点替换旧的用户assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条用户消息,其中依次为快照、请求分隔符和当前提示词,并且不包含任一被遮蔽的字符串。
单元与集成测试覆盖 URI 无损往返与文本边界标点、显式格式错误的引用、会考虑标题的候选排序、终端控制字符转义、投影排除规则、快照的非递归投影、与后端无关的压缩检查点、标签安全封套、去重、自引用、数量限制、读取的全有或全无、存储读取不结束时取消提示词、逐源独立字节保留、提示词阻止、准入期间的暂存、sendsteer 放置方式、标题隔离、功能缺失和精简的 TUI 回放。无密钥 TUI 快照会运行真实的 agent loop智能体循环源表层用一个压缩检查点替换旧的用户assistant 历史,目标会话提交一个提及标记,捕获到的模型请求包含一条带来源的快照消息,后面跟随可读的当前提示词,并且不包含任一被遮蔽的字符串。
## 后果