diff --git a/.agents/notes/AGENTS.md b/.agents/notes/AGENTS.md
new file mode 100644
index 0000000000..958aff54fc
--- /dev/null
+++ b/.agents/notes/AGENTS.md
@@ -0,0 +1,3 @@
+# AGENTS.md — Agent Notes
+
+Agent Notes are effectively RFCs written by agents: durable proposals and decision records that preserve rationale, alternatives, consequences, and verification contracts. Follow the [documentation standard](../../docs/AGENTS.md) and the [Agent Note contract](README.md).
diff --git a/.agents/notes/README.md b/.agents/notes/README.md
new file mode 100644
index 0000000000..e62dc18954
--- /dev/null
+++ b/.agents/notes/README.md
@@ -0,0 +1,111 @@
+# Agent Notes
+
+One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the front door and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format).
+
+## Layout and naming
+
+Every Agent Note has two axes, both encoded in its **path** — `{lifecycle}/{class}/yyyy-mm-dd-topic-title.md`:
+
+- **Lifecycle** (the top-level folder) is the Agent Note's status, and an Agent Note moves between folders as that status changes:
+ - **`proposed/`** — proposals reviewed before implementation; not yet built (or only partly).
+ - **`implemented/`** — the decision shipped. The file records what was decided and what was rejected, and is **kept current with what actually shipped**: when the code later moves a file, renames a package, or changes a key/default, the Agent Note is updated in the same change to match (facts only — paths, names, structure — not the decision itself). See [implemented/AGENTS.md](implemented/AGENTS.md).
+ - **`rejected/`** — the proposal was considered and declined. Kept for the record so the rejection isn't re-litigated.
+- **Class** (the nested folder) is the *kind* of decision — see [Classification](#classification) below.
+
+The date in the filename is when the topic was **first proposed** (per git history). Cross-references between Agent Notes use relative markdown links (`[topic](../../implemented/architecture/2026-…-….md)`) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders.
+
+The tree is the inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale.
+
+## Classification
+
+Each Agent Note belongs to one path-encoded class from the closed set in `scripts/agent-note-tree.ts`; the classification gate rejects other folders. Adding a class requires updating the canonical set and this section. See the [classification Agent Note](implemented/process/2026-06-20-agent-note-classification.md).
+
+| Class | What it covers |
+|---|---|
+| `feature` | A new user- or model-facing capability. |
+| `bug-fix` | Corrects a defect or closes a gap a postmortem surfaced. |
+| `simplification` | Removes code, behavior, or surface area without adding a capability. |
+| `architecture` | A structural decision about the **shipped source** — how packages relate, what the runtime vocabulary is. |
+| `process` | Tooling, policy, or workflow **around** the code — gates, the package manager, vendoring — not runtime behavior. |
+| `testing` | Test infrastructure and strategy. |
+
+The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. (`refactor` is deliberately absent — it overlaps `simplification`, whose discriminator, "does observable behavior change?", already covers it.)
+
+## When to write one
+
+Every non-trivial change MUST add or update at least one Agent Note in the same PR. A change is non-trivial when it alters behavior, architecture, a cross-file or cross-package contract, process or tooling, testing strategy, an on-disk, wire, or configuration format, or another decision a maintainer may reasonably revisit. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)).
+
+Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Only a purely mechanical or local edit with no behavioral, contractual, structural, process, or rationale change is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one and cross-link. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md).
+
+## The file format
+
+Every Agent Note follows one in-file format, enforced by `pnpm run verify-agent-note-format` ([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts), part of `doc-sync`); the rationale for the format — and the alternatives it rejected — is [the uniform-format Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.md).
+
+### The header block
+
+The first three lines of every Agent Note are exactly:
+
+```markdown
+# Agent Note:
+
+Status:
+```
+
+followed by a blank line. The `Status:` value is one of three forms, and must agree with the lifecycle folder the file sits in — the gate cross-checks them:
+
+- `Status: proposed`
+- `Status: implemented`
+- `Status: rejected — `
+
+The status carries no dates and no parentheticals: the filename holds the first-proposed date, git holds everything else, and an "accepted in amended form" note is body content (state the amendment where the decision is stated). The rejection reason is the one status with content, because a rejected Agent Note's verdict is the fact readers come for.
+
+### The body skeleton
+
+Every Agent Note opens its body with `## Problem` — the motivation, written to stand without the solution. What follows depends on the lifecycle; recurring sections use these canonical names and nothing else, while genuinely bespoke technical sections (package topology, wire contracts, schemas) remain free-form between the required ones.
+
+#### `proposed/`
+
+```markdown
+## Problem
+## Proposal
+…bespoke sections…
+## Alternatives considered
+## Acceptance criteria
+## Risks
+```
+
+`## Proposal` is the intended change and may legitimately speak in the future tense — plans, migration steps, and open questions belong here while the work is unbuilt. `## Acceptance criteria` says what observable state means done. `## Risks` covers both what could go wrong and what the change knowingly gives up.
+
+#### `implemented/`
+
+```markdown
+## Problem
+## Decision
+…bespoke sections…
+## Alternatives considered
+## Consequences
+```
+
+`## Decision` describes shipped reality in the present tense, and the whole file is kept current with it per [implemented/AGENTS.md](implemented/AGENTS.md). `## Consequences` records what the trade-off cost **and** bought. Proposal-era headings are spec-speak here and the gate rejects them: `## Proposal`, `## Plan`, `## Migration plan`, and `## Acceptance criteria` may not appear in an implemented Agent Note (the [slop checklist](../../docs/AGENTS.md) names why). A `## Testing`, `## Deferred`, or `## Related` section is fine where it states present-tense fact.
+
+#### `rejected/`
+
+A rejected Agent Note is the proposal, frozen: it keeps whatever proposal-time sections it had (including `## Acceptance criteria` or `## Plan`), and the verdict lives on the `Status:` line. Only the header block, the `## Problem` opener, a `## Proposal` section, and the Alternatives-considered mandate below apply.
+
+### Alternatives considered — mandatory
+
+Every Agent Note carries an `## Alternatives considered` section: each genuine alternative and why it lost, one bold-led paragraph per alternative or a `### Why not ?` subsection per contested one. A decision recorded without what it beat invites re-litigation — the failure Agent Notes exist to prevent.
+
+Alternatives are recorded, never invented. An Agent Note dated before 2026-07-05 whose alternatives are not reconstructible from the record carries this exact comment in place of the section, which the gate accepts for pre-format files only:
+
+```markdown
+
+```
+
+### Moving between lifecycles
+
+Moving a file between lifecycle folders means updating the `Status:` line and re-satisfying that folder's skeleton in the same change — the gate fails the move otherwise. Concretely, `proposed/` → `implemented/` rewrites `## Proposal` into a present-tense `## Decision`, folds `## Acceptance criteria` and `## Risks` into `## Consequences` (or a present-tense `## Testing`/`## Verification` section for what now pins the behavior), and drops plans in favor of what shipped — the rewrite [implemented/AGENTS.md](implemented/AGENTS.md) requires, made mechanical. `proposed/` → `rejected/` only adds the reason to the `Status:` line and freezes the file.
+
+### Chinese counterparts
+
+A `.zh.md` counterpart mirrors its English sibling's structure section-for-section under the [i18n contract](../../docs/i18n/README.md); the machine-checked header tokens (`# Agent Note: ` and the `Status:` line) stay in English verbatim. The format gate skips `.zh.md` files — the pairing gate owns their consistency.
diff --git a/.agents/notes/implemented/AGENTS.md b/.agents/notes/implemented/AGENTS.md
new file mode 100644
index 0000000000..5fb3fde8e2
--- /dev/null
+++ b/.agents/notes/implemented/AGENTS.md
@@ -0,0 +1,11 @@
+# AGENTS.md — Implemented Agent Notes
+
+These Agent Notes describe shipped decisions. Follow the [root instructions](../../../AGENTS.md), [documentation standard](../../../docs/AGENTS.md), and [Agent Note format](../README.md#the-file-format); `verify-agent-note-format` gates the lifecycle-specific structure.
+
+## Keep an implemented Agent Note current with what actually shipped
+
+Keep paths, symbols, defaults, and mechanisms current in the same change that alters them. Rewrite stale facts in place; do not append change history.
+
+### This is not a license to rewrite the *decision*
+
+Update factual realization in place. A reversal of the decision or its rationale requires a new Agent Note and cross-link; see the [Agent Note contract](../README.md).
diff --git a/docs/rfc/implemented/CLAUDE.md b/.agents/notes/implemented/CLAUDE.md
similarity index 100%
rename from docs/rfc/implemented/CLAUDE.md
rename to .agents/notes/implemented/CLAUDE.md
diff --git a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md
similarity index 64%
rename from docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md
rename to .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md
index 1c22f9b7ca..1133b990c3 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md
@@ -1,4 +1,4 @@
-# RFC: Provider-neutral content-block vocabulary owned by dsh-llm
+# Agent Note: Provider-neutral content-block vocabulary owned by dsh-llm
Status: implemented
@@ -10,7 +10,7 @@ The harness needs one internal language for messages that the loop, session log,
Own the vocabulary: messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs.
-In-session context injection (`context/message`, `steering/message`) renders as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role.
+In-session context injection (`context/message`) and mid-turn steering (`steering/message`) originally rendered as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Both now project as plain user content with no wrapper; see [the injected-content-envelope Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md). Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role.
## Alternatives considered
@@ -20,7 +20,7 @@ In-session context injection (`context/message`, `steering/message`) renders as
## Consequences
- Reasoning has a core home without provider-specific shapes.
-- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image RFC](../simplification/2026-07-04-drop-image-content-block.md).
-- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../simplification/2026-07-04-drop-inert-request-knobs.md) RFCs.
+- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md).
+- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../simplification/2026-07-04-drop-inert-request-knobs.md) Agent Notes.
- Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests.
-- IDs that cross package boundaries are branded (`CallId`, `SessionId`, `AgentId`) — nominal typing at zero runtime cost.
+- IDs that cross package boundaries are branded (`CallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost.
diff --git a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.md b/.agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md
similarity index 95%
rename from docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.md
rename to .agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md
index 18923fbb60..bf8c02140a 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md
@@ -1,4 +1,4 @@
-# RFC: Custom typed tool-schema DSL instead of schemastery
+# Agent Note: Custom typed tool-schema DSL instead of schemastery
Status: implemented
diff --git a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
similarity index 98%
rename from docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
rename to .agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
index aac3991f46..90e215019b 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
@@ -1,4 +1,4 @@
-# RFC: Source-owned session immutability and dev-mode invariants
+# Agent Note: Source-owned session immutability and dev-mode invariants
Status: implemented
diff --git a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
similarity index 96%
rename from docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md
rename to .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
index 4539fb40ba..bab36ee783 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
@@ -1,4 +1,4 @@
-# RFC: Event-sourced sessions with derived message history
+# Agent Note: Event-sourced sessions with derived message history
Status: implemented
diff --git a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
similarity index 73%
rename from docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
rename to .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
index 8293924d37..abdadb447b 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
@@ -1,4 +1,4 @@
-# RFC: Microkernel — extension via Cordis event taxonomy, one concrete loop
+# Agent Note: Microkernel — extension via Cordis event taxonomy, one concrete loop
Status: implemented
@@ -10,8 +10,8 @@ The product principle is "everything is a plugin": hooks, /goal, /loop, dynamic
Pure Cordis event taxonomy. The loop's extension seams are typed events with deliberate dispatch modes:
-- **waterfall** (around-middleware) where plugins transform, veto, or wrap: `agent/prompt-submit`, `agent/request`, `agent/step-result`, `agent/turn-continuation`, `tools/pre-execute`, `tools/execute`, `tools/post-execute`, `llm/stream`, `system-prompt/assemble`.
-- **serial** (awaited in listener order; a bail value stops later listeners) for ordered checkpoints: every `agent/pre-step` listener runs when all abstain, while the first stop returned from `agent/turn-stop` makes the terminal decision final.
+- **waterfall** (around-middleware) where plugins transform, veto, recover, or wrap: `agent/prompt-submit`, `agent/request`, `agent/request-error`, `agent/step-result`, `agent/turn-continuation`, `tools/pre-execute`, `tools/execute`, `tools/post-execute`, `llm/stream`, `system-prompt/assemble`.
+- **serial** (awaited in listener order; a bail value stops later listeners) for ordered checkpoints: every `agent/pre-step` and `agent/post-step` listener runs when all abstain, while the first stop returned from `agent/turn-stop` makes the terminal decision final.
- **parallel** (awaited fan-out) where every listener must get an independent chance: the `session/flush` durability checkpoint.
- **emit** (synchronous fire-and-forget) for notifications: turn/step boundaries, stream chunks, lifecycle, errors, and the contained immutable `tools/result` observation.
@@ -23,7 +23,7 @@ The event vocabulary lives in interface packages (dsh-agent declares the agent/*
## Consequences
-- Every MVP feature maps to a listener (the [feature → mechanism map](../../../cookbook/extension-cookbook.md#the-feature--mechanism-map) is the proof obligation, kept current).
+- Every MVP feature maps to a listener (the [feature → mechanism map](../../../../docs/cookbook/extension-cookbook.md#the-feature--mechanism-map) is the proof obligation, kept current).
- HMR and disposal come free: listeners and registrations are Cordis effects.
- Waterfall semantics (call `next()` or short-circuit) are non-obvious and must be taught — documented in AGENTS.md and covered by composition tests.
- The loop must be defensive: plugin exceptions are contained at turn level, steering from any seam is never stranded (regression-tested).
diff --git a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
similarity index 94%
rename from docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md
rename to .agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
index 33bf241c74..454f12d0af 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md
@@ -1,4 +1,4 @@
-# RFC: Runtime arg validation at the model boundary
+# Agent Note: Runtime arg validation at the model boundary
Status: implemented
@@ -19,4 +19,4 @@ The validator mirrors `schemaSpecToJsonSchema` semantics exactly — same struct
- `ToolArgsError` is a plain `Error` with a `code` field for now; if a harness-wide error taxonomy lands it becomes a subclass without changing callers that read `.message`.
- Validation cost is negligible next to a model call.
-
+
diff --git a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md
similarity index 94%
rename from docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md
rename to .agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md
index 01e50da2ff..eacd409a57 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md
@@ -1,4 +1,4 @@
-# RFC: Structured error taxonomy
+# Agent Note: Structured error taxonomy
Status: implemented
@@ -21,4 +21,4 @@ A single `HarnessError extends Error` base in `dsh-llm` (the leaf package every
- `deriveMessages` does not surface `error` into model history — the model still sees the text block; the structured field is for code and replay.
- Argument validation and dev invariants retain their existing codes and behavior; the shared base adds cross-seam routing metadata without changing model-facing text.
-
+
diff --git a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md b/.agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
similarity index 95%
rename from docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
rename to .agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
index 5c78ef4280..59ea9117dc 100644
--- a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
+++ b/.agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md
@@ -1,4 +1,4 @@
-# RFC: Tool schemas are part of the system-prompt assembly
+# Agent Note: Tool schemas are part of the system-prompt assembly
Status: implemented
diff --git a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md
similarity index 88%
rename from docs/rfc/implemented/architecture/2026-06-13-capability-seams.md
rename to .agents/notes/implemented/architecture/2026-06-13-capability-seams.md
index 6887660de0..5ca299abc0 100644
--- a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.md
+++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md
@@ -1,4 +1,4 @@
-# RFC: Capability seams — interface / implementation / consumer split
+# Agent Note: Capability seams — interface / implementation / consumer split
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer surface* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
-This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.bash`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this RFC does.
+This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.bash`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does.
## Decision
@@ -23,8 +23,8 @@ The split is not mandatory when the parts are genuinely one concern: the LLM sea
## Alternatives considered
- **One combined package** — rejected because it recouples the three rates of change the split exists to separate (the whole point).
-- **`@cordisjs/plugin-capability`** — a different axis entirely: it is a permission/capability-*security* service (named permissions with inheritance, tested against a session via `ctx.capability.test`), a candidate for the deferred permissions/sandbox work on the `tools/pre-execute` deny/ask seam, NOT a mechanism for swapping implementations. Confusing the two ("capability") is the trap this RFC names.
+- **`@cordisjs/plugin-capability`** — a different axis entirely: it is a permission/capability-*security* service (named permissions with inheritance, tested against a session via `ctx.capability.test`), a candidate for the deferred permissions/sandbox work on the `tools/pre-execute` deny/ask seam, NOT a mechanism for swapping implementations. Confusing the two ("capability") is the trap this Agent Note names.
## Consequences
-More packages and more boilerplate per capability (a `package.json`/`tsconfig`/README trio, the inject wiring). Bought: implementations and consumers ship and version independently, and a new backend never risks the model-facing contract. The rule is documented in [AGENTS.md](../../../../AGENTS.md) § Conventions ("Capability seams are three packages") and [architecture.md](../../../architecture.md) § "Capability seams"; the bash trio is the reference template. When to fold vs. split is a judgment call the architecture doc spells out — this RFC records *why* the default is to split.
+More packages and more boilerplate per capability (a `package.json`/`tsconfig`/README trio, the inject wiring). Bought: implementations and consumers ship and version independently, and a new backend never risks the model-facing contract. The rule is documented in [AGENTS.md](../../../../AGENTS.md) § Conventions ("Capability seams are three packages") and [architecture.md](../../../../docs/architecture.md) § "Capability seams"; the bash trio is the reference template. When to fold vs. split is a judgment call the architecture doc spells out — this Agent Note records *why* the default is to split.
diff --git a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
similarity index 96%
rename from docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md
rename to .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
index 0ded28f598..7f2f5933ad 100644
--- a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md
+++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
@@ -1,4 +1,4 @@
-# RFC: Two LLM adapters as a design-verification twin
+# Agent Note: Two LLM adapters as a design-verification twin
Status: implemented
@@ -22,4 +22,4 @@ The rule they enforce: **anything the StreamChunk vocabulary cannot express for
## Consequences
-The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the hand-rolled adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding RFC.
+The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the hand-rolled adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note.
diff --git a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
similarity index 82%
rename from docs/rfc/implemented/architecture/2026-06-14-session-persistence.md
rename to .agents/notes/implemented/architecture/2026-06-14-session-persistence.md
index 327aa8eac3..ffb3afa51a 100644
--- a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.md
+++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
@@ -1,10 +1,10 @@
-# RFC: Session persistence as an abstract service over the existing `SessionEvent`
+# Agent Note: Session persistence as an abstract service over the existing `SessionEvent`
Status: implemented
## Problem
-Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume ("continue yesterday's task"), durable forking, and the ACP `session/load` method ([ACP support](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)) were all impossible.
+Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume ("continue yesterday's task"), durable forking, and the ACP `session/load` method ([ACP support](../feature/2026-06-14-acp-agent-client-protocol.md)) were all impossible.
The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append-only log the single source of truth and derives LLM history from it. Persistence had to stay faithful to that: persist the existing `SessionEvent` directly, with no parallel "persisted message" type that the log is converted to and from. The backend also had to be swappable — a file store now, a database store later — behind one interface.
@@ -13,7 +13,7 @@ The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append
Persistence is an abstract **capability seam** ([capability seams](2026-06-13-capability-seams.md), the `dsh-bash` template), not loop or core logic:
1. **Interface** (`dsh-session-persistence`, `ctx.sessionPersistence`) — an abstract `SessionPersistence` service: `create`/`append`/`load`/`list`. Its persisted unit IS the existing `SessionEvent` (`{ type, seq, time, data }`), reused verbatim — no conversion type.
-2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only JSONL log per session (a `SessionHeader` line then one `SessionEvent` per line, verbatim **including `assistant/chunk`**).
+2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session (a `SessionHeader` line then one `SessionEvent` per line, verbatim **including `assistant/chunk`**), encoded as [checksummed Zstandard frames by default](2026-07-19-zstandard-jsonl-session-logs.md) or raw lines by configuration.
Key choices recorded here because they are durable, contested, and surprising:
@@ -21,7 +21,7 @@ Key choices recorded here because they are durable, contested, and surprising:
- **Append-only; a crashed turn is closed, never truncated.** Events through a flushed `turn/end` are never rewritten, and the loop flushes only at turn end. Because one interrupted turn may contain substantial valid work, `load` preserves its contiguous, parseable events and appends error results for unanswered tool calls, a missing `step/end`, and `turn/end` with `{ kind: 'interrupted' }`. The synthetic results keep resumed provider transcripts valid. Only an incomplete final record is discarded; a parse error or sequence gap at or before the last real `turn/end` is corruption and makes the session unloadable.
- **File backend canonical, DB backend a proven drop-in.** `SessionEvent` maps 1:1 onto a row `(session_id, seq, type, time, data)` — `append` is INSERT (in a transaction asserting the contiguous-seq contract), `load` is SELECT … ORDER BY seq. `dsh-session-persistence-sqlite` is exactly this: a `SessionPersistence` subclass with no interface change (opencode runs this exact shape on SQLite/WAL), and it passes the same `runPersistenceContract` suite as the JSONL backend — so the contract holds both backends to identical semantics (lazy materialization, interrupted-turn close on load, contiguous-seq), expressed once over file bytes and once over rows.
- **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header seam is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
-- **`ctx.agents.create()` and `ctx.agents.resume()` are async factories; resume additionally crosses the persistence boundary.** `ctx.agents.resume({ resumeSessionId })` awaits `ctx.sessionPersistence.load`, recreates the live session with the loaded events (so `lastTurnNumber`/`deriveMessages` continue), and starts a fresh agent on the resumed id (NOT `${agentId}-session`). The agent-loop does NOT hard-inject `sessionPersistence` (that would pend non-persistent demos forever); `resume` rejects with a clear error when it is absent.
+- **`ctx.agents.create()` and `ctx.agents.resume()` are async factories; resume additionally crosses the persistence boundary.** `ctx.agents.resume({ resumeSessionId })` awaits `ctx.sessionPersistence.load`, recreates the live session with the loaded events (so `lastTurnNumber`/`deriveMessages` continue), and registers the fresh agent under the exact resumed id. The agent-loop does NOT hard-inject `sessionPersistence` (that would pend non-persistent demos forever); `resume` rejects with a clear error when it is absent.
## Alternatives considered
@@ -31,4 +31,4 @@ Format versioning: the header carries a `version`; `load` rejects any non-curren
## Consequences
-Two new packages and the metadata seam in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and the foundation the ACP `session/load` ([ACP support](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)) needs — all over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, and serializability semantics. Persisting the full log also settles event fidelity: `assistant/chunk` remains verbatim.
+Two new packages and the metadata seam in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and the foundation the ACP `session/load` ([ACP support](../feature/2026-06-14-acp-agent-client-protocol.md)) needs — all over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, and serializability semantics. Persisting the full log also settles event fidelity: `assistant/chunk` remains verbatim.
diff --git a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md b/.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
similarity index 93%
rename from docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
rename to .agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
index e55cd0853e..74ab8b6bf3 100644
--- a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
+++ b/.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md
@@ -1,4 +1,4 @@
-# RFC: Every session event is enclosed in a turn
+# Agent Note: Every session event is enclosed in a turn
Status: implemented
@@ -18,7 +18,7 @@ In case 2, if the injected `context/message` is the last event before a flush/di
**Every session event lives inside a turn** — between a `turn/start` and its matching `turn/end`. Concretely:
- The loop appends queued `user/message` events **after** `turn/start` (inside the turn), not before it. `turn/end` is therefore owed the moment those messages are recorded, and the existing finalizer guarantees it.
-- An `agent.inject()` made while the agent is **running** appends its `context/message` into the already-open turn (unchanged).
+- An `agent.inject()` made while the agent is **running** joins the already-open turn. While the current step executes assistant tool calls, accepted context waits in arrival order until that batch settles, then appends after every recorded result and before the turn closes even when execution is interrupted.
- An `agent.inject()` made while **idle** wraps its `context/message` in a one-shot turn: `turn/start{trigger:{kind:'injection'}}` → `context/message` → `turn/end{completed}`. A new `injection` variant joins the merge-extensible `TurnTriggerMap`.
- The loop derives the next turn number from the log each iteration (`lastTurnNumber(session) + 1`) instead of keeping a private counter, so an idle injection's one-shot turn cannot collide with the next real turn's number.
- The `dsh-invariants` plugin **enforces** the invariant in dev: a `user/message` / `context/message` / `steering/message` appended while no turn is open throws an `InvariantError`.
diff --git a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
similarity index 90%
rename from docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md
rename to .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
index 0121a11fa0..e76ca22e2d 100644
--- a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md
+++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
@@ -1,4 +1,4 @@
-# RFC: Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools
+# Agent Note: Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools
Status: implemented
@@ -18,7 +18,7 @@ We need the filesystem tools to land in the same capability-seam shape as bash b
## Decision
-Filesystem access is a first-class capability seam following [the capability-seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md):
+Filesystem access is a first-class capability seam following [the capability-seam Agent Note](2026-06-13-capability-seams.md):
1. `@deepseek-ai/dsh-fs` (`packages/fs/fs`) owns the abstract `ctx.fs` service, the filesystem vocabulary types, and the `fs/*` policy event vocabulary.
2. `@deepseek-ai/dsh-fs-local` (`packages/fs/fs-local`) provides the first implementation, backed by the local filesystem.
@@ -26,7 +26,7 @@ Filesystem access is a first-class capability seam following [the capability-sea
The consumer package depends only on the interface package, never on `dsh-fs-local`. A deployment that wants a different backend loads a different provider for `ctx.fs` without changing the tool schemas or model-facing prompt guidance.
-The read-before-write/edit and observed-state policy is a fourth package, `@deepseek-ai/dsh-fs-policy` (`packages/fs/fs-policy`), contributed through the `fs/*` event gate rather than living on `ctx.fs`; a deployment loading `dsh-tool-fs` also loads `dsh-fs-policy` to get read-before-write/edit. This RFC established the three-package seam; the split of policy off the provider base class is decided by [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md), and its realization as an event-gate plugin (not a method service) by [the event-gate RFC](2026-06-26-file-context-as-event-gate.md). This document is updated to describe that landed four-package shape.
+The read-before-write/edit and observed-state policy is a fourth package, `@deepseek-ai/dsh-fs-policy` (`packages/fs/fs-policy`), contributed through the `fs/*` event gate rather than living on `ctx.fs`; a deployment loading `dsh-tool-fs` also loads `dsh-fs-policy` to get read-before-write/edit. This Agent Note established the three-package seam; the split of policy off the provider base class is decided by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md), and its realization as an event-gate plugin (not a method service) by [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md). This document is updated to describe that landed four-package shape.
The first backend is deliberately local-only: `dsh-fs-local` implements `ctx.fs` against the host filesystem. Future sibling backends can provide sandboxed, remote, virtual, or project-scoped filesystems behind the same interface.
@@ -34,7 +34,7 @@ The first consumer is deliberately text-file-only: `dsh-tool-fs` exposes model-f
Filesystem permissions and sandboxing are not implied by this split. The local backend resolves relative paths from its configured base directory, but containment policy is a separate decision: either a stricter `ctx.fs` implementation enforces it, or a permission/sandbox plugin wraps `tools/execute` and vetoes calls before they reach the consumer.
-Read-before-write/edit and observed state belong to `dsh-fs-policy`, not `ctx.fs`. Through the `fs/*` event gate, the policy records versions per opaque actor and supplies optional mutation expectations; the provider enforces freshness atomically. `dsh-tool-fs` emits the events without depending on the policy. See the [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](2026-06-26-file-context-as-event-gate.md) RFCs.
+Read-before-write/edit and observed state belong to `dsh-fs-policy`, not `ctx.fs`. Through the `fs/*` event gate, the policy records versions per opaque actor and supplies optional mutation expectations; the provider enforces freshness atomically. `dsh-tool-fs` emits the events without depending on the policy. See the [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](2026-06-26-file-context-as-event-gate.md) Agent Notes.
## Package topology
@@ -71,7 +71,7 @@ The provider seam also carries the freshness hooks that policy builds on — but
- `writeText`/`editText` take an OPTIONAL version expectation: omit it for an unconditional bare-provider mutation, or supply it to guard the mutation inside the backend's atomic critical section.
- The `dsh-fs-policy` plugin decides that expectation on `fs/write-intent`/`fs/edit-intent` and records observed versions on `fs/observed`, keyed by an owner it derives from the opaque event actor (normally `exec.agent.session`).
-Authorization is version freshness, not a full/partial view distinction: any read records the target's version, and a later write/edit is authorized as long as the file is still at that version — so a windowed read of lines 100-150 authorizes an edit of line 120. The observed-state store is a `WeakMap>` inside `dsh-fs-policy`; `dsh-fs` holds none of it and treats the actor as opaque. (This RFC first modeled a `FileState` cache with `full`/`partial` views on `ctx.fs`; the split-fs-seam and event-gate RFCs replaced that with the freshness-based policy plugin described here.)
+Authorization is version freshness, not a full/partial view distinction: any read records the target's version, and a later write/edit is authorized as long as the file is still at that version — so a windowed read of lines 100-150 authorizes an edit of line 120. The observed-state store is a `WeakMap>` inside `dsh-fs-policy`; `dsh-fs` holds none of it and treats the actor as opaque. (This Agent Note first modeled a `FileState` cache with `full`/`partial` views on `ctx.fs`; the split-fs-seam and event-gate Agent Notes replaced that with the freshness-based policy plugin described here.)
Path resolution is explicit and allowed to be async. Local resolution may only normalize a path, but sandboxed/remote/project-scoped backends may need I/O to resolve a user-supplied path into a stable target identity.
@@ -135,7 +135,7 @@ The defensive-pattern classes this repo has been bitten by are pinned directly:
- **Model-facing tools directly over `node:fs`** — the tool package would own execution policy, path resolution, atomic writes, text decoding, and edit semantics at once, coupling the three independently-changing concerns the Problem names and churning schemas on any backend swap.
- **One combined `dsh-fs-tools` package** — the pre-seam shape; rejected for the same interface/implementation/consumer split as bash, and the combined name never became public surface.
-- **Observed-state on `ctx.fs`** — the shape this RFC first landed; superseded by [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [the event-gate RFC](2026-06-26-file-context-as-event-gate.md): a sandboxed/remote backend must not inherit model-facing observation policy, so the provider keeps only the version token and the optional version-guarded mutation.
+- **Observed-state on `ctx.fs`** — the shape this Agent Note first landed; superseded by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md): a sandboxed/remote backend must not inherit model-facing observation policy, so the provider keeps only the version token and the optional version-guarded mutation.
## Consequences
@@ -143,11 +143,11 @@ The defensive-pattern classes this repo has been bitten by are pinned directly:
**The interface can become too local.** Returning fields such as `absolutePath` from `ctx.fs` would make remote, sandboxed, or virtual backends awkward. The contract should expose display metadata without requiring consumers to understand host paths.
-**The interface can become too thin.** If `ctx.fs` only mirrors `node:fs` primitives, `tool-fs` will reimplement binary detection, pagination, atomic writes, and edit semantics. That recreates the coupling this RFC is trying to avoid.
+**The interface can become too thin.** If `ctx.fs` only mirrors `node:fs` primitives, `tool-fs` will reimplement binary detection, pagination, atomic writes, and edit semantics. That recreates the coupling this Agent Note is trying to avoid.
**Edit semantics are race-prone by nature.** Literal edit is a read-modify-write operation; the guard is the backend's atomic mutation critical section plus the optional version expectation, so concurrent edits settle deterministically — one wins, the other gets `FS_STALE_VERSION`.
-**Observed state does not belong on `ctx.fs`.** Recording what an execution context has seen is workflow policy, not raw filesystem I/O. This RFC first placed it inside the filesystem seam; the split-fs-seam RFC then established that a sandboxed/remote backend should not inherit model-facing observation policy, and moved it into the `dsh-fs-policy` plugin. The provider seam keeps only what write/edit safety genuinely needs at the storage layer — a backend-minted version token and an optional version-guarded mutation — while the policy plugin owns owner derivation, observed-state, and read-before-edit gating over the `fs/*` events.
+**Observed state does not belong on `ctx.fs`.** Recording what an execution context has seen is workflow policy, not raw filesystem I/O. This Agent Note first placed it inside the filesystem seam; the split-fs-seam Agent Note then established that a sandboxed/remote backend should not inherit model-facing observation policy, and moved it into the `dsh-fs-policy` plugin. The provider seam keeps only what write/edit safety genuinely needs at the storage layer — a backend-minted version token and an optional version-guarded mutation — while the policy plugin owns owner derivation, observed-state, and read-before-edit gating over the `fs/*` events.
**The `resolve`-then-operate shape costs an extra round-trip per call.** Each tool may resolve a path to an `FsTarget` and then issue the read/write/edit as a separate `ctx.fs` call. For the local backend this is negligible (resolution is in-memory path normalization), but a remote/sandboxed backend may turn each step into its own request, so a single `read` can become two network round-trips. Backends where the round-trip matters can cache or fold resolution internally while preserving the observable contract.
diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md
new file mode 100644
index 0000000000..ef5bb5847a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md
@@ -0,0 +1,48 @@
+# Agent Note: Agent lifecycle and ownership seams
+
+Status: implemented
+
+## Problem
+
+Several ACP and tool-bash limitations were symptoms of the same missing seam: plugins could create or resume agents through `ctx.agents`, but they could not own and dispose one agent independently, and long-running bash tasks carried no stable owner in the executor itself. ACP aborted and awaited agents on disconnect but could not unregister just that session's agent; `session/cancel` could not cancel queued-but-not-yet-started work; and `tool-bash` kept task ownership in a plugin-local `Map`, so an HMR reload could make an old task look unowned.
+
+## Decision
+
+Three seams: the queue-aware cancel, the `AgentHandle` disposer, and the bash owner token.
+
+### 1. Queue-aware `Agent.cancel(reason?)`
+
+A new `cancel()` verb on the `Agent` interface — the single public stop primitive. (It originally shipped alongside a narrower step-only `abort()`; that verb was later removed as unused, leaving `cancel()` the only public way to stop work.) It clears the inbox's queued + steering FIFOs, aborts the in-flight step if any, and drives a **turn-scoped cancellation marker** the driver loop checks at every turn-decision point — so a prompt that is queued-but-not-yet-started never runs, a cancel landing in the pre-step / continuation window drops the about-to-run turn (ending it `aborted`), and a later accepted prompt remains an independent queued turn. `whenIdle()` reaches post-cancel quiescence. ACP `session/cancel` maps to `cancel()`. The marker is armed ONLY when there is something to cancel, so an idle no-op cancel cannot strand the next prompt.
+
+### 2. `AgentHandle` async disposer
+
+`ctx.agents.create`/`resume` (and the `AgentFactory` interface) return `AgentHandle = { agent: Agent; dispose(): Promise }`. The disposer is a **consumer capability** — a registry observer holding only the bare `Agent` cannot tear it down. The caller fiber and registered factory provider are structural co-owners: caller unload enforces structured ownership, while provider unload must stop old instances whose scoped dependency surface resolves through that provider. All three paths reach the same memoized teardown: stop the loop, await its exit and idle flushes (true quiescence, not just the `disposed` status flip), detach the agent, detach its session, and unwind its scope. Each public ID becomes reusable when its exact registry entry detaches; there is no separate reservation-release phase. Config-created agents are already owned by the `AgentLoop` fiber (the handle is discarded). ACP holds each session's disposer in its `SessionRecord` and runs it on disconnect/teardown, so a bare client disconnect leaves no registered agent and no session-store entry — even when `session/load` races teardown (the just-resumed handle is disposed before the closed-guard throw).
+
+**Teardown ORDER is load-bearing for durability**, and the implementation folds the session lifecycle into the agent's SINGLE composite cordis effect (`SessionStore.prepare`/`enter`/`announce`, replacing a sibling-effect split). A fiber unload disposes sibling effects concurrently (`Promise.all`), which would race removing the session store's append publication hooks against the loop's closing `session/flush` and drop the closing `turn/end`; inside one effect the disposers run as an ordered LIFO chain (loop stopped + `await agent.done` BEFORE the session detaches), so the loop's final flush is captured on BOTH the handle's `dispose()` and a fiber unload. The contained `agent/disposed` and `session/disposed` notifications cannot reject the chain or skip later teardown.
+
+### 3. Bash owner token in the seam
+
+Background-task ownership moved from a `tool-bash` plugin-local `Map` into the executor. `BashExecRequest` gains an optional `owner?: string`; the resolved `BashExecSpec` carries it as required-but-nullable `owner: string | undefined` (a forgotten owner is a visible `undefined`, never a silently-absent property). The executor stores the token on its task and exposes it via a new `BashExecutor.ownerOf(id): string | undefined` seam (NOT on the public `BashTask` — one read path, no redundant API). `tool-bash` deletes its `Map` entirely: it stamps `exec.agent?.id` (the shared registry/session id) as the owner at `start`, and `bash_output`/`bash_kill` compare `ctx.bash.ownerOf(id)` to the caller's token with `!== undefined` semantics (an empty-string token is still a real owner). The completion notice finds the live agent by scanning `ctx.get('agents')?.list()` for `agent.id === ownerToken` (read via `ctx.get` — `onTaskDone` runs on the bash fiber, a foreign fiber, where the `ctx.agents` proxy would throw). Because ownership now lives on the task in the executor (disposed with the `dsh-bash` fiber), it SURVIVES a `tool-bash` HMR reload — closing the old `XXX(tool-bash-owner-hmr)` gap. (The `onTaskDone` listener is still effect-scoped to `tool-bash`'s `apply`, so a completion landing during the reload gap still drops its one notice — the pre-existing reload-gap drop — but the ownership fence itself is HMR-proof.)
+
+## Verification
+
+These invariants hold and are pinned by tests:
+
+- ACP disconnect/session close leaves no registered agent AND no session-store entry for that session, even when `session/load` races teardown.
+- `session/cancel` before a queued prompt starts prevents that prompt from running; a later accepted prompt remains an independent queued turn.
+- A `tool-bash` HMR reload does NOT make an existing background task readable or killable by a different session (ownership survives on the executor).
+- Existing non-ACP demos still work without managing handles explicitly; config-created agents remain owned by the `AgentLoop` plugin fiber.
+
+## Session owner tokens are unique among live agents
+
+The bash owner-token comparison relies on the shared `Agent.id`/`SessionId` being unique among live agents. Concurrent same-ID operations may both prepare privately, but publication enters the session and agent in order; `SessionStore.enter()` rejects a duplicate live session id, and every losing transaction rolls its private state back. A programmatic caller therefore cannot publish two live agents with one session token. The access *policy* (token comparison) stays in `tool-bash` (the consumer); the bash seam stores only an opaque `owner` string and never interprets it — the correct interface/implementation/consumer split.
+
+## Alternatives considered
+
+- **A public `BashTask.owner` field** instead of the `BashExecutor.ownerOf(id)` seam — rejected: one read path, no redundant API.
+- **Sibling cordis effects for the agent's session lifecycle** — rejected: a fiber unload disposes sibling effects concurrently (`Promise.all`), racing removal of the store-owned append publication hooks against the loop's closing `session/flush`; the single composite effect's ordered LIFO chain is what captures the closing `turn/end` on both disposal paths.
+- **A separate step-only `abort()` beside `cancel()`** — shipped originally, then removed as unused; `cancel()` is the single public stop primitive ([the public-stop-surface Agent Note](../simplification/2026-06-20-public-agent-stop-surface.md)).
+
+## Consequences
+
+This touched public interfaces (`Agent`, `AgentFactory`, the bash seam) deliberately, not as a local ACP patch. The simple synchronous `Agent.send()` ergonomics were preserved; the async lifecycle path is additive, for owners that need it.
diff --git a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md
similarity index 51%
rename from docs/rfc/implemented/architecture/2026-06-18-session-surface.md
rename to .agents/notes/implemented/architecture/2026-06-18-session-surface.md
index 6ba0df41c4..1916dc1974 100644
--- a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md
@@ -1,4 +1,4 @@
-# RFC: Session surface — a linked list over the event log for LLM message derivation
+# Agent Note: Session surface — an ordered projection over the event log
Status: implemented
@@ -8,13 +8,13 @@ The event log is authoritative, but history manipulation had no durable shared m
## Decision
-Add a **surface** — a derived, cached linked list of "surface nodes" (the subset of events that produce LLM messages) — maintained by `surfaceOp` markers in the event log.
+Add a **surface** — a derived, cached order of event sequences (the subset of events that produce LLM messages) — maintained by `surfaceOp` markers in the event log.
### Two new top-level fields on `SessionEvent`
Every `SessionEvent` gains two optional fields (structural metadata, like `seq`/`time`):
-- **`sourceEventSeqs?: number[]`** — seq numbers of events that are provenance sources (e.g., the `assistant/chunk` seqs that built an `assistant/message`, or the surface nodes shadowed by a compaction marker). Provenance is a core design principle; without it, the replace-range operation cannot be validated on replay.
+- **`sourceEventSeqs?: number[]`** — seq numbers of events that are provenance sources (e.g., the `assistant/chunk` seqs that built an `assistant/message`, or the surface nodes shadowed by a compaction marker). A present `[]` is valid only on `assistant/message` and records a known empty provider stream; omission there means legacy or otherwise unrecorded provenance. Other surface events require a non-empty list when the field is present. Provenance is a core design principle; without it, the replace-range operation cannot be validated on replay.
- **`surfaceOp?: SurfaceOp`** — how this event entered the surface. Absent for non-surface events.
### SurfaceOp: two operations
@@ -25,13 +25,13 @@ export type SurfaceOp =
| { op: 'replace'; start: number; end: number } // shadow [start, end] inclusive
```
-1. **Append** — add a new node to the tail. Used by `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`. The loop passes `surfaceOp: 'append'` on all such appends, and `sourceEventSeqs` where applicable (e.g., `assistant/message` records its `assistant/chunk` sources; `tool/result` records its `tool/call` source).
+1. **Append** — add the new event seq to the tail. Used by `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`. The loop passes `surfaceOp: 'append'` on all such appends and records `sourceEventSeqs` where applicable: every successful `assistant/message` records its complete `assistant/chunk` source set, including `[]`, while `tool/result` records its `tool/call` source.
-2. **Replace** — remove nodes from `start` through `end` (both inclusive) and insert a new node in their place. Both `start` and `end` must be valid surface node seqs in the current surface; `start === end` replaces a single node. The node's `sourceEventSeqs` must contain every shadowed surface node. The shadowed events remain in the log but are no longer on the surface.
+2. **Replace** — remove entries from `start` through `end` (both inclusive) and insert the new event seq in their place. Both `start` and `end` must be present in the current surface; `start === end` replaces one entry. The event's `sourceEventSeqs` must contain every shadowed surface seq. The shadowed events remain in the log but are no longer on the surface.
### SurfaceManager: delta-based, not full rebuild
-A `SurfaceManager` class (private to `Session`) maintains the cached linked list. It tracks `_lastProcessedSeq` and processes only the **delta** (new events since the last access) rather than rescanning the entire log. Because the log is append-only, prior events never change; a seeded log is simply the initial delta folded on first access.
+A `Session` owns one `SurfaceManager` that maintains an ordered `number[]` of event seqs. The manager validates each seed or append candidate without applying it before commit, then processes only committed events since its previous synchronization rather than rescanning the entire log. `Session.surface` exposes the same manager through the readonly `SessionSurface` contract, so acceptance, derived history, compaction, and workspace context share one incremental state. Replace locates its inclusive endpoints by array position and splices the replacement seq into that range; no second manager, link objects, or seq-to-node map duplicates the order.
Delta processing is O(1) when no new events and O(new events) when new events arrive.
@@ -47,23 +47,26 @@ The `repair.ts` module synthesizes `tool/result` closers for orphaned tool calls
### Invariants
-The dev-mode invariants plugin validates: `sourceEventSeqs` references (non-empty, no duplicates, references earlier events, references known seqs) and `surfaceOp` (replace `start ≤ end`, both endpoints are on the tracked surface, the range is non-reversed in surface position, and `sourceEventSeqs` includes every node the range shadows).
+The dev-mode invariants plugin validates: `sourceEventSeqs` references (only `assistant/message` may use an empty list; otherwise no duplicates, references earlier events, and references known seqs) and `surfaceOp` (replace `start ≤ end`, both endpoints are on the tracked surface, the range is non-reversed in surface position, and `sourceEventSeqs` includes every node the range shadows).
Every surface-eligible event must carry `surfaceOp` or it would disappear from derived history. Typed `append` overloads enforce this for literal event types; runtime checks in `append` and the seed constructor cover widened unions and loaded logs. Invalid seeds are rejected rather than upgraded under the pre-release format policy.
## Alternatives considered
- **Per-plugin `agent/request` wrapping** (the pre-surface pattern for history manipulation) — listener-ordering fragility, no durable record of what was changed, and every new manipulation forces another change to core `deriveMessages()`.
-- **Half-open `[start, endExclusive)` replace ranges** — rejected: the surface is a doubly-linked list whose ends are naturally named by node seqs, and single-node replacement (`start === end`) reads naturally with inclusive semantics.
+- **Half-open `[start, endExclusive)` replace ranges** — rejected: endpoints are named by surface event seqs, and single-entry replacement (`start === end`) reads naturally with inclusive semantics.
+- **Linked node objects plus a seq map** — rejected: production did not read predecessor links, the only successor use was the next array position, and replacement already required linear `indexOf` lookup. A single seq array preserves the same asymptotic behavior with one representation to validate.
- **Full rebuild behind a dirty flag** instead of delta processing — O(N²) over a session's lifetime: every single-event append would rescan all prior events.
## Consequences
-- **`packages/core/session`**: New `surface.ts` (`SurfaceManager`), new types (`SurfaceOp`, `SurfaceIntent`), new fields on `SessionEvent`, modified `append()` (third required `SurfaceIntent` param), refactored `deriveMessages()` (walks the surface as the sole derivation path), surface-aware `repair.ts`. The seed constructor rejects a surface-eligible seed event missing its `surfaceOp` marker (see § Invariants).
+- **`packages/core/session`**: `surface.ts` (`SurfaceManager`) maintains one ordered seq array for candidate acceptance and live projection; `SessionSurface` is its readonly public view. `SurfaceOp`/`SurfaceIntent` and the top-level session-event fields record how entries join it. `append()` requires a `SurfaceIntent` for surface events, `deriveMessages()` walks the surface as the sole derivation path, and `repair.ts` emits surface-aware closers. The seed constructor rejects a surface-eligible seed event missing its `surfaceOp` marker (see § Invariants).
- **`packages/core/agent-loop`**: All surface-capable appends pass surface opts. Chunk seqs are collected for `assistant/message` provenance; `tool/call` seqs are captured for `tool/result` provenance.
- **`packages/session-persistence/session-persistence-sqlite`**: Two new nullable TEXT columns (`source_event_seqs`, `surface_op`) on the `events` table; `SCHEMA_VERSION` bumped (bump-and-reject, no migration).
- **`packages/support/invariants`**: Surface-related validation rules.
- **`packages/session-persistence/session-persistence-jsonl`**: No changes required.
- **`packages/session-persistence/session-persistence`**: Abstract interface unchanged.
-The surface is the foundation for future history manipulation. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed nodes — the new node takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically.
+The surface is the foundation for future history manipulation. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically.
+
+A `tool/result` replacement may rewrite exactly one current `tool/result` and must preserve every data field except `content`. Session acceptance enforces this rule together with positional range and provenance validation, independent of optional diagnostic plugins.
diff --git a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
similarity index 88%
rename from docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
rename to .agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
index fef6ad034c..27360087a2 100644
--- a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
+++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
@@ -1,4 +1,4 @@
-# RFC: Shared persistence write coordinator
+# Agent Note: Shared persistence write coordinator
Status: implemented
@@ -10,7 +10,7 @@ Status: implemented
Extract a backend-agnostic `PersistenceCoordinator` into `dsh-session-persistence`. The coordinator owns the orchestration once; each first-party backend composes one (`new PersistenceCoordinator(ctx, this)`), implements a small `PersistenceBackend` hook interface, and delegates its four public service methods (`create`/`append`/`load`/`list`) to it.
-Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The RFC's risk — "a coordinator must not make unusual backends fight an inheritance hierarchy" — is avoided: a backend exposes only the hooks; it cannot reach the coordinator's private orchestration state, and the public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator at all.
+Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The Agent Note's risk — "a coordinator must not make unusual backends fight an inheritance hierarchy" — is avoided: a backend exposes only the hooks; it cannot reach the coordinator's private orchestration state, and the public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator at all.
The coordinator retires each live session from its `session/disposed` notification: it waits for that exact Session object's initialization, serializes a final drain, and then removes the owned state, buffer, and init entries. Failed drains retain their buffers for backend teardown to retry. Settled per-id chain tails remove themselves only when they are still the current tail, so a completion cannot erase a newer operation for the same id. Backend teardown unregisters the write-path listeners before awaiting all admitted retirements, remaining buffers, and chains, then closes the backend.
@@ -28,7 +28,7 @@ Six methods (five required + an optional lifecycle hook) — the only seam betwe
### The opaque torn marker
-The single design choice that keeps the seam clean: the crash-repair "where is the torn tail" token is OPAQUE to the coordinator. The coordinator computes the synthetic closers (it owns `interruptedTurnClosers` from `dsh-session`), but it only ever tests `tornMarker !== undefined` and passes the value straight back to `commitRepair` — it never inspects it. Each backend picks its own marker type: JSONL uses the byte offset to truncate to, SQLite the seq to delete from (both happen to be `number`). The JSONL backend folds its `committedBytes < buffer.byteLength` comparison INSIDE the hook so the returned marker is already `number | undefined`; without that fold the coordinator would have to know about byte lengths.
+The single design choice that keeps the seam clean: the crash-repair "where is the torn tail" token is OPAQUE to the coordinator. The coordinator computes the synthetic closers (it owns `interruptedTurnClosers` from `dsh-session`), but it only ever tests `tornMarker !== undefined` and passes the value straight back to `commitRepair` — it never inspects it. Each backend picks its own marker type: JSONL carries the byte offset to truncate to plus any complete events decoded from an incomplete final frame, while SQLite carries the seq to delete from. The coordinator therefore knows neither byte lengths nor frame recovery state.
## Testing
diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md
new file mode 100644
index 0000000000..e7a3110fce
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md
@@ -0,0 +1,67 @@
+# Agent Note: Branded IDs everywhere they belong
+
+Status: implemented
+
+## Problem
+
+The harness brands `CallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using the `Branded = string & { readonly [BRAND]: B }` machinery (owned by the type-only `@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today.
+
+**Gap 1 — unbranded cross-boundary IDs in the bash seam.** The background-task id is a plain `string`: `BashTask.id: string` (`packages/bash/bash/src/types.ts`), carried as `string` through the whole executor seam (`BashExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)` in `packages/bash/bash/src/index.ts`) and validated/passed as `string` by the model-facing tools (`validateTaskId`, `assertTaskAccess`, the `task_id` schema arg in `packages/bash/tool-bash/src/index.ts`). It is generated by a per-executor counter — `` `bash-${this.nextTaskId++}` `` in `packages/bash/bash-local/src/index.ts` — which gives it **exactly the same `name-N` shape as `SessionId`'s default** (`` `session-${++counter}` `` in `packages/core/session/src/index.ts`). A bash task id and a session id are trivially swappable at a call site and the compiler says nothing. This is the headline case the user asked about, and it is a model-facing id (the model passes `task_id` back to `bash_output`/`bash_kill`), so a confusion here is reachable from untrusted input.
+
+The bash **owner token** is the related sub-case: `BashExecRequest.owner?: string` and `BashExecSpec.owner: string | undefined` (`packages/bash/bash/src/types.ts`) are documented as a deliberately *opaque* isolation key, but in every live caller the value IS the owning agent's shared `Agent.id`/`SessionId` (`callerToken = (exec) => exec.agent?.id` in `packages/bash/tool-bash/src/index.ts`) wearing a different seam-local name. It is compared for access control (`owner !== callerToken(exec)`), so a mismatched-but-well-typed string here is a cross-session isolation bug the type system currently cannot catch. This is the shared id alias covered by the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md).
+
+**Gap 2 — brand erosion at the seams of the *already-branded* IDs.** Even `CallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), `ToolPresenter`'s call-id map, ACP's session-id records and loading set, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized.
+
+## Decision
+
+A type-only change. Brands are zero-cost casts; nothing about runtime behavior, serialization, comparison, or the wire format changes. The work is in three parts, all honoring the existing "not every string" policy.
+
+- **Brand the bash task id.** Add `BashTaskId = Branded<'BashTaskId'>` plus its same-named factory in `packages/bash/bash/src/types.ts` (the package that *owns* the id), importing `Branded` from `@deepseek-ai/dsh-brand` exactly as `SessionId` does. The brand primitive lives in the dependency-free `dsh-brand` utility package precisely so `dsh-bash` can brand its ids by depending on it alone — it never pulls in `dsh-llm` (or `dsh-session`) just to reach `Branded`. Thread it through `BashTask.id`, the `BashExecutor` seam methods (`get`/`ownerOf`/`readOutput`/`kill`), the generation site in `dsh-bash-local` (brand the counter output once, at creation), and the `dsh-tool-bash` validate/access surface (`validateTaskId` returns a `BashTaskId`; `task_id` is branded at the tool boundary where the model's string arrives).
+
+- **Mint a distinct `OwnerToken` brand.** Add `OwnerToken = Branded<'OwnerToken'>` in `packages/bash/bash/src/types.ts`; type `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` as `OwnerToken | undefined`. The `dsh-tool-bash` consumer casts the agent's shared `id` (`SessionId`) into an `OwnerToken` at the boundary — the one place the two vocabularies meet. The bash seam never imports `dsh-session`. (Rationale in the next section.)
+
+- **Stop the brand erosion.** Propagate the existing brands to the `Map` key types and public method params listed under Gap 2 — `Map`, `Map`, `get(id: SessionId)`, `Map`, ACP's `SessionId` surface, and the coordinator's `Map`. This is the larger mechanical share of the diff and the part that makes the *existing* brands actually load-bearing on lookups, not just on struct fields.
+
+Illustrative shape (the factory pattern is identical to the three existing brands):
+
+```ts ignore-check
+import type { Branded } from '@deepseek-ai/dsh-brand'
+
+/** A background bash task handle (generated `bash-N` by the local executor). */
+export type BashTaskId = Branded<'BashTaskId'>
+export function BashTaskId(id: string): BashTaskId {
+ return id as BashTaskId
+}
+
+/** A bash task's opaque isolation key — the consumer's owner identity, NOT the bash seam's. */
+export type OwnerToken = Branded<'OwnerToken'>
+export function OwnerToken(id: string): OwnerToken {
+ return id as OwnerToken
+}
+```
+
+## Alternatives considered
+
+### Why not typing `owner` as `SessionId`?
+
+The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (interface `dsh-bash`, implementation `dsh-bash-local`, consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/bash/bash/src/types.ts`). Typing the seam's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-bash` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that casts its `SessionId` into an `OwnerToken`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling.
+
+## Out of scope / possible extensions
+
+Kept deliberately narrow per the "not every string needs a brand" policy. Each of these is a plausible future brand, deferred with a reason, not a commitment:
+
+- **`ModelId`** (`GenerateOptions.model`, the `LlmService` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this Agent Note's blast radius focused.
+- **`ToolName`** (the `ToolRegistry` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand.
+- **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything.
+- **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low.
+- **Validated construction** — the brand factories are pure casts with no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string today. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a *runtime-behavior* change with its own design (what is "malformed"? what do we do on failure?) and belongs in its own Agent Note, not bundled into this type-only pass.
+
+## Verification
+
+The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-bash` and threaded end-to-end (executor seam, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing surface) with no `dsh-bash` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `task_id`), never as scattered `as` casts.
+
+## Consequences
+
+- **Mechanical churn across two surfaces.** Propagating brands touches the bash seam (interface + impl + consumer) and the ACP session-id surface plus the persistence coordinator. The churn is broad but low-severity: a missed site is a compile error, not a silent bug. The change is observably type-only — no snapshot or e2e behavioral diff. It sits next to the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md) because both touch the session-id / owner-token boundary; `OwnerToken` stays distinct from the unified id for the decoupling reason above.
+- **Brands do not validate.** A brand is a confusability guard, not a correctness proof: a *wrong* session id that is still a well-formed string passes the type checker exactly as before. This Agent Note does not close that gap (see Out of scope) — it only stops the *category* error of passing the wrong *kind* of id.
+- **The "where to stop" line stays a judgment call.** Branding `BashTaskId` but not `ToolName`, `OwnerToken` but not `ModelId`, is a taste call about which strings "could plausibly be confused." Reasonable reviewers may want more or fewer; the policy in `brand.ts` is the tie-breaker, and this Agent Note errs toward the ids that are model-facing or used for access control.
diff --git a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md b/.agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md
similarity index 55%
rename from docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md
rename to .agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md
index 0582515c62..05d72e4e49 100644
--- a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md
@@ -1,4 +1,4 @@
-# RFC: Extract example apps into packages
+# Agent Note: Extract example apps into packages
Status: implemented
@@ -6,29 +6,28 @@ Status: implemented
An example folder is supposed to be *thin* — the variable wiring of a demo, not the demo's machinery. Before this change it was thick. Each example carried a hand-rolled `start.ts` boot bootstrap, an infra preamble (`timer`, and — for the stdio demos — `logger` + `hmr`), nested includes of three shared YAML fragments (`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`), and per-example `agent-loop`/persistence/system-prompt config. The actual app — the spine of services every agent needs — was spread across the leaf and those includes.
-The leaf configs also owned a coupled front door. ACP requires stdout purity and creates agents through `session/new`; stdio requires a console logger and a pre-created `main`. Prose warnings were the only guard against combining these incorrectly, while three `start.ts` files duplicated the Loader bootstrap and lifecycle code.
+The leaf configs also owned coupled front doors. ACP requires stdout purity and creates agents through `session/new`; terminal and Headless apps pre-create `main` but have different process I/O contracts. Prose warnings were the only guard against combining these incorrectly, while three `start.ts` files duplicated the Loader bootstrap and lifecycle code.
## Decision
Each example is now **mostly an invocation of an app package**, splitting the wiring along the existing [interface / implementation / consumer seam](2026-06-13-capability-seams.md): the **app package owns the composition**, the leaf `cordis.yml` owns only the **swappable choices** (which LLM adapter, which bash executor, model, prompt, persistence root).
- **`@deepseek-ai/dsh-agent-spine-demo`** ([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo)) composes the providerless, executor-less, UI-less spine and forwards the loop's agent-list config. Its dependency on the concrete loop is intentional because this package composes the spine rather than extending it; swapping the loop means supplying another bundle.
-- **`@deepseek-ai/dsh-stdio-demo`** ([packages/examples/stdio-demo](../../../../packages/examples/stdio-demo)) and **`@deepseek-ai/dsh-acp-demo`** ([packages/examples/acp-demo](../../../../packages/examples/acp-demo)) bake in their front doors. Stdio includes `ui-stdio`, a console logger, and `main`; ACP includes the bridge and JSONL persistence but no stdout logger or pre-created agent. Leaves may add plugins, but the safe composition is now the default artifact.
-- **`start.ts` is gone.** Each app package exposes a `bin` (`dsh-stdio-demo` / `dsh-acp-demo`); the `demo:*` scripts invoke it (e.g. `dsh-stdio-demo ./cordis.yml`). The Loader-boot tail, `.env` loading, and fail-loud guards live in the shared [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) package (unit-tested under the per-file coverage gate — see [share the app bins' boot glue](../simplification/2026-07-04-share-app-bin-boot-glue.md)); each bin is a thin self-executing composition over those helpers plus its app-specific lifecycle (the ACP bin: snapshot-mode selection and stdin-dispose). The `bin.ts` files themselves stay coverage-excluded (self-executing CLI entries, like the old `start.ts`) and are driven by the keyless Loader-path tests.
-- **Each leaf `cordis.yml` collapses** to backends + config: the LLM adapter (`llm-deepseek` with apiKey/models, or `llm-replay`), the bash executor (`bash-local`), `hmr` for the stdio demos (see the amendment below), and one app entry carrying the app's config (model, system prompt, persistence root — surfaced as the app package's own `Config`, which routes each value to wherever the app wires it: stdio onto its pre-created agent, acp onto the bridge plugin).
-- **echo-agent folds onto `dsh-stdio-demo`**, swapping the LLM backend to the local `mock-llm` and adding the local `echo-tool` (plus `bash-local`, which the spine's `tool-bash` injects) at the leaf — the clean demonstration of "swap the backend, keep the app". `mock-llm.ts` / `echo-tool.ts` stay as example-local teaching plugins.
+- **`@deepseek-ai/dsh-tui-demo`**, **`@deepseek-ai/dsh-cli-demo`**, and **`@deepseek-ai/dsh-acp-demo`** bake in their process roles. TUI includes the full-screen UI and a pre-created `main`; Headless includes the one-shot driver and a pre-created `main`; ACP includes the bridge and no pre-created agent. All three include JSONL persistence and omit stdout loggers.
+- **`start.ts` is gone.** Each app package exposes a bin; the `demo:*` scripts invoke it. Loader boot, `.env` loading, and fail-loud guards live in the shared [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) package (unit-tested under the per-file coverage gate — see [share the app bins' boot glue](../simplification/2026-07-04-share-app-bin-boot-glue.md)); the thin self-executing entries are driven by keyless Loader-path tests.
+- **Each leaf `cordis.yml` collapses** to backends, optional product tools, and one app entry carrying the app config. TUI and Headless route model/session choices onto a pre-created agent; ACP routes the initial provider/model onto its bridge.
- **`base.yml`, `base-core.yml`, and `acp-agent/acp-tail.yml` are retired** — the spine they shared now lives in `dsh-agent-spine-demo`.
`bash-local` and the LLM adapter stay **leaf choices**: the bundle ships `tool-bash` (the consumer schema), the leaf picks the executor implementation, so a sandboxed executor or replay adapter swaps in without touching the app.
### Amendment on implementation: `hmr` stays a leaf entry
-The proposal listed `hmr` among the stdio app's baked-in front-door cluster. Validating against the code, baking `hmr` into the `dsh-stdio-demo` package fights cordis in two ways, so it ships as a **leaf `cordis.yml` entry** instead:
+The proposal listed `hmr` among the interactive app's baked-in front-door cluster. Validating against the code, baking `hmr` into the app package fights Cordis in two ways, so it ships as a **leaf `cordis.yml` entry** instead:
1. `@cordisjs/plugin-hmr` is a Loader-only, subprocess-only dev plugin — its constructor throws without `node --expose-internals` + a live `loader` service, so it can only run in the real `demo:*`/bin subprocess, never in the in-process unit/coverage tier.
2. The in-process test tier (vitest) cannot even *import* the vendored `hmr` module (its class-decorator `@Inject` form fails under Vite's transform), so a package whose `apply` statically imported it could never satisfy the per-file 100% coverage gate on its headline function.
-Crucially, `hmr` is **not** a stdout-purity footgun the way the console logger is — a stray `hmr` in the ACP config would not corrupt the JSON-RPC frames — so leaving it at the leaf costs none of the safety the coupling argument is about. The **logger** (the real coupling) stays baked in: the stdio app includes it, the ACP app omits it.
+Crucially, `hmr` is not a stdout-purity footgun: a stray entry in the ACP config does not corrupt JSON-RPC frames. Every shipped app omits a stdout console logger; the app or protocol driver alone owns stdout.
## Alternatives considered
@@ -39,13 +38,13 @@ The old `base*.yml`/`acp-tail.yml` includes already deduped the *config*, but a
## Verification
- Example directories contain only their config, README, and tests: `start.ts`, the infrastructure preamble, and the shared YAML includes are gone.
-- `demo:echo`, `demo:repl`, and `demo:acp` invoke the app-package bins.
-- Each new package has a README and per-file 100% coverage; each app package also has a keyless real-Loader-path bin smoke that catches export-shape failures described in [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md).
+- `demo:tui`, `demo:headless`, and `demo:acp` invoke the app-package bins.
+- Each new package has a README and per-file 100% coverage; each app package also has a keyless real-Loader-path bin smoke that catches export-shape failures described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md).
- The ACP replay transcript remains unchanged because the plugin set and load order did not change.
## Consequences
-- **The bare-plugin-tree pedagogy.** echo-agent's inlined `cordis.yml` showed every plugin at once; the spine now lives behind a bundle, so seeing the whole tree means opening `dsh-agent-spine-demo`. The app package's README carries that teaching weight.
+- **The bare-plugin-tree pedagogy.** The spine lives behind a bundle, so seeing the whole tree means opening `dsh-agent-spine-demo`. The app package's README carries that teaching weight.
- **A layer of indirection.** "What does this demo load?" becomes a package read, not a single YAML scan.
## Related
@@ -53,3 +52,4 @@ The old `base*.yml`/`acp-tail.yml` includes already deduped the *config*, but a
- Supersedes [Make the shared example base providerless](../../rejected/architecture/2026-06-20-providerless-example-base.md): renaming `base.yml` to the providerless core is moot once the spine moves into `dsh-agent-spine-demo` and the `base*.yml` files are deleted.
- Builds on the [capability-seams](2026-06-13-capability-seams.md) interface/implementation/consumer split — backends and presentation stay leaf choices; the spine is the shared bundle.
- Complements [Reorganize packages into a modular hierarchy](2026-06-20-package-hierarchy.md): the new app/core packages slot into existing groups under that hierarchy (`core` for the reusable spine bundle, `ui` for the app-specific front doors).
+- The later [redundant-agent removal](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) owns the final TUI/Headless split and removes the line-oriented and mock-only leaves.
diff --git a/docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
similarity index 95%
rename from docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
rename to .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
index c99eb9f0fe..4db0d78910 100644
--- a/docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
@@ -1,4 +1,4 @@
-# RFC: The background task runtime (`ctx.tasks`) and generic task control tools
+# Agent Note: The background task runtime (`ctx.tasks`) and generic task control tools
Status: implemented
@@ -21,7 +21,7 @@ Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into in
## Runtime contract
-The literal types live in the [task data-structure catalog](../../../core-data-structures/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id.
+The literal types live in the [task data-structure catalog](../../../../docs/core-data-structures/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id.
The producer hooks define three responsibilities:
@@ -123,6 +123,6 @@ Unit coverage pins preflight atomicity, per-kind ids, stream and final reads, wa
## Consequences
-Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../cookbook/adding-a-tool.md) points producers to this contract.
+Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../../docs/cookbook/adding-a-tool.md) points producers to this contract.
Owned background bash now stops with its agent instead of surviving it. Background processes have no executor timeout; callers must kill irrelevant work or rely on owner/service disposal. Stream reads support one consuming reader, completion notices do not wake idle agents, and a producer that returns from `cancel` without settling `done` can still stall teardown. Durable jobs, independent observation cursors, and foreground promotion remain separate designs.
diff --git a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md b/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
similarity index 93%
rename from docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md
rename to .agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
index 89b50eb3df..85c62b7e17 100644
--- a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md
+++ b/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
@@ -1,7 +1,9 @@
-# RFC: Reorganize packages into a modular hierarchy
+# Agent Note: Reorganize packages into a modular hierarchy
Status: implemented
+The later [fold-stdio-helper](../simplification/2026-07-04-fold-stdio-ui-helper.md) decision superseded the original `support/ui-stdio` placement, and the [redundant-agent removal](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) subsequently removed that surface entirely. The uniform depth-two hierarchy remains the decision owned here.
+
## Problem
`packages/` was flat: 18 packages all sat at `packages//`, so a package's location said nothing about whether it was core product API, a swappable capability seam, a provider adapter, a product integration, or example/test support. The package README carried a `FIXME(package-hierarchy)` and `scripts/publint-all.ts` a `TODO(package-inventory)` flagging exactly this. Core packages, provider integrations, capability seams, example UI support, and snapshot-only replay support all looked equally foundational.
diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
new file mode 100644
index 0000000000..28de5eb97c
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
@@ -0,0 +1,146 @@
+# Agent Note: Bounded recovery for transient LLM request failures
+
+Status: implemented
+
+## Problem
+
+`dsh-llm` can report provider failures either by throwing during adapter dispatch or iteration or by ending with `finish { kind: 'error' | 'aborted' }`. The final adapter boundary tags thrown failures so `dsh-agent-loop` can distinguish them from middleware and result-processing defects, and the loop normalizes both delivery forms into `agent/request-error` after closing the failed step. The default decision is `fail`; `dsh-compact-basic` is the only shipped recovery listener, and it retries a canonical context-window overflow only after compaction proves that the durable surface shrank.
+
+That boundary is already safe for another request attempt. Raw `assistant/chunk` events carry the failed `turn` and `step`, message derivation ignores them unless a successful `assistant/message` cites them, tool calls are dispatched only after a successful terminal finish and assembly, and a retry opens a new numbered step from the durable log. The harness therefore does not need a second response lifecycle or tentative-output protocol to keep two attempts separate.
+
+The prior boundary left three narrower gaps.
+
+- Provider failures retain only a message and usually a code. HTTP status, retry delay, and provider request id are discarded or recoverable only through provider-specific error objects, so generic recovery cannot make or explain a decision without parsing text.
+- Retry ownership differs by adapter. The hand-written DeepSeek adapter makes one attempt, while pi-ai profiles can enable opaque library retries. Combining hidden transport retries with an `agent/request-error` listener would multiply attempts and omit intermediate failures from the session log.
+- A recovered failure has no durable status fact. The failed step and chunks remain reconstructable, but an observer cannot tell whether the agent is deliberately backing off, for how long, or why. A long silent wait looks like a stalled loop.
+
+The goal is bounded recovery from transient failures of the same explicit provider/model request. Provider or model failover, response splicing, and semantic output repair are different problems and have no current consumer.
+
+## Decision
+
+### Preserve failure facts without embedding policy
+
+`@deepseek-ai/dsh-llm` exports one JSON-serializable `LlmFailure` payload:
+
+```ts ignore-check
+type ProviderRequestId = Branded<'ProviderRequestId'>
+
+interface LlmFailure {
+ message: string
+ code: string
+ status?: number
+ providerRetryAfterMs?: number
+ requestId?: ProviderRequestId
+}
+```
+
+`code` remains the provider-neutral machine-routing taxonomy established by `HarnessError`; the new fields are observations from the provider boundary. `ProviderRequestId` is owned and constructed by `dsh-llm`, then serializes as its provider-issued string. The payload deliberately has no `retryable`, `failover`, `partialOutput`, provider, model, phase, or route id fields. Retryability belongs to policy, provider/model are already in the durable request header, and partial output is derived from the failed step's `assistant/chunk` events.
+
+`LlmError` carries `failure: LlmFailure` and preserves `failure.code === error.code`. `FinishReasonMap.error` and `FinishReasonMap.aborted` carry the same payload instead of parallel failure shapes. An adapter-thrown `Error` keeps its exact object identity: the final-adapter scope associates the normalized facts with that object in call-local sidecar state and rethrows it unchanged; a non-`Error` throw is wrapped as today. `llmFailureOf(stream, error)` retrieves those facts alongside the existing provenance check, while an in-band finish without an error object becomes a new `LlmError`. This preserves listeners that key on error type or identity while giving all final-adapter failures, including unknown SDK exceptions, an `UNKNOWN` terminal payload.
+
+The agent loop keeps `RequestError` as that exact error object and passes `LlmFailure` as a separate argument to `agent/request-error`; it does not mutate possibly frozen third-party errors. It also uses the payload when converting an in-band finish and when recording an unrecovered `turn/end.reason`.
+
+Adapters extract structured facts before falling back to message inspection. They validate HTTP status, parse `Retry-After` seconds or dates into a positive finite millisecond delay, brand the provider request id when exposed, and distinguish their own timeout from the caller's abort. Provider-specific codes and messages may refine a mapping, but no recovery listener parses them.
+
+The initial shared transient-code set is intentionally small: the adapters' existing `RATE_LIMIT` and `SERVER` mappings plus explicit `TIMEOUT` and `TRANSPORT` codes for the two missing remote-failure families. Authentication, quota, invalid request, context overflow, protocol, abort, and unknown failures keep distinct stable codes and are not transient by default. Adding a code requires adapter fixtures and a documented policy decision; it does not require expanding a second failure-class enum.
+
+### Put retry policy on the existing failed-step seam
+
+`@deepseek-ai/dsh-llm-retry` is a function plugin that listens to `agent/request-error`. It introduces no service or new loop branch; the agent-loop package changes only the data carried through its existing failed-step recovery control flow.
+
+The `agent/request-error` seam carries the current `LlmFailure` and an immutable list of prior failures that led to another request attempt in this consecutive recovery sequence. `dsh-llm-retry` counts only prior failures whose codes are in its configured transient set, while `dsh-compact-basic` counts only prior context-overflow failures. A successful model request clears the history. Alternating transient and context-overflow failures therefore consume their owning policy budgets independently; the maximum request count is one plus the sum of the finite budgets of the loaded recovery policies.
+
+The plugin resolves and validates this deployment configuration at load:
+
+```ts ignore-check
+interface Config {
+ maxTransientRetries?: number
+ initialDelayMs?: number
+ maxDelayMs?: number
+ jitterRatio?: number
+ retryableCodes?: string[]
+}
+```
+
+The defaults are two transient retries, a 500 millisecond initial delay, a 10 second delay cap, 10 percent jitter, and the four transient codes above. The count and delay bounds match the conservative edge of the inspected implementations: [OpenCode uses two request retries with 500 ms/10 s bounds](https://github.com/anomalyco/opencode/blob/9976269ab1accfc9f9dc98a4a688c516934de422/%70ackages/llm/src/route/executor.ts#L36-L39), [Pi separates three agent-level retries from provider retries and defaults provider retries to zero](https://github.com/earendil-works/pi/blob/3da591ab74ab9ab407e72ed882600b2c851fae21/%70ackages/coding-agent/docs/settings.md#L139-L147), and [Codex uses finite request/stream budgets plus a five-minute idle timeout](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/model-provider-info/src/lib.rs#L25-L33). Ten percent follows [Codex's bounded jitter](https://github.com/openai/codex/blob/0fb559f0f6e231a88ac02ea002d3ecd248e2b515/codex-rs/codex-client/src/retry.rs#L40-L47). Two retries mean at most three provider requests when no other recovery policy applies. `maxTransientRetries` is a non-negative integer, delays are positive finite numbers with `initialDelayMs <= maxDelayMs`, `jitterRatio` is in `[0, 1]`, and codes are non-empty and unique. These are Cordis config fields rather than hidden constants so deployments can choose different cost and latency budgets.
+
+For an eligible failure with budget remaining, the one-based transient retry count uses bounded exponential backoff. A valid `providerRetryAfterMs` replaces exponential backoff only when it does not exceed `maxDelayMs`; a longer provider delay causes delegation instead of an earlier retry that violates the provider instruction. Local backoff multiplies by an injected random factor in `[1 - jitterRatio, 1 + jitterRatio]` and clamps the final value to `maxDelayMs`; provider delay is not jittered.
+
+The plugin owns a lifetime `AbortController` and tracks every active backoff callback. Each wait fuses the waterfall's turn signal with that lifetime signal. Effect cleanup first unregisters the listener, then aborts and awaits the active callbacks; a captured callback whose lifetime signal aborts returns `fail` and can neither retry nor enter the rest of its captured waterfall after disposal. This makes HMR disposal quiescent even though Cordis has already captured the listener.
+
+Before sleeping, `dsh-llm-retry` appends one non-surface `llm/retry` session event containing the turn, failed step, one-based transient retry number, configured maximum, scheduled delay, and `LlmFailure`. The plugin owns the `SessionEventMap` augmentation; `dsh-session` remains generic persistence and does not absorb the optional policy's vocabulary. The event says what was scheduled, not that the next request completed; cancellation during the delay is subsequently visible on `turn/end`. The event ships only with a production renderer and replay/snapshot coverage, because its purpose is operational state rather than trace collection.
+
+The listener calls `next()` for a non-transient code, an exhausted policy budget, or an over-cap provider delay. This preserves composition with context-overflow recovery and later policy plugins. It returns `{ action: 'retry' }` only after the delay completes under both signals; turn cancellation and plugin disposal return `fail`, after which the loop's cancellation/disposal checks remain authoritative.
+
+The agent-spine demo bundle loads the plugin so the shared stdio/TUI, one-shot CLI, and ACP example compositions use the same bounded policy. Library consumers retain explicit plugin composition: omitting the plugin leaves `agent/request-error` at its current fail default.
+
+### Make one layer own visible attempts
+
+Adapters perform one provider request per `stream()` call. The pi-ai adapter removes public `maxRetries` and `maxRetryDelayMs` profile fields and disables library retries; the hand-written adapter keeps its current single-attempt behavior. This prevents an SDK budget from multiplying the agent budget and ensures every transient retry is represented by a closed failed step plus `llm/retry`.
+
+`ctx.llm.stream()` remains the raw one-attempt waterfall. Direct callers such as compaction summarization receive the structured failure but do not gain automatic retry, because they have no agent step boundary or general durable place to separate attempts. A future direct-call consumer may justify a buffering helper that retries only before emitting a chunk; this decision adds no such helper.
+
+### Bound stalled streams where they can be stopped
+
+Each adapter exposes a validated `streamIdleTimeoutMs` configuration field with the five-minute prior-art default cited above. The interval is capped at Node's maximum timer delay so it cannot be clamped to one millisecond. It covers each outstanding iterator `next()` from demand to the next valid `StreamChunk`; time a consumer spends between `next()` calls is not provider idle time.
+
+`@deepseek-ai/dsh-timeout` exposes a rearmable idle-watchdog primitive. One stable local `AbortController` is fused with the caller signal and passed to the transport for the whole adapter call; each outstanding `next()` arms the watchdog, resolution disarms it, and the next demand rearms it. Timeout aborts that stable controller with a capability-owned `TimeoutReason`, and `finally` clears the timer. The adapter classifies its watchdog as `TIMEOUT` and an earlier upstream abort as `ABORTED`. The existing one-shot `deadline()` is not presented as a sliding timer.
+
+Boundary tests prove termination at both actual transports. The hand-written adapter aborts its fetch/reader, and the pi-ai adapter maps the stable signal through the SDK and proves the SDK closes the response. A timer that merely rejects a consumer promise while leaving the request running does not satisfy the contract.
+
+### Keep attempts separate in the existing log
+
+A failed attempt may leave `assistant/chunk` events in its closed step, but it never appends `assistant/message` and never dispatches a tool. A retry opens the next numbered step, reconstructs the request from the durable surface, and produces its own chunks. UIs may render live chunks while a step is open, then mark or clear that transient view when `llm/retry` identifies the failed step or `turn/end` records terminal failure; message derivation continues to ignore the failed chunks.
+
+If recovery is exhausted, the final failure is stored once on `turn/end.reason` with the structured facts. If transient recovery continues, `llm/retry` is the durable home for that attempt's failure and delay. No standalone final-error event or response-id vocabulary is added.
+
+## Out of scope
+
+- Automatic provider or model failover. Requests already select one explicit provider and model, and the provider registry deliberately has one adapter owner per provider.
+- Retrying or continuing after a successful terminal finish, or splicing chunks from two attempts into one assistant message.
+- Repairing malformed tool arguments, refusals, content filters, or other semantic model output.
+- Unbounded retries, unattended retry-until-cancelled behavior, circuit breakers, shared provider health, or cross-agent retry budgets.
+- Changing `llm/stream` into a response lifecycle or adding convenience generation APIs without a production consumer.
+
+## Alternatives considered
+
+- **Retry inside `llm/stream` or the provider SDK** — rejected because a raw stream has no durable attempt boundary after emitting chunks, hidden SDK retries multiply budgets, and neither path can record each failed attempt consistently.
+- **Add response start, interrupted, discarded, failed, and committed events to `dsh-llm`** — rejected because the agent log already separates raw chunks, successful messages, and numbered attempts. A second state machine would duplicate ownership without enabling the bounded same-route retry.
+- **Add logical routes, capability matrices, and failover selection** — rejected because current requests already name provider and model explicitly, one adapter owns each provider, and no current consumer requires automatic fallback or can prove semantic compatibility.
+- **Put `retryable` or `failover` on `LlmFailure`** — rejected because adapters report facts while deployment policy decides action. The same 429 may be retried in an interactive bundle and rejected in a cost-capped batch.
+- **Retry forever while the caller remains active** — rejected because it gives one request unbounded cost and latency. Visible status makes bounded waiting understandable; it does not make an unlimited budget safe.
+- **Log retry status only through the process logger** — rejected because process logs do not reconstruct session behavior and cannot drive replayed UI state.
+- **Keep only flat codes** — rejected because retry delay and provider request id are structured provider facts, and HTTP status is necessary for diagnosis when different wire failures share one stable code.
+
+## Verification
+
+- `LlmFailure` is the single serializable payload for thrown, error-finish, and aborted-finish final-adapter failures; normalization preserves stable code, status, retry delay, branded provider request id, error cause, and caller-abort versus adapter-timeout classification where available.
+- An adapter-thrown `Error` reaches `agent/request-error` as the exact same object while its sidecar `LlmFailure` reaches the adjacent argument; tests retain the existing identity assertion for extensible and frozen third-party errors.
+- DeepSeek and pi-ai adapter tests cover representative 400, 401/403, 429, 5xx, connection, malformed/truncated stream, timeout, abort, retry-after seconds/date, request-id, and unknown-SDK-error paths without recovery policy parsing message text.
+- Pi-ai pins the SDK option to zero retries and performs one observed wire attempt for a retryable provider response; separate tests make removing either boundary fail.
+- `agent/request-error` carries current failure facts plus immutable prior-retried failure facts; a success clears that history, and alternating transient/context-overflow integration tests prove the two policies consume only their own finite budgets.
+- `dsh-llm-retry` validates every config field at Loader startup, delegates all ineligible paths with `next()`, and makes at most `maxTransientRetries + 1` provider requests when no other policy applies.
+- HMR-during-backoff tests prove disposal unregisters the listener, aborts and awaits its captured callbacks, emits no retry decision after disposal, and leaves no timer or promise alive.
+- Pure unit tests cover transient-code selection, exponential backoff and jitter bounds, valid and over-cap `Retry-After`, exhausted budgets, deterministic timer/random seams, and abort during backoff.
+- Real agent-loop tests cover failure before chunks, partial chunks then failure, thrown and in-band failures, retry to success in a new step, exhaustion to structured `turn/end.reason`, and composition with `dsh-compact-basic` context-overflow recovery.
+- The partial-chunk integration test proves failed chunks remain attributed to the failed step, no assistant message or tool side effect is committed for that step, and the successful retry has distinct provenance.
+- The plugin-owned `llm/retry` event is non-surface, survives JSONL and SQLite round trips, is ignored by message derivation, and drives TUI retraction plus durable discarded-attempt markers in append-only ACP and stdio streams. Keyless snapshots cover scheduling, cancellation, success, and exhaustion.
+- Idle-watchdog tests prove the stable signal is rearmed only while `next()` is outstanding, disarmed during consumer think time and in `finally`, and classified separately from a total-call deadline and an earlier caller abort; adapter tests prove the signal stops the underlying request rather than merely detaching it.
+- Direct `ctx.llm.stream()` callers remain single-attempt and receive the same structured failure facts.
+
+## Consequences
+
+- Every transient recovery attempt is visible as a closed step plus `llm/retry`, and the bounded policy prevents hidden SDK retries from multiplying cost. A retry can still duplicate provider billing even when no chunk arrived; the finite attempt budget limits but cannot remove that risk.
+- Provider SDKs may hide status or retry headers. Those adapters retain the stable facts they expose and otherwise use a coarse code rather than letting recovery policy parse fragile text.
+- Durable retry events expand the session protocol and UI state machine. Shipping the event and its consumer together prevents an unused telemetry vocabulary, but later schema changes still require persistence and replay work.
+- Clearing a failed step's live chunks can visibly retract output. That is preferable to presenting discarded text or partial tool JSON as committed history, and snapshots pin the transition.
+- Adapter-local idle enforcement stops stalled transports without counting consumer think time. Contract tests at each transport boundary guard against SDK drift.
+- Multiple recovery plugins add their finite budgets. Their classifiers remain disjoint here; an overlapping classifier would be registration-order policy and must be documented and tested by the plugins that introduce it.
+
+## Related
+
+- [Structured error taxonomy](../../implemented/architecture/2026-06-11-structured-error-taxonomy.md) owns stable machine-routable codes and cause chaining.
+- [Reconstructable requests](../../implemented/architecture/2026-07-05-reconstructable-requests.md) makes provider/model and complete request inputs durable before dispatch.
+- [Timeout deadline library](../../implemented/architecture/2026-07-06-timeout-deadline-library.md) separates shared deadline classification from capability-owned termination.
+- [After-call compaction pressure and context-overflow recovery](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md) owns the current closed-step request-recovery seam and bounded overflow retry.
+- [Provider-routed LLM adapters](../../implemented/architecture/2026-07-14-provider-routed-llm-adapters.md) owns explicit provider/model routing and the one-adapter-per-provider invariant.
diff --git a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
similarity index 81%
rename from docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
rename to .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
index 102a29d613..fdd98b89cb 100644
--- a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
+++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
@@ -1,10 +1,10 @@
-# RFC: Mandatory `User-Agent` attribution for provider requests
+# Agent Note: Mandatory `User-Agent` attribution for provider requests
Status: implemented
## Problem
-LLM provider requests should identify the product making them. That is useful for provider-side support, abuse investigation, compatibility debugging, and traffic analytics. Before this RFC the harness only partially did this: the hand-rolled DeepSeek adapter sent a hand-copied `User-Agent` constant (`packages/llm/llm-deepseek/src/adapter.ts`), while the pi-ai-backed twin sent no harness-owned headers at all (`packages/llm/llm-pi-ai/src/adapter.ts`). New adapters could therefore omit attribution silently, and a library-backed adapter could drift from the hand-rolled adapter even though [the twin-adapter RFC](2026-06-13-twin-llm-adapters.md) exists to keep the provider seam honest across both implementations.
+LLM provider requests should identify the product making them. That is useful for provider-side support, abuse investigation, compatibility debugging, and traffic analytics. Before this Agent Note the harness only partially did this: the hand-rolled DeepSeek adapter sent a hand-copied `User-Agent` constant (`packages/llm/llm-deepseek/src/adapter.ts`), while the pi-ai-backed twin sent no harness-owned headers at all (`packages/llm/llm-pi-ai/src/adapter.ts`). New adapters could therefore omit attribution silently, and a library-backed adapter could drift from the hand-rolled adapter even though [the twin-adapter Agent Note](2026-06-13-twin-llm-adapters.md) exists to keep the provider seam honest across both implementations.
The immediate prompt came from OpenRouter's [App Attribution](https://openrouter.ai/docs/app-attribution) docs. OpenRouter creates app pages and rankings from `HTTP-Referer` plus display/category headers. That is valuable, but it is not the HTTP standard for application identity. The risk is adopting OpenRouter's exact header set as if it were universal, then leaking provider-specific headers to direct DeepSeek requests, future OpenAI/Anthropic/Vertex adapters, test servers, or proxies that log unknown fields indefinitely.
@@ -24,11 +24,11 @@ The immediate prompt came from OpenRouter's [App Attribution](https://openrouter
Provider request attribution is mandatory at the LLM adapter boundary, using the standard `User-Agent` header only. The rule: every product LLM adapter sends a static, non-secret application identity on every provider HTTP request, and every adapter has tests proving that `User-Agent` reaches the wire (a mock server asserting received headers; for a library-backed adapter, the library's header hook feeding the same mock-server assertion).
-Do **not** implement OpenRouter app attribution in this RFC. `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, and `X-OpenRouter-Categories` are OpenRouter-specific product-surface headers, not provider-neutral model-request attribution. They can be proposed later by an OpenRouter adapter or explicit OpenRouter mode, with its own privacy/product decision, tests, and docs. Until then, even requests pointed at OpenRouter send only the shared `User-Agent` attribution from this RFC.
+Do **not** implement OpenRouter app attribution in this Agent Note. `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, and `X-OpenRouter-Categories` are OpenRouter-specific product-surface headers, not provider-neutral model-request attribution. They can be proposed later by an OpenRouter adapter or explicit OpenRouter mode, with its own privacy/product decision, tests, and docs. Until then, even requests pointed at OpenRouter send only the shared `User-Agent` attribution from this Agent Note.
The provider-neutral identity is owned by `dsh-llm` (`packages/llm/llm/src/attribution.ts`), not by individual adapters. `AppIdentity` contains only public product facts needed to build `User-Agent`, and the default `APP_IDENTITY` settles the values the proposal left open:
-- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-RFC wire value and the repo/org identity)
+- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-Agent Note wire value and the repo/org identity)
- version: read from the owning package's manifest via `createRequire`, never a hand-copied constant
- app URL: `https://github.com/deepseek-ai/deepseek-harness-sdk` - the planned public home; a `FIXME` in `attribution.ts` blocks release until that repository actually exists
@@ -40,10 +40,10 @@ Wire mapping (`attributionHeaders`; header names lowercase in code - HTTP field
|---|---|
| All HTTP-based adapters | `User-Agent: {product}/{version} (+{url})` - the parenthesized `+url` comment stays within RFC 9110's conservative product/comment syntax. |
| Direct DeepSeek endpoint | `User-Agent`; do not send OpenRouter-only headers unless DeepSeek documents an equivalent contract. |
-| OpenRouter endpoints | `User-Agent` only for now. Do not send `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, or `X-OpenRouter-Categories` under this RFC. |
-| Future providers | `User-Agent` only unless a later provider-specific RFC accepts additional headers. Do not reuse `HTTP-Referer` by analogy. |
+| OpenRouter endpoints | `User-Agent` only for now. Do not send `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, or `X-OpenRouter-Categories` under this Agent Note. |
+| Future providers | `User-Agent` only unless a later provider-specific Agent Note accepts additional headers. Do not reuse `HTTP-Referer` by analogy. |
-Endpoint detection is not part of this RFC because no endpoint-specific mapping is accepted here. If OpenRouter support lands later, detection must be explicit: either a dedicated OpenRouter provider package or an explicit `provider: 'openrouter'` / `attributionTarget: 'openrouter'` config, not arbitrary path fragments or model names.
+Endpoint detection is not part of this Agent Note because no endpoint-specific mapping is accepted here. If OpenRouter support lands later, detection must be explicit: either a dedicated OpenRouter provider package or an explicit `provider: 'openrouter'` / `attributionTarget: 'openrouter'` config, not arbitrary path fragments or model names.
## Verification
@@ -53,19 +53,19 @@ The landed contract:
- A shared helper (`attributionHeaders` / `userAgent`) constructs the app identity and the standard `User-Agent` value from package metadata, so adapters do not hand-copy version constants.
- `dsh-llm-deepseek` sends the shared `User-Agent` on every request and its mock-server suite asserts the exact value.
- `dsh-llm-pi-ai` sends the same `User-Agent` through pi-ai's `StreamOptions.headers` hook and its mock-server suite asserts the exact value.
-- No adapter sends OpenRouter-specific attribution headers (`HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, `X-OpenRouter-Categories`) as part of this RFC.
+- No adapter sends OpenRouter-specific attribution headers (`HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, `X-OpenRouter-Categories`) as part of this Agent Note.
- No app-attribution field carries secrets, local paths, session ids, prompt text, model output, user email, or per-user stable identifiers.
- The adapter READMEs state the `User-Agent` attribution policy and explicitly avoid documenting OpenRouter app attribution as implemented behavior.
## Alternatives considered
-**OpenRouter app attribution now.** Rejected for this RFC. Sending `HTTP-Referer` plus `X-OpenRouter-Title` would satisfy OpenRouter rankings, but those headers are a provider-specific product feature, not the provider-neutral model-request attribution this RFC is trying to standardize. Supporting them should be an explicit OpenRouter adapter/mode decision later, not hidden inside the first shared attribution helper.
+**OpenRouter app attribution now.** Rejected for this Agent Note. Sending `HTTP-Referer` plus `X-OpenRouter-Title` would satisfy OpenRouter rankings, but those headers are a provider-specific product feature, not the provider-neutral model-request attribution this Agent Note is trying to standardize. Supporting them should be an explicit OpenRouter adapter/mode decision later, not hidden inside the first shared attribution helper.
**OpenRouter headers everywhere.** Rejected. It would treat a custom OpenRouter contract as a universal standard and send fields with misleading semantics to providers that did not ask for them. It also risks using `HTTP-Referer` as a generic app URL field even though standard HTTP already has `User-Agent` for product identity and `Referer` for a different browsing-context concept.
**Only provider account/project identity.** Rejected. Organization/project headers, API keys, cloud accounts, and billing projects identify who pays or owns the request, not which application is sending traffic. They also expose no public app title/category and do not help gateways like OpenRouter build app rankings.
-**End-user `user`/`metadata` fields.** Rejected for this RFC. Those are valuable for abuse monitoring and customer support but describe the human or tenant behind a request. App attribution must be static product identity and safe to send on every request.
+**End-user `user`/`metadata` fields.** Rejected for this Agent Note. Those are valuable for abuse monitoring and customer support but describe the human or tenant behind a request. App attribution must be static product identity and safe to send on every request.
**Config-only opt-in attribution.** Rejected. A default-off setting is exactly how adapters keep drifting. The policy is mandatory default attribution with overrideable public values, not optional attribution.
diff --git a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
similarity index 97%
rename from docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md
rename to .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
index 2909ccd0e6..b3dad98f18 100644
--- a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md
+++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
@@ -1,4 +1,4 @@
-# RFC: Web capability seam - stable tools over multiple providers
+# Agent Note: Web capability seam - stable tools over multiple providers
Status: implemented
@@ -14,7 +14,7 @@ There is also a provider-selection question. Existing `tool-bash` and `tool-fs`
## Decision
-Web access is a first-class capability seam following [the capability-seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md):
+Web access is a first-class capability seam following [the capability-seam Agent Note](2026-06-13-capability-seams.md):
1. `@deepseek-ai/dsh-web` (`packages/web/web`) owns `ctx.web`, provider registration, provider selection, shared request/result vocabulary, and web-specific errors.
2. Provider packages implement concrete backends and register capabilities with `ctx.web`, for example `@deepseek-ai/dsh-web-search-exa`, `@deepseek-ai/dsh-web-search-perplexity`, `@deepseek-ai/dsh-web-search-deepseek`, and `@deepseek-ai/dsh-web-fetch-local`.
@@ -32,7 +32,7 @@ Search and fetch are separate tools but one web-access seam. `ctx.web` owns prov
This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
-The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface RFC](../simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes.
+The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes.
## Package topology
@@ -276,7 +276,7 @@ Tool execution lets these errors flow through `ToolRegistry.execute()`, which al
## Testing
-Each layer is pinned at its own seam: the registry/selection/truncation/abort contract and the `WebError` codes in `dsh-web`; per-provider request/response mapping over recorded fixtures (Perplexity fixtures include URL-only citations so the optional source fields stay honest) plus a self-skipping with-key smoke per real provider; real local-HTTP behavior in `web-fetch-local`; and enablement-driven registration, structured execution errors, and result formatting through the real tool registry in `dsh-tool-web`. A real-Loader smoke guards the two export shapes ([postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)): `dsh-web` is a default-exported service, while the providers and `tool-web` are namespace plugins where a stray `export default` would drop `inject`.
+Each layer is pinned at its own seam: the registry/selection/truncation/abort contract and the `WebError` codes in `dsh-web`; per-provider request/response mapping over recorded fixtures (Perplexity fixtures include URL-only citations so the optional source fields stay honest) plus a self-skipping with-key smoke per real provider; real local-HTTP behavior in `web-fetch-local`; and enablement-driven registration, structured execution errors, and result formatting through the real tool registry in `dsh-tool-web`. A real-Loader smoke guards the two export shapes ([postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)): `dsh-web` is a default-exported service, while the providers and `tool-web` are namespace plugins where a stray `export default` would drop `inject`.
## Alternatives considered
diff --git a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md
similarity index 92%
rename from docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md
rename to .agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md
index 486d9aafb4..3f34cf3559 100644
--- a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md
+++ b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md
@@ -1,10 +1,10 @@
-# RFC: Make `dsh-fs-policy` an event-gate plugin, not a method interface
+# Agent Note: Make `dsh-fs-policy` an event-gate plugin, not a method interface
Status: implemented
## Problem
-[The split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) put `ctx.fileContext` between the model-facing tools and the `ctx.fs` provider: `dsh-tool-fs` injects `fileContext` and routes every `read`/`write`/`edit` through its methods. That makes `fileContext` **in-path and mandatory**. The tool cannot reach `ctx.fs` without it, the policy layer owns the fs I/O and the read windowing, and a deployment that does not want observed-state policy cannot simply drop the package — `dsh-tool-fs` would fail to resolve `ctx.fileContext`.
+[The split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) put `ctx.fileContext` between the model-facing tools and the `ctx.fs` provider: `dsh-tool-fs` injects `fileContext` and routes every `read`/`write`/`edit` through its methods. That makes `fileContext` **in-path and mandatory**. The tool cannot reach `ctx.fs` without it, the policy layer owns the fs I/O and the read windowing, and a deployment that does not want observed-state policy cannot simply drop the package — `dsh-tool-fs` would fail to resolve `ctx.fileContext`.
This couples three things that should be separable:
@@ -148,7 +148,7 @@ Both mutations are still atomic (the backend's per-target lock is unconditional)
## Supersedes
-This amends — does not reverse — [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md). The four-layer split, the provider contract, and the freshness *policy* are all kept. What changes is the **coupling between the tool and the policy layer**: a mandatory method service became a plugin-owned event gate, and the fs I/O + read windowing moved from `fileContext` up into `dsh-tool-fs`. The split-fs-seam RFC's description of `dsh-tool-fs` injecting `fileContext` and of `fileContext` owning `read`/`write`/`edit` was updated to match in the same change.
+This amends — does not reverse — [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md). The four-layer split, the provider contract, and the freshness *policy* are all kept. What changes is the **coupling between the tool and the policy layer**: a mandatory method service became a plugin-owned event gate, and the fs I/O + read windowing moved from `fileContext` up into `dsh-tool-fs`. The split-fs-seam Agent Note's description of `dsh-tool-fs` injecting `fileContext` and of `fileContext` owning `read`/`write`/`edit` was updated to match in the same change.
## Verification
@@ -156,7 +156,7 @@ Tests pin both paths: without `dsh-fs-policy`, the root tool plugin boots agains
## Alternatives considered
-- **Keep `ctx.fileContext` as an in-path method service** — the shape [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) first landed; rejected because the tool could not run without the policy layer, making policy load-bearing for basic operation instead of an opt-in tightening.
+- **Keep `ctx.fileContext` as an in-path method service** — the shape [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) first landed; rejected because the tool could not run without the policy layer, making policy load-bearing for basic operation instead of an opt-in tightening.
- **Policy-side version checking** (`dsh-fs-policy` stats and compares in its waterfall handler) — rejected for the TOCTOU gap between that check and the tool's actual write; the provider's mutation critical section is the only race-free place, so the policy only chooses the CAS basis and gates on prior observation.
- **Per-tool `/read`/`/write`/`/edit` subpath plugins** — dropped on implementation: no consumer needed a single-tool deployment, and subpath publishing forced bespoke `tsdown`/`tsconfig`/`files`/workspace-constraint handling no sibling tool package carries; the per-tool registration helpers remain internal modules the root plugin composes.
@@ -164,6 +164,6 @@ Tests pin both paths: without `dsh-fs-policy`, the root tool plugin boots agains
- **Event indirection over a method call.** A waterfall + emit is less direct than `await ctx.fileContext.edit(...)`. The payoff is removing the tool-to-policy method dependency while keeping the default policy plugin; the cost is one more event vocabulary to learn. Mitigated by keeping the three events narrow and documenting the default-thunk semantics on each.
- **Policy events in the storage seam.** `dsh-fs` gains two version-decision events plus a recording event though it is "just storage". This is the price of decoupling (the emitter cannot depend on the policy plugin). The events carry only `dsh-fs` vocabulary plus an opaque `object` actor and no model-facing concepts, so the seam stays free of line-window/observation policy types and of the agent/session owner structure.
-- **Single policy occupant, first-wins by convention.** The `fs/write-intent`/`fs/edit-intent` slots hold exactly one decider; the first-registered (or `prepend`ed) listener wins and the rest are short-circuited. `dsh-fs-policy` owning the slot is a deployment convention, not an event-enforced invariant — a second decider registered first would bypass it. This is acceptable because a second fs-version-policy decider is a misconfiguration, not a feature. If a future need for *layered* fs version policy appears, it is a new RFC (a composable value-passing seam), not a silent second listener on these events. Layered permission/audit/sandbox interception already has its home on `tools/execute`.
+- **Single policy occupant, first-wins by convention.** The `fs/write-intent`/`fs/edit-intent` slots hold exactly one decider; the first-registered (or `prepend`ed) listener wins and the rest are short-circuited. `dsh-fs-policy` owning the slot is a deployment convention, not an event-enforced invariant — a second decider registered first would bypass it. This is acceptable because a second fs-version-policy decider is a misconfiguration, not a feature. If a future need for *layered* fs version policy appears, it is a new Agent Note (a composable value-passing seam), not a silent second listener on these events. Layered permission/audit/sandbox interception already has its home on `tools/execute`.
- **Dropping the post-read confirming stat** makes a follow-up *guarded* edit occasionally fail-closed (`FS_STALE_VERSION` → re-read) under a read/write race. This is a UX nicety lost, never a correctness hole; the provider lock still prevents wrong-version writes.
- **The bare provider does no read-before-write/edit and no version check.** A deployment without `dsh-fs-policy` lets the model overwrite or edit any existing file unconditionally. This is the deliberate meaning of keeping the tool independent of a policy service: the safety disciplines live in the `dsh-fs-policy` plugin. A deployment that omits it is opting into an unconstrained filesystem on purpose; that is not the intended stance for a config that ships the fs tools.
diff --git a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
similarity index 94%
rename from docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
rename to .agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
index bc91d425b6..cc1de3c53e 100644
--- a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
+++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md
@@ -1,12 +1,12 @@
-# RFC: stdin + extra env on the bash seam
+# Agent Note: stdin + extra env on the bash seam
Status: implemented
## Problem
-The hooks subsystem runs external hook commands the way Claude Code and Codex do: a hook is a shell command that receives its event payload as **JSON on stdin** and reads context from a handful of **environment variables** (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, `PLUGIN_ROOT`, …). The harness already has a perfectly good command runner behind the `ctx.bash` capability seam ([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)), with process-group kills, output truncation/spill, and a credential scrub. Reusing it for hook execution means a hook bridge does not re-implement subprocess plumbing — but the seam had no way to write stdin or set extra env. This RFC adds those two inputs.
+The hooks subsystem runs external hook commands the way Claude Code and Codex do: a hook is a shell command that receives its event payload as **JSON on stdin** and reads context from a handful of **environment variables** (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, `PLUGIN_ROOT`, …). The harness already has a perfectly good command runner behind the `ctx.bash` capability seam ([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)), with process-group kills, output truncation/spill, and a credential scrub. Reusing it for hook execution means a hook bridge does not re-implement subprocess plumbing — but the seam had no way to write stdin or set extra env. This Agent Note adds those two inputs.
-`stdin` and `env` do not create a new model capability because ordinary shell syntax already supplies both. Ambient credentials are protected by `dsh-bash-local`'s child-environment scrub, not by hiding these seam fields; model tool arguments are static JSON and do not expand shell variables. The fields therefore serve trusted in-process callers, such as hook bridges, that need to pass structured input and `CLAUDE_*` variables without embedding them in model-visible shell text. See [defensive-patterns.md](../../../defensive-patterns.md) for the ambient-environment rule.
+`stdin` and `env` do not create a new model capability because ordinary shell syntax already supplies both. Ambient credentials are protected by `dsh-bash-local`'s child-environment scrub, not by hiding these seam fields; model tool arguments are static JSON and do not expand shell variables. The fields therefore serve trusted in-process callers, such as hook bridges, that need to pass structured input and `CLAUDE_*` variables without embedding them in model-visible shell text. See [defensive-patterns.md](../../../../docs/defensive-patterns.md) for the ambient-environment rule.
## Decision
@@ -28,4 +28,4 @@ Three deliberate choices:
## Consequences
-Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../core-data-structures/bash.md).
+Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../../docs/core-data-structures/bash.md).
diff --git a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
similarity index 74%
rename from docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md
rename to .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
index 4cf055179c..56c3fdf231 100644
--- a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md
+++ b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
@@ -1,10 +1,10 @@
-# RFC: Event-domain semantics — session is the fact log, agent is the live surface
+# Agent Note: Event-domain semantics — session is the fact log, agent is the live surface
Status: implemented
## Problem
-The harness extends the agent loop through a Cordis event taxonomy (see [the microkernel event-taxonomy RFC](2026-06-11-microkernel-event-taxonomy.md)). As that taxonomy grew, the line between the three event domains blurred:
+The harness extends the agent loop through a Cordis event taxonomy (see [the microkernel event-taxonomy Agent Note](2026-06-11-microkernel-event-taxonomy.md)). As that taxonomy grew, the line between the three event domains blurred:
- `session/*` carries the durable, event-sourced log (`SessionEventMap`).
- `agent/*` carries live runtime signals that hand a plugin the `Agent` handle.
@@ -24,14 +24,14 @@ This vocabulary is the foundation for interception decisions, the durable `hook/
**The boundary rule:** a durable, replayable fact is a `SessionEvent`; a live interception or a transient/live-object signal is an `agent`/`tools` Cordis event. A turn or step boundary is a durable fact, so it lives in the session log and is read off the `session/event` feed — it is NOT mirrored as an `agent/*` emit.
-**Applying the rule to the boundary twins:** all four boundary mirrors — `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end` — are **REMOVED**. No production consumer needs the live `Agent` at a boundary: the ACP bridge settles from `session/event` `turn/end` plus `agent/status`, and the only turn-mirror consumer (`dsh-ui-stdio`, a disposable test REPL) was migrated to render boundaries from `session/event`, recovering the short agent label from an `agent/created`→id map. The step mirrors were removed first (they had no consumer at all); the turn mirrors followed once ui-stdio was migrated — see [the remove-boundary-mirror-events RFC](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md), which owns that decision. Removing the emits also simplifies the loop's `closeStep`/`closeTurn` (one append each, no paired emit).
+**Applying the rule to the boundary twins:** all four boundary mirrors — `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end` — are **REMOVED**. No production consumer needs the live `Agent` at a boundary: the ACP bridge settles from `session/event` `turn/end` plus `agent/status`, and the only turn-mirror consumer (`dsh-ui-stdio`, a disposable test REPL) renders boundaries from `session/event` while retaining its live target object for the fixed `main` label. The step mirrors were removed first (they had no consumer at all); the turn mirrors followed once ui-stdio was migrated — see [the remove-boundary-mirror-events Agent Note](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md), which owns that decision. Removing the emits also simplifies the loop's `closeStep`/`closeTurn` (one append each, no paired emit).
## Consequences
- The loop no longer emits any boundary mirror; `closeStep` appends `step/end` only and `closeTurn` appends `turn/end` only. `Session.append` owns post-commit observer containment, so a throwing boundary observer cannot change the turn outcome or starve later consumers; an acceptance or internal validation failure still escapes before the boundary enters the log.
- Tests that observed boundaries via the removed emits now observe the durable `turn/start`/`turn/end`/`step/start`/`step/end` session events — the behavior they pin (boundary ordering, step counting) is unchanged; only the feed they read moved to the canonical one. The tests that exercised a *throwing turn-boundary emit listener* were deleted, because that code path no longer exists (there is no emit to throw from). Per [AGENTS.md "tests document behavior, not golden truth"](../../../../AGENTS.md), the behavior and its test moved (or died) together.
- The loop marks the step open (`stepOpen = true`) only after `append('step/start')` returns. Internal dispatch validation runs before the log push and may reject without opening a step; post-commit `session/event` observer failures are contained inside `Session.append`. The marker therefore represents exactly the committed boundary that owes a later `step/end`.
-- The full realization of this is [the simplification RFC "Stop mirroring durable boundaries as agent events"](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md): all four boundary mirrors are removed and every consumer reads boundaries off `session/event`. `agent/steering` (not a boundary mirror) stayed outside that RFC's scope and was removed by its own follow-up, [Remove the `agent/steering` mirror emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) — it mirrored the durable `steering/message`.
+- The full realization of this is [the simplification Agent Note "Stop mirroring durable boundaries as agent events"](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md): all four boundary mirrors are removed and every consumer reads boundaries off `session/event`. `agent/steering` (not a boundary mirror) stayed outside that Agent Note's scope and was removed by its own follow-up, [Remove the `agent/steering` mirror emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) — it mirrored the durable `steering/message`.
- The cordis events catalog (`docs/cordis-catalog/events.md`) is regenerated to drop the mirror events.
-
+
diff --git a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md b/.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md
similarity index 92%
rename from docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md
rename to .agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md
index f60607df4d..ece39654ea 100644
--- a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md
+++ b/.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md
@@ -1,10 +1,10 @@
-# RFC: Resolve filesystem paths against the caller's session cwd
+# Agent Note: Resolve filesystem paths against the caller's session cwd
Status: implemented
## Problem
-The ACP bridge gives every session its own workspace: `session/new` records the editor's project directory as `SessionHeader.cwd`, and `dsh-tool-bash` defaults each bash call's `workdir` to the calling agent's `session.header.cwd` (see [the per-session cwd RFC work in `packages/ui/acp`](../../../../packages/ui/acp) and `resolveWorkdir` in `dsh-tool-bash`). So a bash command in session A runs in A's project, and in session B runs in B's — one server process, N workspaces.
+The ACP bridge gives every session its own workspace: `session/new` records the editor's project directory as `SessionHeader.cwd`, and `dsh-tool-bash` defaults each bash call's `workdir` to the calling agent's `session.header.cwd` (see [the per-session cwd Agent Note work in `packages/ui/acp`](../../../../packages/ui/acp) and `resolveWorkdir` in `dsh-tool-bash`). So a bash command in session A runs in A's project, and in session B runs in B's — one server process, N workspaces.
Filesystem resolution used one plugin-load cwd while bash used the session project directory. Relative paths therefore disagreed whenever the editor project differed from the server launch directory; snapshots hid the bug by making those paths identical.
diff --git a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md b/.agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
similarity index 97%
rename from docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
rename to .agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
index 218cb29297..8ddd1e8941 100644
--- a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
+++ b/.agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
@@ -1,4 +1,4 @@
-# RFC: Result-time applied-hunk diffs for file mutations
+# Agent Note: Result-time applied-hunk diffs for file mutations
Status: implemented
@@ -54,6 +54,6 @@ Per the [capability-seam split](2026-06-13-capability-seams.md), the storage bac
## Related
-- Completes the one remaining representation difference named as a non-goal in [Tagged render-intent union](2026-07-02-tool-render-intent-union.md) — that RFC's Non-goals section is updated to record that applied-hunk diffs shipped here.
+- Completes the one remaining representation difference named as a non-goal in [Tagged render-intent union](2026-07-02-tool-render-intent-union.md) — that Agent Note's Non-goals section is updated to record that applied-hunk diffs shipped here.
- Builds on the [filesystem capability seam](2026-06-17-filesystem-capability-seam.md) (the before/after are storage facts the backend returns) and [event-sourced sessions](2026-06-11-event-sourced-sessions.md) (the `meta` payload persists on the `tool/result` event, so replay reproduces the card).
- The `meta` channel is deliberately generic: a future tool (a structured search, a data-table result) can attach its own durable result presentation without another core change.
diff --git a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
similarity index 92%
rename from docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md
rename to .agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
index 37507c0c55..9d7fac0471 100644
--- a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md
+++ b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
@@ -1,4 +1,4 @@
-# RFC: Tagged render-intent union for tool-call presentation
+# Agent Note: Tagged render-intent union for tool-call presentation
Status: implemented
@@ -10,7 +10,7 @@ A tool declares how its calls render in a UI (an editor's tool-call card) throug
- Which combinations are *valid* is unwritten: a `terminal` call that also sets `content` means "description above the card"; a generic call that sets `terminal` is meaningless but representable. The type permits nonsense.
- There is no way to express the one file-tool affordance an editor most wants — a **diff card** (`{path, oldText, newText}`, which Zed renders as an inline diff / new-file preview). `ToolCallPresentation.content` is the *LLM* `ContentBlock[]` vocabulary (text/image), so a tool literally cannot ask for a diff.
-The existing `FIXME(tool-presentation)` in `packages/core/tools/src/index.ts` named the fix: "redesign the type so a tool declares its render INTENT once (e.g. a tagged union over card kinds) rather than a bag of optional fields the bridge stitches together." The rejected RFC [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md) deferred it explicitly: rich rendering "should return later as a tagged render-intent union after there are at least two real tools and two real consumers to validate the vocabulary." That bar is now met — two producer families (`dsh-tool-bash`, `dsh-tool-fs`) and two consumers (the ACP bridge live path + the snapshot-golden replay path).
+The existing `FIXME(tool-presentation)` in `packages/core/tools/src/index.ts` named the fix: "redesign the type so a tool declares its render INTENT once (e.g. a tagged union over card kinds) rather than a bag of optional fields the bridge stitches together." The rejected Agent Note [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md) deferred it explicitly: rich rendering "should return later as a tagged render-intent union after there are at least two real tools and two real consumers to validate the vocabulary." That bar is now met — two producer families (`dsh-tool-bash`, `dsh-tool-fs`) and two consumers (the ACP bridge live path + the snapshot replay path).
## Decision
@@ -70,7 +70,7 @@ A new render intent is a compile-breaking change at the bridge switch — delibe
## Non-goals
-- **Live incremental `terminal_output_delta` streaming** and **command classification** — the terminal-rendering RFC's own deferred follow-ups, untouched here.
+- **Live incremental `terminal_output_delta` streaming** and **command classification** — the terminal-rendering Agent Note's own deferred follow-ups, untouched here.
## Related
diff --git a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md b/.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
similarity index 98%
rename from docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
rename to .agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
index 451cce45d3..d40c50695d 100644
--- a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
+++ b/.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md
@@ -1,4 +1,4 @@
-# RFC: Add direct directory listing to the filesystem seam
+# Agent Note: Add direct directory listing to the filesystem seam
Status: implemented
diff --git a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
similarity index 82%
rename from docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
rename to .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
index 854807c109..cdd37091a2 100644
--- a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
+++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
@@ -1,4 +1,4 @@
-# RFC: Prompt variables and tool-guidance ownership
+# Agent Note: Prompt variables and tool-guidance ownership
Status: implemented
@@ -8,9 +8,9 @@ The assembled system prompt had four defects, all of one family: facts the harne
**The model could not know its own name.** `AgentOptions.model` drives every request, but no prompt text carried it — and nothing COULD carry it: sections in `dsh-system-prompt` were context-global while the model name is per-agent, and `assemble()` took no per-agent input at all.
-**Tool guidance was hand-written prose in leaf YAML.** The bash/subagent/todo_write usage guidance lived in the `systemPrompt` strings of `examples/coding-agent/cordis.yml` and `examples/acp-agent/cordis.yml` — two drifting copies (the ACP one was already abridged) — while `dsh-tool-fs` and `dsh-tool-web` owned their guidance as `ctx.systemPrompt.section()` contributions. Loading or dropping a tool plugin meant editing every deployment's persona by hand; both YAMLs carried a `FIXME(config-comments)` apologizing for a symptom of the split, and the stdio welcome banner hand-enumerated the tool set too.
+**Tool guidance was hand-written prose in leaf YAML.** The bash/subagent/todo_write usage guidance lived in the coding-agent and ACP persona strings — two drifting copies (the ACP one was already abridged) — while `dsh-tool-fs` and `dsh-tool-web` owned their guidance as `ctx.systemPrompt.section()` contributions. Loading or dropping a tool plugin meant editing every deployment's persona by hand; both YAMLs carried a `FIXME(config-comments)` apologizing for a symptom of the split, and the old terminal welcome banner hand-enumerated the tool set too.
-**The persona rendered after tool guidance.** The loop string-joined `agent.options.systemPrompt` AFTER the assembled sections, so the model read "Use the read tool…" before "You are coding-agent" — backwards relative to the identity-first convention (Claude Code, Codex) and a second composition path besides the section pipeline.
+**The persona rendered after tool guidance.** The loop string-joined `agent.options.systemPrompt` AFTER the assembled sections, so the model read "Use the read tool…" before "You are a coding agent" — backwards relative to the identity-first convention (Claude Code, Codex) and a second composition path besides the section pipeline.
**The fork tool's description was false.** `dsh-tool-subagent` hardcoded one description written for spawn semantics — "a separate agent that works in its own context … it does not see this conversation" — and the `subagent_fork` instance (whose child inherits the parent's completed turns) got the same words; the YAML prose corrected the lie out-of-band. Minor kin: `PromptSection.name` was documented "(diagnostics / dedup)" but duplicates were silently accepted.
@@ -30,7 +30,7 @@ Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, prov
### Persona as the order-0 section
-`dsh-system-prompt` owns `harness:identity` at order `-100` and the configured `deployment:persona` at order 0, so both survive a replacement loop. Prompt rendering has one path, `renderPrompt(assembly)`, and `agent/pre-step` therefore measures the exact prompt used for compaction. An agent-scoped `deployment:persona` shadows the global default and lets subagent providers install a persona before publication. The conventional order bands are identity `-100`, persona `0`, and tool guidance `100–199`.
+`dsh-system-prompt` owns `harness:identity` at order `-100` and the configured `deployment:persona` at order 0, so both survive a replacement loop. Prompt rendering has one path, `renderPrompt(assembly)`, and the routed request header therefore records the exact prompt later replayed by `ctx.tokenMeter` for compaction pressure. An agent-scoped `deployment:persona` shadows the global default and lets subagent providers install a persona before publication. The conventional order bands are identity `-100`, persona `0`, and tool guidance `100–199`.
### Tool guidance ownership
@@ -38,16 +38,16 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect
### The subagent conversation-history descriptor
-`SubagentProvider.inheritsParentContext` describes conversation seeding, not scope, services, tools, or authority. Spawn and ACP set it to `false`; fork sets it to `true`. `dsh-tool-subagent` derives its tool and prompt-parameter descriptions from the flag, including that fork inherits completed turns but not the in-flight turn. Provider lifecycle events keep that wording synchronized with reactive provider registration; their rationale lives in the [provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md).
+`SubagentProvider.inheritsParentContext` describes conversation seeding, not scope, services, tools, or authority. Spawn and ACP set it to `false`; fork sets it to `true`. `dsh-tool-subagent` derives its tool and prompt-parameter descriptions from the flag, including that fork inherits completed turns but not the in-flight turn. Provider lifecycle events keep that wording synchronized with reactive provider registration; their rationale lives in the [provider-lifecycle-events Agent Note](2026-07-05-subagent-provider-lifecycle-events.md).
## Alternatives considered
- **The loop composes an identity line itself** — hardcodes model-facing prose in the one package that must stay thin ("plugins, not loop changes"), and outside the section pipeline it would be a second composition path. (The identity DOES ship as a code literal — but as an ordinary section registered by `dsh-system-prompt`, whose `system-prompt/assemble` waterfall remains the escape valve for a deployment that must drop it.)
-- **Inject the model name via the `agent/request` waterfall** — prompt text composed in two places, and `agent/pre-step`'s `fullSystemPrompt` would omit it, so compaction would measure a prompt that is not what the model sees.
-- **Hand-write the model name in each persona** — duplicates the `model:` key one line above and silently lies after a config edit; the exact disease this RFC cures.
+- **Inject the model name via the `agent/request` waterfall** — prompt text would be composed in two places and the earlier rendered persona could disagree with the final routed header. The request plugin that owns late routing must also own any earlier prompt claim about that model.
+- **Hand-write the model name in each persona** — duplicates the `model:` key one line above and silently lies after a config edit; the exact disease this Agent Note cures.
- **Lenient interpolation (leave unknown refs verbatim, or substitute empty)** — a typo ships `{{modle}}` (or a hole) to the model and nobody notices until transcript review.
- **Per-instance subagent wording in config** — returns model-facing prose to every deployment × instance, the P2 disease again. **Keying wording off the provider NAME** — `providerName` is itself config, so a renamed provider silently gets the wrong words.
-- **Resolving the provider at `apply` time (a load-order requirement)** and **section-only subagent wording (lazily resolved at assemble)** — the alternatives to the provider-lifecycle events; both rejected in [the provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md).
+- **Resolving the provider at `apply` time (a load-order requirement)** and **section-only subagent wording (lazily resolved at assemble)** — the alternatives to the provider-lifecycle events; both rejected in [the provider-lifecycle-events Agent Note](2026-07-05-subagent-provider-lifecycle-events.md).
## Out of scope
@@ -56,7 +56,7 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect
## Shipped invariants
-- The coding-agent prompt renders identity, persona with the interpolated model, then fs/bash/web guidance through one assembly path.
+- The tui-agent prompt renders identity, persona with the interpolated model, then fs/bash/web guidance through one assembly path.
- Fork and fresh subagent descriptions reflect whether the provider inherits completed conversation turns; the tool appears, disappears, and is reworded with provider lifecycle changes.
- Unknown, valueless, malformed, or unbalanced variable references name the section and throw; duplicate section, variable, and tool registrations also throw.
- Snapshot replay is prompt-independent: it keys recorded chunk streams by turn and step without comparing the outgoing request.
diff --git a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
similarity index 62%
rename from docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md
rename to .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
index 86b352c5b6..162a83ad0c 100644
--- a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md
+++ b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
@@ -1,4 +1,4 @@
-# RFC: Every LLM request is reconstructable from the session log
+# Agent Note: Every LLM request is reconstructable from the session log
Status: implemented
@@ -6,25 +6,25 @@ Status: implemented
The request pipeline did not guarantee prefix stability for provider caching, and the session log could not reconstruct what the model saw. It omitted model, system prompt, and tool schemas while allowing per-call request rewrites. Cache behavior and replay equivalence therefore depended on whichever plugins happened to be loaded.
-The reference shape for the happy path is MiniCode's `LLMClient`: a stateful conversation client, appended to — never rebuilt — as the conversation advances, resetting only when the system prompt, tool set, or compaction genuinely changes what the model must see. The design question this RFC answers is how to get that discipline without giving up event-sourcing.
+The reference shape for the happy path is MiniCode's `LLMClient`: a stateful conversation client, appended to — never rebuilt — as the conversation advances, resetting only when the system prompt, tool set, or compaction genuinely changes what the model must see. The design question this Agent Note answers is how to get that discipline without giving up event-sourcing.
## Decision
### The principle
-**Model-visible ⟺ logged.** Anything that reaches a model request must be recorded in the session log. The checkable consequence: **every conversation request the loop sends is a pure function of the session log** — anyone holding the log reconstructs it byte-for-byte. Scope, stated precisely: the guarantee covers the loop-built `GenerateOptions`; provider wire bytes follow from it because both adapters' serialization is a pure per-message function at a pinned code version; direct one-shots (compaction's summarize call) log their envelope scalars (`compact/summary.{model, maxTokens}`) and their input is deterministic code over the logged region — reconstructable from log + code, outside the invariant by the unfrozen-request marker.
+**Model-visible ⟺ logged.** Anything that reaches a model request must be recorded in the session log. The checkable consequence: **every conversation request the loop sends is a pure function of the session log** — anyone holding the log reconstructs it byte-for-byte. Scope, stated precisely: the guarantee covers the loop-built `GenerateOptions`; provider wire bytes follow from it because both adapters' serialization is a pure per-message function at a pinned code version; direct one-shots (compaction's summarize call) log their envelope scalars (`compact/summary.{provider, model, maxTokens}`) and their input is deterministic code over the logged region — reconstructable from log + code, outside the invariant by the unfrozen-request marker.
Prefix-cache stability is corollary #1, not the headline: an append-only log projected by a per-node pure function yields requests that are append-extensions of their predecessors whenever the header is unchanged — stability is emergent, not managed. Byte-exact audit/replay is corollary #2; resume and fork with *attributable* drift is corollary #3.
### The mechanism
-**Messages.** `Session.deriveMessages()` is cached: each surface node is projected exactly once, when first seen, through the public per-node function `deriveEventMessage(event)`; a surface rewrite (a compaction `replace` — `SurfaceManager.replaceGeneration`) rebuilds. Callers get a fresh array per call over shared, deep-frozen messages: mutating logged history through a projection is unrepresentable (it throws), replacing the old clone-per-call isolation. External reconstructors fold the same public function over a log prefix, so no two paths can disagree.
+**Messages.** `Session.deriveMessages()` is cached: each surface entry is projected exactly once, when first seen, through the public per-event function `deriveEventMessage(event)`; a surface rewrite (a compaction `replace` — `SurfaceManager.replaceGeneration`) rebuilds. Callers get a fresh array per call over shared, deep-frozen messages: mutating logged history through a projection is unrepresentable (it throws), replacing the old clone-per-call isolation. External reconstructors fold the same public function over a log prefix, so no two paths can disagree.
-`EpochHeader` records the request's non-history state: call config, rendered system prompt, tool schemas, and session prefix, with empty values canonicalized to absence. `request/header` writes a full initial, resume, or fallback snapshot. `request/header-delta` encodes system changes by common-prefix/suffix line trim, tools by name-keyed additions/removals/changes, and config or prefix by full replacement. `foldRequestHeader`, `diffHeader`, and `applyHeaderDelta` are the pure codec. Each loop instance writes a snapshot on its first request to anchor process boundaries. Deltas are only an optimization: the writer verifies round-trip equality and falls back to a full snapshot for unrepresentable changes such as pure tool reordering.
+`EpochHeader` records the request's non-history state: call config, rendered system prompt, tool schemas, and session prefix, with empty values canonicalized to absence. `request/header` always writes a full snapshot: the first loop instance uses reason `initial`, later instances use `resume`, and an in-instance change uses `change`. `foldRequestHeader` selects the latest snapshot. Legacy `request/header-delta` events and the removed `fallback` reason are rejected when appended or loaded.
-Each step rebuilds prompt assembly. On the instance's first step, `agent/session-prefix` extends a frozen empty seed with request-only opener messages; the result is frozen and cached for that loop instance. `agent/pre-step` then receives the composed prefix before messages are snapshotted immediately ahead of `step/start`. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header. `agent/request` may replace only that frozen config seed, while model-visible content enters through logged channels. The loop records the owed header event—the prefix's only durable home—builds `GenerateOptions` from prefix, snapshot, and header, and deep-freezes it while leaving `AbortSignal` live. Per-instance state is only the cached prefix and whether its anchoring snapshot has been written.
+Each step rebuilds prompt assembly. On the instance's first step, `agent/session-prefix` extends a frozen empty seed with request-only opener messages; the result is frozen and cached for that loop instance before the generic `agent/pre-step` checkpoint and boundary snapshot. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header. `agent/request` may replace only that frozen config seed, while model-visible content enters through logged channels. The loop records the owed header event—the prefix's only durable home—builds `GenerateOptions` from prefix, snapshot, and header, and deep-freezes it while leaving `AbortSignal` live. Per-instance state is only the cached prefix and whether its anchoring snapshot has been written.
-**`step/start` is the reconstruction boundary.** A step derives messages from events before that sequence. Injection after the snapshot joins the next request, and reentrant appends are rejected during event publication. `agent/pre-step` is the seam for content needed by the current request. Header reconstruction folds through the step's own `request/header*` event, or carries the prior fold when no new header is written.
+**`step/start` is the reconstruction boundary.** A step derives messages from events before that sequence. Injection after the snapshot joins the next request, and reentrant appends are rejected during event publication. `agent/pre-step(agent, turn, step, signal)` remains the generic seam for content needed by the current request. Header reconstruction selects the step's `request/header`, or carries the prior snapshot when no new header is written.
**Enforcement.** In development, `dsh-invariants` independently rebuilds each loop request through a fresh `Session`, so the live cache cannot vouch for itself, then compares messages and folded header fields at `llm/stream`. Loop requests are identified by their frozen shape and session id; direct one-shots are excluded. Correctness depends on sequence-bounded reconstruction rather than listener order. A with-key e2e requires positive cache-read tokens after the first request; per-step usage is the production signal, and a header change or compaction appears as a cache-read drop on the next step.
@@ -39,15 +39,16 @@ Like MiniCode, the conversation advances append-only and resets only when model-
- **Per-call request scalars** (a freely mutable config handed to each `agent/request` dispatch): a listener flips the model per call with zero accounting, silently abandoning the provider cache this design exists to protect. Config is per-conversation logged state; the waterfall proposes, the log records.
- **Detect-and-report** (compare consecutive requests, warn on divergence): catches violations after the fact; a violating request is still constructible and ships. Rejected for interface-level unrepresentability.
- **Event-driven assembly** (re-render only on change signals): a missed-signal bug class — a tool registered mid-session emits `tools/change`, not `system-prompt/change`, and a third-party provider may emit nothing. Per-step render + value compare is robust with zero signal discipline.
-- **Narrative fields on the header events** (a `reason`/`changed` list on deltas): derivable by diffing consecutive events — one home per fact; snapshots carry a reason because an anchor's cause is NOT derivable from the data.
+- **A custom header-delta codec** (system line edits, name-keyed tool edits, whole config/prefix replacements): reduced repeated bytes but duplicated the representation and its diff/apply/fallback machinery. Full snapshots retain one replay representation.
+- **Narrative changed-field lists on header snapshots**: derivable by comparing consecutive snapshots. The `reason` remains because an instance boundary is not derivable from the snapshot values.
## Consequences
- A request that is not explained by the log cannot be constructed by accident — not by the loop, not by a listener; mutating a built request throws; every header change is a durable, diffable log event.
- Choosing between the advisory channels is a change-frequency decision, and the design makes the stable one structural: an `agent/session-prefix` contribution is composed once per loop instance and reused verbatim, so it extends the cacheable prefix at zero marginal cost and CANNOT bust the provider cache mid-session; content that changes mid-session flows through the append-only history channels — `agent.inject()` and tool/prompt-submit `additionalContexts` — each a durable `context/message` paid once and prefix-cached thereafter, at the price of accumulating in history and the log. Route session-frozen openers to the prefix and change notices to the history channels; a per-step request-only tail slot was deliberately dropped (no consumer, and a durable append covers every current update pattern).
-- What still costs full price at the provider is inherent and logged: compaction (its `compact/*` events and replace node), a real prompt/tool change (`request/header-delta`), a config switch (ditto), a process boundary with drift (`'resume'` snapshot differing from its predecessor). The provider's own reasoning-content exclusion is managed server-side.
+- What still costs full price at the provider is inherent and logged: compaction (its `compact/*` events and replacement entry), a real prompt, tool, or config change (`request/header` with reason `change`), or a process boundary with drift (a differing `resume` snapshot). The provider's own reasoning-content exclusion is managed server-side.
- The `step/start`-listener behavior change (above) is the one observable semantics change for plugins; `agent/pre-step` is the current-request seam.
-- Tool-result trimming (planned) needs no new mechanism: a logged single-node surface replace (`start === end`) carrying a trimmed `tool/result` under the same `callId` — compaction-family, replay-correct, cache-bust batched by the same pressure logic.
-- Session logs grow one `request/header` snapshot per conversation (system + tool schemas: the dominant term), plus deltas on real changes — small next to `assistant/chunk` volume; `SESSION_FORMAT_VERSION` stays `0` (pre-release churn is absorbed, backends reject-not-migrate).
-- Snapshot goldens changed once (every transcript gains its header events); the fs-writing fixtures are stored in the normalized authored form with cwd-relative tool arguments, because replay only round-trips cwd-independent argument paths.
+- Tool-result trimming (planned) needs no new mechanism: a logged single-entry surface replace (`start === end`) carrying a trimmed `tool/result` under the same `callId` — compaction-family, replay-correct, cache-bust batched by the same pressure logic.
+- Session logs grow one `request/header` snapshot per loop instance plus snapshots on real changes. This is larger than a delta codec but small beside chunk-heavy logs and retains one replay representation. `SESSION_FORMAT_VERSION` stays `0`; legacy delta events are rejected rather than migrated.
+- Snapshot expected outputs changed once (every transcript gains its header events); the fs-writing fixtures are stored in the normalized authored form with cwd-relative tool arguments, because replay only round-trips cwd-independent argument paths.
- FIXME(call-config-shape): revisit `LlmCallConfig`'s exact field set — which fields are genuinely epoch-level for cache purposes (`model` certainly; the sampling scalars sit there out of caution), and where provider-specific extras (reasoning options, extra body params) belong when an adapter needs them.
diff --git a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md
similarity index 77%
rename from docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md
rename to .agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md
index 30674f8c8b..44733bb8c9 100644
--- a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md
+++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md
@@ -1,12 +1,12 @@
-# RFC: Subagent provider-lifecycle events — `subagent/provider-added` / `subagent/provider-removed`
+# Agent Note: Subagent provider-lifecycle events — `subagent/provider-added` / `subagent/provider-removed`
Status: implemented
## Problem
-[The prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) makes `dsh-tool-subagent` DERIVE its model-facing wording from its provider: `SubagentProvider.inheritsParentContext` (spawn/ACP `false`, fork `true`) drives both the tool description and the `prompt` parameter description (`providerWording`), so the fork tool stops lying about context inheritance. That fix created a cross-fiber data dependency: a tool's description is fixed at TOOL REGISTRATION (deliberately — the description is where tool-choice guidance lives), but the provider arrives on its own plugin fiber, on no particular schedule.
+[The prompt-variables Agent Note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) makes `dsh-tool-subagent` DERIVE its model-facing wording from its provider: `SubagentProvider.inheritsParentContext` (spawn/ACP `false`, fork `true`) drives both the tool description and the `prompt` parameter description, so the fork tool stops lying about context inheritance. That fix created a cross-fiber data dependency: a tool's description is fixed at TOOL REGISTRATION (deliberately — the description is where tool-choice guidance lives), but the provider arrives on its own plugin fiber, on no particular schedule.
-Resolving the provider at the tool plugin's `apply` time creates an implicit load-order requirement ("list the backend before the tool in cordis.yml"). That requirement fails because the Cordis Loader starts sibling entries concurrently and `Entry.init()` does not await activation: a delayed backend can leave the tool fiber failed even when listed first. The Loader offers no sibling-order guarantee — "async state is not synchronous state" ([defensive patterns](../../../defensive-patterns.md)).
+Resolving the provider at the tool plugin's `apply` time creates an implicit load-order requirement ("list the backend before the tool in cordis.yml"). That requirement fails because the Cordis Loader starts sibling entries concurrently and `Entry.init()` does not await activation: a delayed backend can leave the tool fiber failed even when listed first. The Loader offers no sibling-order guarantee — "async state is not synchronous state" ([defensive patterns](../../../../docs/defensive-patterns.md)).
## Decision
@@ -23,12 +23,12 @@ The events also complete the seam's vocabulary: `ctx.subagents` is a named regis
- **Resolve the provider at `apply` time and throw when absent** — rejected because "list backends first" would claim a Loader ordering guarantee that does not exist.
- **Retrying the lookup (poll until the provider appears)** — converges eventually but invents a private readiness protocol beside the one the framework already has (effect registration + disposal); it also cannot notice a provider LEAVING, so HMR would strand a tool whose wording describes a disposed backend.
-- **Section-only subagent wording, lazily resolved at assemble time** — tolerates any load order too, but moves tool-choice guidance out of the DESCRIPTION, contradicting the ownership rule the prompt-variables RFC establishes (per-tool semantics and when-to-use belong in the description). Reactive registration keeps the description authoritative AND order-free.
+- **Section-only subagent wording, lazily resolved at assemble time** — tolerates any load order too, but moves tool-choice guidance out of the DESCRIPTION, contradicting the ownership rule the prompt-variables Agent Note establishes (per-tool semantics and when-to-use belong in the description). Reactive registration keeps the description authoritative AND order-free.
- **Keying wording off the provider NAME instead of the provider object** — `providerName` is itself config, so a renamed provider silently gets the wrong words; deriving from the resolved provider's own `inheritsParentContext` cannot drift.
## Consequences
- Consumers deriving state from a named provider react to `subagent/provider-added`/`-removed` instead of reading the registry at `apply` time; `dsh-tool-subagent` is the reference implementation.
-- **Addition fails loud; removal is contained per listener.** An addition listener may unwind registration. Removal runs during disposal, so one throwing listener is logged without starving later mirrors or disrupting teardown. `start()` still resolves the provider by name for every run, preventing stale tools from calling a removed backend. See the [events catalog](../../../cordis-catalog/events.md) and [producer/consumer map](../../../event-producer-consumer.md).
+- **Addition fails loud; removal is contained per listener.** An addition listener may unwind registration. Removal runs during disposal, so one throwing listener is logged without starving later mirrors or disrupting teardown. `start()` still resolves the provider by name for every run, preventing stale tools from calling a removed backend. See the [events catalog](../../../../docs/cordis-catalog/events.md) and [producer/consumer map](../../../../docs/event-producer-consumer.md).
- **A window where the tool is absent.** Between backend disposal and re-registration (an HMR reload), the model sees no subagent tool. This is the honest state — the alternative is a tool that dispatches into nothing — and the tool registry's `tools/change` emit keeps prompt assembly current.
- **Two waiting fibers sharing a `toolName` is an invalid config caught late.** If two loads of `dsh-tool-subagent` name different providers but the same `toolName`, both wait, and whichever provider arrives first registers; the second registration throws only when ITS provider arrives. `TODO(subagent-dup-toolname)` in the plugin records this blast radius; the tool registry's duplicate-name rejection remains the backstop.
diff --git a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md
similarity index 80%
rename from docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md
rename to .agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md
index 7aa987c60f..1c407d777d 100644
--- a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md
+++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md
@@ -1,4 +1,4 @@
-# RFC: A shared timeout/deadline primitive, with hard-kill left to each capability
+# Agent Note: A shared timeout/deadline primitive, with hard-kill left to each capability
Status: implemented
@@ -18,7 +18,7 @@ Each new external-process or network tool re-derived the same four things — cl
### The library surface
-Three functions plus one reason type:
+Four functions, one watchdog interface, and one reason type:
```ts ignore-check
/** The internal reason attached to a timeout abort, so consumers can classify it after the fact. */
@@ -51,19 +51,34 @@ export function deadline(
code: string,
): { signal: AbortSignal; [Symbol.dispose](): void }
+/** A stable signal plus one-at-a-time, timer-guarded async-iterator demand. */
+export interface IdleWatchdog {
+ readonly signal: AbortSignal
+ next(iterator: AsyncIterator): Promise>
+ [Symbol.dispose](): void
+}
+
+/** Arm only while one iterator `next()` is outstanding, then rearm on later demand. */
+export function idleWatchdog(
+ upstream: AbortSignal | undefined,
+ timeoutMs: number,
+ code: string,
+): IdleWatchdog
+
/** Recover the TimeoutReason from an aborted signal (or error); `code` scopes the match to this deadline's timer. */
export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string): TimeoutReason | undefined
```
-`deadline` fuses an upstream signal with a timer through `AbortSignal.any`, adds a typed `TimeoutReason`, and exposes disposable timer cleanup. Non-positive timeouts are an internal no-timeout sentinel for backend-owned background work; external hints pass through `clampTimeout` and must be positive and finite. Without a timer or upstream signal, the function returns a never-aborting signal with the same disposal shape. Providers translate timeout reasons into seam-specific results. `timeoutOf(signal, code)` scopes classification so an outer nested deadline is treated as upstream cancellation rather than the inner capability's timeout.
+`deadline` fuses an upstream signal with a one-shot timer through `AbortSignal.any`, adds a typed `TimeoutReason`, and exposes disposable timer cleanup. Non-positive timeouts are an internal no-timeout sentinel for backend-owned background work; external hints pass through `clampTimeout` and must be positive and finite. Without a timer or upstream signal, the function returns a never-aborting signal with the same disposal shape. `idleWatchdog` instead requires a positive finite interval, keeps one stable fused signal for the entire stream, and arms its timer only while one iterator `next()` is outstanding; resolution disarms it, later demand rearms it, concurrent demand fails, and disposal clears the active arm. Providers translate timeout reasons into seam-specific results. `timeoutOf(signal, code)` scopes classification so an outer nested deadline is treated as upstream cancellation rather than the inner capability's timeout.
### The division of labor
| Concern | Owner |
|---|---|
| Validate request hint and clamp default/max | `dsh-timeout` (`clampTimeout`) — pure arithmetic plus the shared positive-finite request contract |
-| Arm timer, abort on deadline, carry reason, fuse with upstream cancel | `dsh-timeout` (`deadline`) |
-| Clear the timer | `dsh-timeout` (`[Symbol.dispose]`) |
+| Arm one-shot timer, abort on deadline, carry reason, fuse with upstream cancel | `dsh-timeout` (`deadline`) |
+| Arm and rearm only around outstanding iterator demand | `dsh-timeout` (`idleWatchdog`) |
+| Clear the timer | `dsh-timeout` (`[Symbol.dispose]` on either primitive) |
| Classify the first abort reason after abort | `dsh-timeout` (`timeoutOf`) |
| **Actually terminate the work** | the capability's implementation |
| The default/max *values* | the capability's config |
@@ -75,6 +90,7 @@ The signal only *notifies*; termination is always the listener's job, and the li
- **web_fetch** — the tool stays validate-and-forward; the provider's hand-rolled controller + `setTimeout` + manual listener + `finally` + `signal.reason` recovery is replaced by provider-owned `deadline`/`timeoutOf`. A pre-aborted upstream signal still throws `WEB_ABORTED` up front; otherwise `fetch` runs against the fused `d.signal`, and `translateAbortOrNetwork` classifies a thrown error by the signal (`timeoutOf` → `WEB_FETCH_TIMEOUT`, else aborted → `WEB_ABORTED`, else network → `WEB_PROVIDER_ERROR`). The public error-code contract is unchanged, and `TimeoutReason` never crosses the web seam as the public error.
- **bash** — `resolve()` clamps the request into an explicit spec. Foreground `run()` creates the deadline and passes its signal to process execution, whose existing abort listener performs the process-group kill. The executor classifies the first abort as timeout or cancellation. Background starts remain timeout-free and forward only upstream cancellation.
+- **LLM adapters** — `dsh-llm-deepseek` and `dsh-llm-pi-ai` wrap actual transport iteration with `idleWatchdog`. The five-minute configured interval covers only outstanding provider demand, not time the downstream consumer spends between chunks. The stable signal reaches `fetch` or the SDK for the whole call, so timeout closes the underlying request and maps to `TIMEOUT`, while an earlier caller abort maps to `ABORTED`.
## Consequences
@@ -82,6 +98,7 @@ The signal only *notifies*; termination is always the listener's job, and the li
- `SpawnSpec.timeoutMs` and `SpawnOutcome.timedOut`/`aborted` were removed rather than kept as always-zero/always-false vestiges: with `runBash` owning no timer and the executor owning classification, they were read nowhere. This is the one deviation from the literal proposal shape (which passed `timeoutMs: 0` into `runBash`); an always-0 field read by nothing is dead weight under the per-file coverage gate.
- web_fetch shed its bespoke controller/timer/listener/reason-recovery; the classifier now keys off the deadline signal (`timeoutOf` + `aborted`) rather than the thrown error's shape, which is robust across both the request-phase reject-with-reason and the read-phase bare-`AbortError`.
- `AbortSignal.any` and `using`/`Symbol.dispose` enter the repo for the first time here (Node ≥ 24 baseline, already met).
+- Model streams now share one rearmable timer contract without turning a sliding idle interval into a total-call deadline or charging consumer think time. The primitive still only notifies; adapter tests prove their transports observe its stable signal and terminate.
Out of scope, named to mark the boundary: `web_search` can gain an optional model-facing `timeout_ms` once its tool-schema/snapshot coverage is planned; future ripgrep-backed fs discovery tools can consume the same provider-owned deadline shape once they exist; a `tools/execute` waterfall middleware could arm a default deadline for every tool call by driving `exec.signal` — that would be a plugin that *consumes* this library and still only notifies, the hard kill remaining each capability's job.
diff --git a/docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md
similarity index 99%
rename from docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md
rename to .agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md
index f35cf56e84..f90e653a7c 100644
--- a/docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md
+++ b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md
@@ -1,4 +1,4 @@
-# RFC: Tool result retention library
+# Agent Note: Tool result retention library
Status: implemented
diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
similarity index 83%
rename from docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
rename to .agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
index 1e516a9aa0..040a6c2fdc 100644
--- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
+++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
@@ -1,10 +1,10 @@
-# RFC: Tool-call timeout policy as a plugin
+# Agent Note: Tool-call timeout policy as a plugin
Status: implemented
## Problem
-The [timeout/deadline RFC](2026-07-06-timeout-deadline-library.md) extracted the timing-and-classification primitive into `@deepseek-ai/dsh-timeout`, but timeout policy was still attached to individual capabilities and model-facing schemas. `bash` exposed `timeoutMs`; `web_fetch` exposed `timeout_ms`; `web_search` had no model-facing timeout even though providers already honor `exec.signal`; a future grep/glob tool would either import the timeout library directly or invent its own timeout policy. That is the wrong authoring shape for a plugin SDK: a tool author should normally forward `exec.signal` to the implementation it calls, and deployment policy should decide the budget.
+The [timeout/deadline Agent Note](2026-07-06-timeout-deadline-library.md) extracted the timing-and-classification primitive into `@deepseek-ai/dsh-timeout`, but timeout policy was still attached to individual capabilities and model-facing schemas. `bash` exposed `timeoutMs`; `web_fetch` exposed `timeout_ms`; `web_search` had no model-facing timeout even though providers already honor `exec.signal`; a future grep/glob tool would either import the timeout library directly or invent its own timeout policy. That is the wrong authoring shape for a plugin SDK: a tool author should normally forward `exec.signal` to the implementation it calls, and deployment policy should decide the budget.
At the same time, not every timeout in the repo is a model-facing tool-call budget. Hooks execute command hooks by calling `ctx.bash` directly, not through `ctx.tools.execute()`, and the `bash` model tool multiplexes foreground execution, background start, background polling, and hook reuse through the same backend. Moving every timeout into a tool plugin in one step would conflate those paths and risk breaking hook timeout semantics.
@@ -84,7 +84,7 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit
## Alternatives considered
-**Name the plugin `tool-timeout`.** The literal RFC name matched the `gen-tool-catalog` completeness guard's `packages/*/tool-*` glob, which requires every match to register a model-facing tool. This plugin registers none — it is a `tools/execute` wrapper — so a `tool-*` name would either fail `verify-tool-catalog` or force a misleading boot entry. The package is `@deepseek-ai/dsh-timeout-policy` in a new `packages/timeout/` group; the cordis.yml `id` can still be `timeout-policy`.
+**Name the plugin `tool-timeout`.** The literal Agent Note name matched the `gen-tool-catalog` completeness guard's `packages/*/tool-*` glob, which requires every match to register a model-facing tool. This plugin registers none — it is a `tools/execute` wrapper — so a `tool-*` name would either fail `verify-tool-catalog` or force a misleading boot entry. The package is `@deepseek-ai/dsh-timeout-policy` in a new `packages/timeout/` group; the cordis.yml `id` can still be `timeout-policy`.
**Keep per-tool timeout handling only.** This was the shape for `bash` and `web_fetch`, and it matches Claude Code and Codex for shell commands. It loses for web-style tools because every new timeout-capable tool must choose validation, cap semantics, docs, snapshots, and classification. The plugin centralizes policy and classification while leaving each tool's schema focused on business input.
@@ -98,7 +98,7 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit
**Use `tools/pre-execute` plus `tools/post-execute` instead of a new around seam.** A pre listener could arm a deadline and mutate `exec.signal`; a post listener could classify and replace. That loses because the deadline lifetime would cross two independent waterfalls: a call-id map, cleanup on every pre-deny/tool-throw/post-throw/dispose path, and ordering rules with every other listener. `tools/pre-execute` is also the allow/deny gate, not an execution wrapper. `tools/execute` gives the timeout one lexical scope: arm, delegate, classify, dispose.
-**Use `Promise.race` to enforce timeouts for non-cooperative tools.** Rejected for the same reason as the timeout-library RFC: it returns control to the caller while the underlying process, fetch, or provider operation may still be running. The plugin only sends a signal; termination remains the implementation's responsibility.
+**Use `Promise.race` to enforce timeouts for non-cooperative tools.** Rejected for the same reason as the timeout-library Agent Note: it returns control to the caller while the underlying process, fetch, or provider operation may still be running. The plugin only sends a signal; termination remains the implementation's responsibility.
## Consequences
@@ -106,4 +106,4 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit
- Multiple `tools/execute` listeners compose by ordinary Cordis waterfall order: a listener that calls `next()` wraps downstream listeners plus dispatch; one that returns without `next()` short-circuits them. A deployment combining timeout with a future retry/sandbox/metrics wrapper chooses semantics by registration order ("timeout covers the whole retry" vs "timeout covers each attempt").
- Opt-in by declaration is a deliberate misconfiguration risk: a tool can declare a `timeoutMs` without honoring `exec.signal`, and that tool will not stop on timeout. The plugin contract states that declaring a budget means cooperative; the web tools prove the pattern on tools that already forward the signal.
- During the transition `bash` and the migrated web tools use different timeout paths on purpose: `TOOL_TIMEOUT` is the model-facing tool-call budget, while `BASH_TIMEOUT` remains the bash backend timeout used by bash and hooks.
-- Deviation from the literal proposal, recorded per the implemented-RFC rule: the plugin package is `@deepseek-ai/dsh-timeout-policy` (not `tool-timeout`), signal replacement is in-place `exec.signal` mutation before `next()` (not `next({ ...exec, signal })`, which cordis ignores), and the per-tool budget is declared on the `ToolDefinition` (`timeoutMs`, set by the owning tool plugin from its config) rather than mapped by tool name in this plugin's config — so the enforcer is zero-config and a mistyped tool name is impossible. All three are described in `## Decision` above.
+- Deviation from the literal proposal, recorded per the implemented-Agent Note rule: the plugin package is `@deepseek-ai/dsh-timeout-policy` (not `tool-timeout`), signal replacement is in-place `exec.signal` mutation before `next()` (not `next({ ...exec, signal })`, which cordis ignores), and the per-tool budget is declared on the `ToolDefinition` (`timeoutMs`, set by the owning tool plugin from its config) rather than mapped by tool name in this plugin's config — so the enforcer is zero-config and a mistyped tool name is impossible. All three are described in `## Decision` above.
diff --git a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
similarity index 95%
rename from docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md
rename to .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
index dcff4d3220..68c9bd3b3e 100644
--- a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md
+++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
@@ -1,4 +1,4 @@
-# RFC: The agent is a registration scope
+# Agent Note: The agent is a registration scope
Status: implemented
@@ -14,7 +14,7 @@ The mechanism also needs a publication boundary. An agent must not become visibl
Every live agent owns one flat registration layer exposed as `agent.ctx`. Code registers through the context that owns a contribution; scope-aware services combine deployment-global registrations with exactly one matching agent layer; operations choose that layer from their real agent; and the layer exists for the agent's complete published lifetime.
-Cordis is the plugin framework underneath the SDK. A Cordis **context** is the object plugins use to access services and register effects whose cleanup follows that context. The [Cordis primer](../../../cordis-primer.md) explains the framework in more detail.
+Cordis is the plugin framework underneath the SDK. A Cordis **context** is the object plugins use to access services and register effects whose cleanup follows that context. The [Cordis primer](../../../../docs/cordis-primer.md) explains the framework in more detail.
For most contributors, the complete contract is four rules:
@@ -43,7 +43,7 @@ flowchart LR
The missing cross-edges are the isolation rule: Agent A's local registrations do not enter Agent B's view, and a parent's registrations do not enter a child merely because the parent owns the child's lifetime.
-The companion [runtime-design RFC](2026-07-12-agent-scope-runtime-design.md) explains the implementation and correctness reasoning. The [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the separate `persona`, `toolFilter`, and `maxDepth` feature.
+The companion [runtime-design Agent Note](2026-07-12-agent-scope-runtime-design.md) explains the implementation and correctness reasoning. The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the separate `persona`, `toolFilter`, and `maxDepth` feature.
### Registration origin chooses visibility and cleanup
@@ -60,8 +60,7 @@ The ordinary contributor pattern is to register the complete local world during
```js
const handle = await ctx.agents.create({
- agentId: AgentId('reviewer'),
- sessionId: SessionId('reviewer-session'),
+ sessionId: SessionId('reviewer'),
agentOptions: { model: 'model-name' },
setup(agentCtx) {
agentCtx.systemPrompt.section({
@@ -103,7 +102,7 @@ An event about Agent A normally reaches unscoped listeners and A-scoped listener
At the Cordis level, `Scoped` is an opaque routing receiver. It carries the filter used to choose listeners but is not the domain object. Event signatures therefore keep the real `Agent`, tool execution, approval request, or other subject as an explicit argument that listeners can inspect.
-A listener registered with `{ global: true }` deliberately bypasses contextual audience filtering while its cleanup still follows the registering context. Registry-membership notifications remain unfiltered because they describe shared registry state rather than one agent's operation. The generated [event catalog](../../../cordis-catalog/events.md) is the exhaustive event reference.
+A listener registered with `{ global: true }` deliberately bypasses contextual audience filtering while its cleanup still follows the registering context. Registry-membership notifications remain unfiltered because they describe shared registry state rather than one agent's operation. The generated [event catalog](../../../../docs/cordis-catalog/events.md) is the exhaustive event reference.
### Creation publishes last and disposal revokes last
diff --git a/docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
similarity index 96%
rename from docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md
rename to .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
index 3a60c5c223..a9197de179 100644
--- a/docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md
+++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
@@ -1,4 +1,4 @@
-# RFC: Tool output spill policy
+# Agent Note: Tool output spill policy
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
Tool outputs need bounded model-facing previews, but some oversized results are still useful later. A fetched page body or a verbose tool response should not consume the next model request in full, but the model should be able to inspect the complete formatted result later with existing file-reading tools.
-Before this change the behavior was uneven. `dsh-bash-local` already writes complete stdout/stderr streams to private temp spill files when its in-memory tail overflows, but ordinary text tool results were returned inline unless the tool hand-rolled its own cap. The [tool result retention library](./2026-07-06-tool-result-retention-library.md) owns preview mechanics, but it does not own storage or an execution-pipeline policy that applies those mechanics to final tool results.
+Before this change the behavior was uneven. `dsh-bash-local` already writes complete stdout/stderr streams to private temp spill files when its in-memory tail overflows, but ordinary text tool results were returned inline unless the tool hand-rolled its own cap. The [tool result retention library](2026-07-06-tool-result-retention-library.md) owns preview mechanics, but it does not own storage or an execution-pipeline policy that applies those mechanics to final tool results.
The shape matches the timeout policy design: a tool author normally returns the text result, and a policy plugin enforces the deployment's default context budget. Tool-specific early spill remains possible later for outputs that do not survive to the final `ToolExecutionResult`; the first cut proves the default final-result path.
@@ -162,7 +162,7 @@ Those cases can consume `ctx.spillStore` directly in later work. They are not pa
- `dsh-spill-local` unit tests cover `saveText`, `encodeSegment` sanitization (separators/tilde/whole-segment dots/empty), the session-hash directory, owner-only permissions, distinct paths per save, the configured/private root, and a storage-failure rejection.
- `dsh-spill-policy` unit tests drive real tools through `ctx.tools.execute`: disabled-mode no-op, oversized-text replacement, small/non-text passthrough, `read` skip, best-effort fallback (save failure / no backend / no owner), and downstream-composition (bounding a replaced result, preserving `additionalContexts`).
- `dsh-tool-web` integration drives `web_fetch` through `ctx.tools.execute` with the real `spill-local` backend + policy, proving the model-facing text changes only by the deliberate spill notice while the spill file holds the full formatted result.
-- The `coding-agent` example loads `spill-local` + `spill-policy`, so its keyless Loader smoke exercises the real load path (the namespace-plugin export shape + `inject`).
+- The `tui-agent` example loads `spill-local` + `spill-policy`, so its keyless Loader/PTY smoke exercises the real load path (the namespace-plugin export shape + `inject`).
## Consequences
diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml
new file mode 100644
index 0000000000..68b9d2d55b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml
@@ -0,0 +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-10-after-call-compaction-pressure-and-overflow-recovery.md: d470cceaff68229b3872d0ade93d5fabc2e10c3f
+2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: b7753fb226638b16b0f244b681cd2b9bcc9f25c2
diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md
new file mode 100644
index 0000000000..d470cceaff
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md
@@ -0,0 +1,61 @@
+# Agent Note: After-call compaction pressure and context-overflow recovery
+
+Status: implemented
+
+English | [中文](2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md)
+
+## Problem
+
+`agent/pre-step` runs before final request routing and before assistant output, tool results, buffered context, and steering exist. Even with the assembled prompt and session prefix, its pressure view is provisional because `agent/request` can still change routing or call configuration and tool schemas are not frozen with those inputs. Adding fields cannot make pre-call state describe a completed call and couples the generic seam to compaction.
+
+Successful calls are not the only pressure signal. A provider can reject a request for exceeding its context window before it returns usage, and some successful calls omit usage. The system therefore needs replayable post-call pressure plus a narrow failure-recovery path that preserves the provider error whenever compaction cannot prove useful progress.
+
+## Decision
+
+### Successful pressure moves to a durable post-step checkpoint
+
+`agent/pre-step` is narrowed to `(agent, turn, step, signal)`. It remains a generic serial checkpoint before `step/start`, but it carries no compaction-only prompt or prefix fields.
+
+The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A propagated listener failure is an ordinary turn failure; it never enters model-request recovery. Compact-basic contains its expected operational failures as described below.
+
+`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue from the latest durable surface: full history before any replacement, or the pruned surface if pruning already landed.
+
+### Request recovery is limited to the final model boundary
+
+`RequestError`, `RequestErrorDecision`, and the `agent/request-error` waterfall represent failures after the final adapter has been selected. Each returned stream handle owns a private failure set that preserves the original thrown error identity across dispatch, iterator construction, and iteration without leaking nested-call provenance into an outer call. Terminal in-band `error` or `aborted` finishes enter the same path. Prompt assembly, request middleware, request logging, result processing, tools, post-step listeners, and cleanup remain ordinary failures.
+
+The failed step closes before recovery runs. A retry opens the next numbered step and rebuilds the request from the durable log; consecutive recovery attempts reset only after a successful provider request. Both DeepSeek adapters normalize recognized provider context-limit failures to `CONTEXT_WINDOW_EXCEEDED`.
+
+If cancellation lands after assistant tool calls are durable but before all calls dispatch, the loop records a synthetic `tool/call` and aborted `tool/result` pair for every undispatched call before following the normal abort path. The surface therefore never retains orphaned durable tool calls merely because cancellation won the race.
+
+### CompactService exposes intent, not token accounting
+
+`CompactService.compactIfNeeded(agent, trigger, signal)` accepts `trigger: 'pressure' | 'context-overflow'`. The interface gains no estimation methods or token types; `ctx.tokenMeter` remains the reusable accounting owner.
+
+For `pressure`, compact-basic applies the service-wide threshold and retained-tail policy to one unified `ctx.tokenMeter.measure()` result. Below pressure it returns without pruning. Once pressure qualifies, optional `ctx.toolResultPrune` rewrites oversized current results and compact-basic remeasures through the same meter; safe pressure skips the model call, while remaining pressure selects and summarizes from the pruned surface. The same singleton meter owns range pricing, provenance, shadowed token counts, and non-shrinking-summary rejection. The common defaults remain threshold ratio `0.8`, retained history `floor(contextWindow × 0.16)`, summarization provider/model `''`, `maxTokens: 8192`, `compactionRetries: 1`, and `auto: true`.
+
+For canonical overflow, compact-basic bypasses scalar pressure and the normal retained-token budget. It prunes first, then chooses the maximal tool-balanced head range while leaving the newest indivisible unit and attempts one shrinking summary compaction under the same signal when a range exists. The automatic listener snapshots `session.surface.replaceGeneration` and returns `{ action: 'retry' }` whenever pruning or summarization increases it. This remains true when pruning lands before later summary work throws; cancellation still wins. A backend returning a result without replacement cannot authorize retry, while pruning-only progress can authorize a retry without a `CompactionResult`.
+
+`maxOverflowRetries` is optional and defaults to `1`; `0` disables overflow recovery without disabling pressure. `auto: false` registers neither automatic listener. Noncanonical errors, exhausted attempts, an already-aborted signal, a missing routed model, no safe range, no generation change, and recovery throws before any replacement all delegate to the next listener. With no later recovery, the loop reports the original provider error object and code. A recovery throw after generation advances authorizes retry from durable progress; cancellation or disposal remains authoritative even if recovery work completes concurrently.
+
+The default summarizer resolves explicit configuration, then the latest logged route, then agent options. Because direct `llm/stream` middleware may reroute that auxiliary call, `compact/summary.{provider, model}` records the final mutable `GenerateOptions` target observed after dispatch rather than the pre-waterfall candidate.
+
+## Testing
+
+Unit tests cover final-adapter failure provenance and identity, closed-step retry numbering and reset, cancellation and disposal, post-step ordering, routed-envelope pressure, pressure-gated pruning, pruning-only relief, pruned-input summarization, balanced overflow reduction, durable prune progress before later failure, generation proof, caps, delegation, and auxiliary-call routing. Real-loop tests cover thrown and in-band overflow through pruning or summary compaction to a reconstructed retry request.
+
+## Alternatives considered
+
+- **Keep provisional pre-step pressure and add more arguments** — rejected because later routing and request mutation remain outside any earlier snapshot, while generic lifecycle becomes coupled to one plugin.
+- **Retry the same numbered step** — rejected because recovery appends durable events after the failed boundary. A new step preserves balanced nesting and reconstructability.
+- **Retry whenever `compactIfNeeded` returns a result** — rejected because a custom backend can report success without changing model-visible state. `replaceGeneration` is the authoritative proof.
+- **Let compact-basic parse provider wording** — rejected because classification belongs at adapters and must cover both thrown and in-band delivery.
+- **Fall back to `AgentOptions.model` when no durable route exists** — rejected because automatic policy must describe a completed logged request. Headerless pressure and recovery delegate unchanged.
+
+## Consequences
+
+Post-step pressure describes the completed routed request, including durable tool results and request-only prefix fields. Optional model-free pruning removes predictable tool-output bulk before summary selection and can independently create retry-worthy progress. Canonical overflow supplies the backstop when no successful usage anchor exists. Recovery is bounded, cancellation-owned, and monotonic: it retries only after a visible surface generation change.
+
+The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder remains oversized. The optional pruner can repair an otherwise indivisible tool pair when removable text-bearing tool-result content is the bulk.
+
+This Agent Note supersedes only the pre-step automatic-trigger portion of the [compaction capability-seam Agent Note](../feature/2026-06-18-compaction-capability-seam.md). The service split, standalone token meter, balanced range contract, log-recorded lock, summary replacement, and sole `summarize()` subclass hook remain unchanged.
diff --git a/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md
new file mode 100644
index 0000000000..b7753fb226
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md
@@ -0,0 +1,61 @@
+# Agent Note:调用后压缩压力与上下文溢出恢复
+
+Status: implemented
+
+[English](2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md) | 中文
+
+## 问题
+
+`agent/pre-step` 运行在最终请求路由之前,也早于 assistant 输出、工具结果、缓冲上下文与 steering 的产生。即使它接收已装配提示词与会话前缀,压力视图仍是临时的,因为 `agent/request` 还可以改变路由或调用配置,工具 schema 也没有与这些输入一同冻结。增加字段无法让调用前状态描述已完成调用,还会把通用 seam 与压缩耦合。
+
+成功调用也不是唯一的压力信号。提供方可能在返回 usage 之前就因上下文窗口超限拒绝请求,一些成功调用也不提供 usage。因此,系统需要可回放的调用后压力,以及一条狭窄的失败恢复路径;当压缩无法证明取得有效进展时,必须保留原始提供方错误。
+
+## 决策
+
+### 成功压力移动到持久 post-step 检查点
+
+`agent/pre-step` 收窄为 `(agent, turn, step, signal)`。它仍是 `step/start` 之前的通用串行检查点,但不再携带压缩专用的提示词或前缀字段。
+
+循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。向外传播的监听器失败属于普通 turn 失败,绝不会进入模型请求恢复;compact-basic 会按下文所述在内部处理其预期的操作性失败。
+
+`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并从最新持久表层继续:任何替换发生前使用完整历史;若剪枝已经落盘,则使用已剪枝表层。
+
+### 请求恢复只覆盖最终模型边界
+
+`RequestError`、`RequestErrorDecision` 与 `agent/request-error` waterfall 表示最终适配器已经选定之后的失败。每个返回的流句柄都绑定一个私有失败集合;该集合在分发、异步迭代器构造与迭代过程中保留原始抛出错误的身份,同时防止把嵌套调用的错误来源误归到外层调用。终止性的带内 `error` 或 `aborted` finish 进入同一路径。提示词装配、请求中间件、请求日志、结果处理、工具、post-step 监听器与清理仍属于普通失败。
+
+恢复运行前,失败 step 已经关闭。重试会打开下一个编号 step,并从持久日志重建请求;连续恢复尝试计数只在提供方请求成功后重置。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。
+
+如果取消发生在 assistant 工具调用已经持久化之后、所有调用完成分发之前,循环会为每个尚未分发的调用记录一对合成的 `tool/call` 与 aborted `tool/result`,随后进入正常中止路径。因此,表层不会仅因取消赢得竞态而留下孤立的持久工具调用。
+
+### CompactService 暴露意图,而不拥有 token 核算
+
+`CompactService.compactIfNeeded(agent, trigger, signal)` 接收 `trigger: 'pressure' | 'context-overflow'`。接口不增加估算方法或 token 类型;`ctx.tokenMeter` 继续作为可复用的核算所有者。
+
+对于 `pressure`,compact-basic 把服务级阈值与保留尾部策略应用到一次统一的 `ctx.tokenMeter.measure()` 结果。低于压力时直接返回,不执行剪枝。压力达到条件后,可选的 `ctx.toolResultPrune` 会改写当前表层中过大的工具结果,compact-basic 再通过同一个 meter 重新计量;若压力恢复安全则跳过模型调用,否则从已剪枝表层选择范围并生成摘要。范围定价、来源、被遮蔽 token 数与非缩小摘要拒绝也由同一个单例 meter 完成。通用默认值保持为阈值比例 `0.8`、保留历史 `floor(contextWindow × 0.16)`、摘要提供方/模型 `''`、`maxTokens: 8192`、`compactionRetries: 1` 与 `auto: true`。
+
+对于规范化溢出,compact-basic 绕过标量压力与普通保留 token 预算。它先执行剪枝,再在保留最新不可分割单元的同时选择最大的工具配对平衡头部范围;存在范围时,才在同一 signal 下尝试一次缩小摘要压缩。自动监听器先记录 `session.surface.replaceGeneration`,剪枝或摘要让 generation 增加时就返回 `{ action: 'retry' }`。即使剪枝先落盘而后续摘要工作抛错,这条规则仍然成立;取消依然优先。后端若只返回结果但没有替换表层,不能授权重试;只有剪枝取得进展时,即使没有 `CompactionResult` 也可以授权重试。
+
+`maxOverflowRetries` 可选且默认为 `1`;`0` 只禁用溢出恢复,不会禁用压力检查。`auto: false` 不注册任何自动监听器。非规范化错误、尝试耗尽、已经中止的 signal、缺失路由模型、没有安全范围、generation 未变化,以及在任何替换之前恢复抛错,都会委托给下一个监听器。若没有后续恢复,循环报告原始提供方错误对象与代码。generation 增加后的恢复抛错会基于持久进展授权重试;即使恢复工作并发完成,取消或销毁仍具有最终优先级。
+
+默认摘要器依次解析显式配置、最近记录的路由与 agent options。因为直接 `llm/stream` 中间件可以重新路由该辅助调用,`compact/summary.{provider, model}` 记录分发后最终可变的 `GenerateOptions` 目标,而不是 waterfall 之前的候选值。
+
+## 测试
+
+单元测试覆盖最终适配器失败的来源与身份、已关闭 step 的重试编号与重置、取消与销毁、post-step 顺序、已路由信封压力、压力门控剪枝、剪枝独立解除压力、从已剪枝输入生成摘要、平衡溢出缩减、后续失败前已落盘的剪枝进展、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出,并验证剪枝或摘要压缩后的重试请求从替换表层重建。
+
+## 考虑过的替代方案
+
+- **保留临时 pre-step 压力并增加更多参数**——不予采纳,因为后续路由与请求变换仍在更早快照之外,同时通用生命周期会耦合到单个插件。
+- **重试相同编号的 step**——不予采纳,因为恢复会在失败边界之后追加持久事件。新 step 保持边界配对与可重建性。
+- **只要 `compactIfNeeded` 返回结果就重试**——不予采纳,因为自定义后端可能报告成功却没有改变模型可见状态。`replaceGeneration` 才是权威证明。
+- **让 compact-basic 解析提供方措辞**——不予采纳,因为分类属于适配器,而且必须同时覆盖抛出式与带内交付。
+- **没有持久路由时回退到 `AgentOptions.model`**——不予采纳,因为自动策略必须描述已完成且已记录的请求。没有请求头的压力检查与恢复会原样委托。
+
+## 后果
+
+Post-step 压力描述已完成的路由请求,包括持久工具结果与仅请求前缀字段。可选的无模型剪枝会在选择摘要前移除可预测的工具输出体积,也能独立产生足以重试的进展。当成功 usage 锚点不存在时,规范化溢出提供兜底路径。恢复有明确上限、以取消为准,并保持单调:只有模型可见的表层 generation 变化后才重试。
+
+代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分不可分割的非工具节点,或修复非可剪枝剩余部分仍然过大的工具单元。若可移除的文本工具结果是主要体积,可选剪枝器仍可修复原本不可分割的工具配对。
+
+本 Agent Note 只取代[压缩能力接缝 Agent Note](../feature/2026-06-18-compaction-capability-seam.md) 中的 pre-step 自动触发部分。服务拆分、独立 token meter、平衡范围契约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。
diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
similarity index 70%
rename from docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
rename to .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
index 0655e89ba5..6db1ef4ece 100644
--- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: b177af24e988c6a314db522b8de0d1c09e30464f
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 0b964e8a748e4adcc32c017957e5294a3f258365
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: 0d4686a5a233785ca4832ef068a118b484a872fe
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: dcc9213c6b3a088b8b8bce2a442c5232ed5b7d0b
diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
similarity index 98%
rename from docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
rename to .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
index b177af24e9..0d4686a5a2 100644
--- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
@@ -1,4 +1,4 @@
-# RFC: Single-file executable SDK runtime distribution (single-exe)
+# Agent Note: Single-file executable SDK runtime distribution (single-exe)
Status: implemented
@@ -21,7 +21,7 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h
`--sea` requires target ≥ node22; the exe uniformly targets node24. One pkg invocation packages exactly one target; multi-platform builds invoke it once per platform.
-Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay goldens, `$DSH_SNAPSHOT`); this document says "VFS" for the former.
+Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, `$DSH_SNAPSHOT`); this document says "VFS" for the former.
### The serving surface is a plugin: the two packages ui/jsonrpc + examples/jsonrpc-demo
diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
similarity index 98%
rename from docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
rename to .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
index 0b964e8a74..dcc9213c6b 100644
--- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
+++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
@@ -1,4 +1,4 @@
-# RFC: 单文件可执行的 SDK 运行时分发(single-exe)
+# Agent Note: 单文件可执行的 SDK 运行时分发(single-exe)
Status: implemented
@@ -21,7 +21,7 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)(vercel/pkg 归档后
`--sea` 要求构建目标 ≥ node22,exe 统一以 node24 为构建目标;每次 pkg 调用只打包一个构建目标,多平台各调用一次。
-术语提醒:pkg 的 `/snapshot` VFS 与本仓库测试体系的“快照”(ACP 回放 golden、`$DSH_SNAPSHOT`)无关,本文用“VFS”指前者。
+术语提醒:pkg 的 `/snapshot` VFS 与本仓库测试体系的“快照”(ACP 回放预期输出、`$DSH_SNAPSHOT`)无关,本文用“VFS”指前者。
### 对外服务接口也是插件:ui/jsonrpc + examples/jsonrpc-demo 两包
diff --git a/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
similarity index 96%
rename from docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
rename to .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
index 2c3126793f..2ddbe0a351 100644
--- a/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
+++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
@@ -1,4 +1,4 @@
-# RFC: Agent-scope runtime design and correctness
+# Agent Note: Agent-scope runtime design and correctness
Status: implemented
@@ -26,9 +26,9 @@ The design can be skimmed as seven choices:
| Compose the model-visible prompt and tool surface | One shared tool view plus the authoritative assembly-waterfall result |
| Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary |
-The rest of this RFC expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks.
+The rest of this Agent Note expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks.
-The [July 8 RFC](2026-07-08-agent-scope-contexts.md) remains the contributor contract. The separate [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns `persona`, `toolFilter`, and `maxDepth`; this document discusses only how their setup fits the lifecycle.
+The [July 8 Agent Note](2026-07-08-agent-scope-contexts.md) remains the contributor contract. The separate [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns `persona`, `toolFilter`, and `maxDepth`; this document discusses only how their setup fits the lifecycle.
## Cordis model: context, fiber, effect, receiver, and waterfall
@@ -206,7 +206,7 @@ Tool presentation and execution share one private resolver. Prompt assembly rema
The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, Code Mode SDK generation, and restriction validation all use that resolver or its pre-restriction global-name view.
-The [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed.
+The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed.
`ToolRestriction` accepts readonly allow/deny names and compiles them into internal sets. Multiple restrictions intersect. Public `visible()` and `knownNames()` methods are unnecessary because only the registry needs the intermediate views.
@@ -328,13 +328,13 @@ The plugin does not police trusted setup by scanning registries or reject prompt
### Generated artifacts keep public contracts aligned
-The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates RFC](../process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules.
+The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and Code Mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
## Alternatives considered
-The [July 8 RFC](2026-07-08-agent-scope-contexts.md#alternatives-considered) owns alternatives to the public flat-scope contract. The alternatives here concern implementation shape.
+The [July 8 Agent Note](2026-07-08-agent-scope-contexts.md#alternatives-considered) owns alternatives to the public flat-scope contract. The alternatives here concern implementation shape.
### Use a transparent proxy as the scope carrier
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
new file mode 100644
index 0000000000..3a6b18cecf
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml
@@ -0,0 +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-14-provider-routed-llm-adapters.md: 98205d18d07752e0cdba86d7cba80368d45fd816
+2026-07-14-provider-routed-llm-adapters.zh.md: c35225a86baf4c2d09732b5940abbc8046d365fb
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
new file mode 100644
index 0000000000..98205d18d0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md
@@ -0,0 +1,91 @@
+# Agent Note: Provider-routed LLM adapters and a generic pi-ai backend
+
+Status: implemented
+
+English | [中文](2026-07-14-provider-routed-llm-adapters.zh.md)
+
+## Problem
+
+`dsh-llm` registered adapters by exact model name. A plugin supplied a model list at Cordis startup, `LlmService` stored one adapter per listed string, and `GenerateOptions.model` selected the adapter and the provider model at once. This worked while both shipping adapters targeted the same two DeepSeek models, but it conflated two independent decisions: which upstream provider owns a request, and which model that provider should run.
+
+The conflation prevents a provider gateway from serving an open-ended model catalog. OpenRouter, for example, is one provider with many model ids, while a private OpenAI-compatible endpoint may add models without changing the Harness plugin tree. Every newly selected model currently needs to have been registered during plugin startup. The same model id can also exist at multiple providers, so model-only registration cannot state which provider the caller intended.
+
+`dsh-llm-pi-ai` exposed none of pi-ai's provider abstraction. It constructed an inline DeepSeek `openai-completions` model, applied DeepSeek-specific payload patches, and stamped every replayed assistant message as DeepSeek. pi-ai itself has a provider/model catalog, selects APIs such as `openai-responses`, `anthropic-messages`, and `google-generative-ai`, and preserves provider-specific response ids and reasoning/tool signatures for later turns. The Harness conversion dropped that provenance, so simply replacing the inline model with a catalog lookup would have made same-model replay and cross-provider handoff incomplete.
+
+The adapter configuration also assumes one DeepSeek API key and endpoint. A generic backend needs independent credentials and endpoint overrides per provider while leaving AWS, Google ADC, OAuth, and other ambient authentication mechanisms to pi-ai.
+
+## Decision
+
+### Provider is the adapter registration key
+
+`GenerateOptions` and `LlmCallConfig` carry `provider: string` beside `model: string`; `AgentOptions` carries the corresponding optional creation field. A loop request is valid only after both values are non-empty, and both values are part of the logged request header. `agent/request` may return a replacement pair on any step, so a session can switch providers and models without changing the Cordis plugin lifecycle.
+
+`LlmService` registers and resolves adapters by provider. `registerAdapter(providers, adapter)` checks the entire provider list before mutating the registry, rejects a duplicate with `DUPLICATE_ADAPTER`, and disposes the whole registration as one effect. Model ids are not registration keys; the selected adapter still validates or forwards them. The later [LLM catalog and ACP selection Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) added advisory `listProviders()` / `listModels()` discovery without turning model membership into request validation.
+
+A provider has exactly one adapter owner in a Cordis context. `dsh-llm-deepseek` registers `deepseek`; `dsh-llm-pi-ai` may also register `deepseek`, but loading both owners is a configuration error rather than an ordering rule or fallback. A deployment that wants the hand-rolled DeepSeek implementation excludes `deepseek` from the pi-ai profiles. A deployment that wants pi-ai's DeepSeek implementation does not mount `dsh-llm-deepseek`.
+
+`dsh-llm-deepseek` removes its model registration list and accepts any model string routed through provider `deepseek`. Its request serialization, `/chat/completions` endpoint, thinking options, SSE parsing, and error behavior remain unchanged; `options.model` is still sent verbatim.
+
+### Explicit pi-ai provider profiles
+
+`dsh-llm-pi-ai` takes one non-empty list of provider profiles. Provider names must be unique within the list and present in pi-ai's `getProviders()` result. Each profile contains the provider name plus optional `apiKey`, `baseURL`, headers, reasoning level and budgets, cache retention, transport, SDK timeouts, and a Harness stream-idle timeout. Provider retry fields are deliberately absent: the adapter forces pi-ai's `maxRetries` to zero so one `stream()` call makes one visible provider attempt, while `dsh-llm-retry` owns bounded agent-level recovery. Credentials are never global: an explicit key applies only to its profile, while an absent key lets pi-ai resolve its standard environment variable, OAuth token, AWS credential chain, Google ADC, or other provider-native ambient authentication. An explicitly empty key is invalid configuration rather than an environment fallback.
+
+The plugin registers all configured provider names against one `PiAiAdapter` in one all-or-nothing call. A request uses its provider to select the matching profile and finds its model in `getModels(provider)` to obtain the catalog descriptor. An unknown provider fails at plugin load; an unknown model fails before network I/O with `UNKNOWN_MODEL`. The catalog object is never mutated. When a profile supplies `baseURL`, the adapter clones the selected descriptor and overrides only `baseUrl`, so a private endpoint can retain pi-ai's API, capabilities, compatibility flags, context limits, and reasoning map. The private endpoint must implement the selected provider's protocol, and the model id must still exist in the installed pi-ai catalog.
+
+The adapter calls pi-ai's `streamSimple()` so each catalog model chooses its registered API implementation, including OpenAI Responses instead of Chat Completions where the descriptor says `openai-responses`. Harness temperature, maximum tokens, signal, session id, and the profile's common stream options flow through directly. Profile headers merge with the mandatory Harness attribution headers, with Harness attribution winning its reserved names. The adapter no longer maintains DeepSeek-specific payload rewrites or a provider-protocol matrix.
+
+pi-ai's common stream options do not expose stop sequences. `dsh-llm-pi-ai` rejects a defined Harness `stop` option with `UNSUPPORTED_OPTION` rather than silently ignoring it or growing a second provider-specific payload implementation. `dsh-llm-deepseek` continues to support `stop` through its native request serializer.
+
+### Durable assistant provenance and replay state
+
+Assistant messages carry provider-neutral provenance containing the request's `provider` and `model`, plus an optional JSON-serializable adapter replay state. A successful `assistant/message` session event records this provenance and `deriveMessages()` returns it with the assistant message. User, system, context, and tool-result messages carry no assistant provenance. The provider/model fields are authoritative loop data; an adapter owns only its opaque replay-state payload.
+
+A terminal successful `finish` chunk may carry replay state, and `BlockAssembler` retains it alongside usage and finish reason. The loop attaches it to the assistant provenance only when the post-`agent/step-result` content is structurally equal to the assembled provider output. A listener that rewrites content keeps the provider/model provenance but loses the now-stale replay state. Error and aborted responses do not produce a normal assistant message and therefore do not enter future model history.
+
+The pi-ai replay state is a versioned, minimal projection of its successful `AssistantMessage`: source API/provider/model, response id/model, stop reason, and index-aligned text, thinking, and tool-call signatures. It does not duplicate text or tool arguments already carried by Harness content blocks, and it omits diagnostics, timestamps, usage, and errors. On a later request, `LlmService` gives replay state to the target adapter only when the historical provider and target provider are currently owned by the same adapter instance. That adapter combines the logged Harness content with replay state when it can restore the historical response, and owns any required cross-model or cross-provider conversion. An adapter receiving replay state with an unknown version or mismatched block shape fails explicitly; a different adapter receives only provider-neutral content and provenance.
+
+This state is model-visible replay input and therefore follows the existing [reconstructable-request rule](2026-07-05-reconstructable-requests.md): it is present in both the terminal `finish` chunk and the assembled `assistant/message` provenance that drives derivation. Resume and fork preserve it verbatim. Compaction that shadows the assistant message also removes its replay state from the active surface; the summary is ordinary provider-neutral content.
+
+### Propagate the target through every request producer
+
+Every model-selection surface carries provider and model together: declarative agents, ACP and stdio app config, the JSON-RPC initialize request, subagent overrides and inheritance, workflow child overrides, and direct compaction summarization. Subagents inherit both fields from their parent before applying request overrides. The system-prompt variable set gains `provider` beside `model`.
+
+Compaction configuration gains `summarizationProvider` beside `summarizationModel`. Both are empty to inherit, or both are non-empty to select an explicit target; a half-configured pair fails load. Inheritance uses the last logged request target when one exists and falls back to the agent's creation options. `compact/summary` records both fields with the existing model-call envelope.
+
+The JSON-RPC runtime receives provider and model explicitly. Its convenience fallback mounts `dsh-llm-deepseek` only for provider `deepseek` when that provider has no registered owner; other missing providers fail without guessing an adapter.
+
+The on-disk session format remains the pre-release pinned version `0`, with no compatibility promise. Seed/load validation rejects request headers lacking provider and assistant messages lacking required provenance instead of accepting an old shape that can no longer reconstruct the request.
+
+## Alternatives considered
+
+**Keep model names as registry keys and add wildcard adapters.** A wildcard introduces fallback ordering between exact registrations and catch-all plugins, makes duplicate ownership dependent on listener order, and still cannot distinguish the same model id at two providers without another convention.
+
+**Encode provider and model into one string.** Values such as OpenRouter's `openai/gpt-*` already contain provider-like prefixes and slashes. A delimiter convention would leak routing syntax into every model selector and require escaping rules; two explicit fields are unambiguous and independently loggable.
+
+**Add `backend + provider + model`.** A backend key would allow `dsh-llm-deepseek` and pi-ai's DeepSeek implementation to coexist and switch per request. The accepted deployment rule is instead one adapter owner per provider: implementations of the same upstream are alternatives selected by plugin composition. A third routing dimension would burden every request and configuration for a capability with no current consumer.
+
+**Let `dsh-llm-pi-ai` automatically register every pi-ai provider.** This would claim ambient credentials and provider names the deployment never intended to expose, and would conflict with native adapters such as `dsh-llm-deepseek`. Explicit profiles make capability and credential scope reviewable.
+
+**Mount one pi-ai plugin instance per provider.** Separate instances isolate config but repeat plugin declarations and cannot make profile registration atomic. One adapter already receives provider on every request, so a validated profile map is the smaller lifecycle surface.
+
+**Accept arbitrary inline pi-ai model descriptors.** This would support catalog-external private model ids, but it exposes pi-ai's model and compatibility schema as Harness configuration and makes the adapter responsible for validating protocol-specific combinations. The first version supports custom endpoints by overriding `baseURL` on catalog models; custom descriptors require a separate decision after a real catalog-external deployment is identified.
+
+## Consequences
+
+- Provider names are deployment-wide route ownership keys: two providers may use the same model string, but mounting two adapters for one provider fails at load instead of creating fallback order.
+- Model selection no longer changes the Cordis plugin graph. Catalog-backed adapters can accept any installed catalog model selected after startup, while the native DeepSeek adapter forwards arbitrary DeepSeek model ids.
+- A custom `baseURL` preserves the selected catalog model's protocol and capabilities; it does not make catalog-external model ids valid. Private endpoints must implement that catalog entry's protocol.
+- pi-ai credentials, transport knobs, SDK timeouts, and the five-minute-default `streamIdleTimeoutMs` watchdog are scoped per provider profile. Hidden provider retries are disabled; bounded retries belong to the separately composed agent recovery policy.
+- `dsh-llm-pi-ai` rejects stop sequences because pi-ai's common stream API cannot express them; the native DeepSeek adapter retains its stop support.
+- Replay state is portable only within the adapter instance that owns both the historical and target providers. Cross-provider and cross-model restoration is an adapter responsibility, and another adapter receives provider-neutral history without the opaque state.
+- Current pre-release session JSONL requires provider/model request headers and assistant provenance. Older shapes remain version `0` but are rejected rather than migrated.
+
+## Testing
+
+- Unit coverage exercises registry conflicts, request reconstruction, session validation, profile resolution, single-attempt option forwarding, native API selection including OpenAI Responses, conversion, replay validation, error mapping, caller cancellation, idle-timeout transport termination, content rewrites, and same-instance versus different-instance replay dispatch.
+- Keyless loop/session tests and ACP snapshots exercise durable provider/model metadata, resume and fork propagation, workflow/subagent overrides, and unchanged user-visible transcripts; the key-gated DeepSeek e2e retains real provider streaming and tool follow-up coverage.
+- Public JSDoc, package READMEs, architecture and core-data-structure docs, generated catalogs, examples, session fixtures, and Python SDK pairs use provider/model targets consistently and are checked by the repository documentation and type-equivalence gates.
+
+## Risks
+
+This is a repo-wide pre-release API break: model-only request construction, adapter registration, app protocols, fixtures, and persisted version-0 event shapes all change together, with no compatibility aliases. The provider exclusivity rule deliberately prevents two implementations of the same upstream from coexisting in one context. A pi-ai dependency update can change the accepted provider/model catalog, so the lockfile and adapter e2e matrix define the tested set. Custom `baseURL` endpoints inherit the chosen catalog model's protocol assumptions and cannot repair an incompatible proxy. Catalog-external model descriptors and multimodal content remain unsupported. pi-ai replay state may contain opaque encrypted reasoning signatures; it is persisted because the provider requires it for continuity, but it is never rendered or logged outside the existing session record.
diff --git a/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
new file mode 100644
index 0000000000..c35225a86b
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md
@@ -0,0 +1,91 @@
+# Agent Note: 基于提供方路由的 LLM 适配器与通用 pi-ai 后端
+
+Status: implemented
+
+[English](2026-07-14-provider-routed-llm-adapters.md) | 中文
+
+## 问题
+
+`dsh-llm` 按精确模型名称注册适配器。插件在 Cordis 启动时提供模型列表,`LlmService` 为列表中的每个字符串保存一个适配器,`GenerateOptions.model` 同时选择适配器与提供方模型。两个正式适配器都只面向相同的两个 DeepSeek 模型时,这种方式可以工作,但它混淆了两个独立决策:由哪个上游提供方承接请求,以及该提供方应运行哪个模型。
+
+这种混淆使提供方网关无法提供开放的模型目录。例如,OpenRouter 是一个包含大量模型 ID 的提供方,私有 OpenAI 兼容端点也可能在不修改 Harness 插件树的情况下增加模型。目前,每个新选择的模型都必须在插件启动期间完成注册。同一个模型 ID 还可能存在于多个提供方中,因此仅按模型注册无法表达调用方预期使用的提供方。
+
+`dsh-llm-pi-ai` 没有暴露 pi-ai 的提供方抽象。它以内联方式构造 DeepSeek `openai-completions` 模型,应用 DeepSeek 专用的 payload 补丁,并将每条回放的助手消息标记为 DeepSeek。pi-ai 自身提供提供方/模型目录,能够选择 `openai-responses`、`anthropic-messages`、`google-generative-ai` 等 API,并保留提供方专用的响应 ID,以及后续轮次所需的推理和工具签名。Harness 转换丢弃了这些来源信息,因此仅将内联模型替换为目录查询,会导致同模型回放与跨提供方移交不完整。
+
+适配器配置同样假定只存在一个 DeepSeek API 密钥和端点。通用后端需要为各提供方分别配置凭据和端点覆盖,同时继续由 pi-ai 处理 AWS、Google ADC、OAuth 等环境认证机制。
+
+## 决策
+
+### 提供方作为适配器注册键
+
+`GenerateOptions` 与 `LlmCallConfig` 在 `model: string` 之外携带 `provider: string`,`AgentOptions` 则携带对应的可选创建字段。只有两个值都非空时,agent loop(智能体循环)请求才有效;两个值也都会写入请求头日志。`agent/request` 可以在任意步骤返回替换后的字段组合,因此会话可以切换提供方与模型,无需改变 Cordis 插件生命周期。
+
+`LlmService` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时返回 `DUPLICATE_ADAPTER`,并将整组注册作为一个 effect 释放。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP 模型选择 Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。
+
+在一个 Cordis 上下文中,一个提供方只能有一个适配器所有者。`dsh-llm-deepseek` 注册 `deepseek`;`dsh-llm-pi-ai` 也可以注册 `deepseek`,但同时加载两个所有者属于配置错误,不采用顺序规则或回退行为。若部署选择手写的 DeepSeek 实现,需从 pi-ai 配置中排除 `deepseek`;若部署选择 pi-ai 的 DeepSeek 实现,则不挂载 `dsh-llm-deepseek`。
+
+`dsh-llm-deepseek` 移除模型注册列表,接受通过 `deepseek` 提供方路由的任意模型字符串。其请求序列化、`/chat/completions` 端点、thinking 选项、SSE(Server-Sent Events)解析和错误行为保持不变;`options.model` 仍会原样发送。
+
+### 显式 pi-ai 提供方配置
+
+`dsh-llm-pi-ai` 接受一个非空的提供方配置列表。列表内的提供方名称必须唯一,并且存在于 pi-ai 的 `getProviders()` 结果中。每项配置包含提供方名称,以及可选的 `apiKey`、`baseURL`、headers、推理级别和预算、缓存保留设置、传输方式、SDK 超时和 Harness 流空闲超时。配置中有意不提供重试字段:适配器强制将 pi-ai 的 `maxRetries` 设为零,使一次 `stream()` 调用只发起一次可见的提供方请求;有界的 agent 层恢复由 `dsh-llm-retry` 负责。凭据不设全局值:显式密钥仅对所属配置生效;未提供密钥时,pi-ai 使用标准环境变量、OAuth token、AWS 凭据链、Google ADC 或其他提供方原生环境认证。显式空密钥属于无效配置,不会回退到环境认证。
+
+插件通过一次全有或全无调用,将所有已配置的提供方名称注册到同一个 `PiAiAdapter`。请求按 provider 选择对应配置,并在 `getModels(provider)` 中查找模型以取得目录描述符。未知提供方会在插件加载时失败;未知模型会在网络 I/O 前以 `UNKNOWN_MODEL` 失败。适配器不会修改目录对象。当配置提供 `baseURL` 时,适配器复制选中的描述符,仅覆盖 `baseUrl`,使私有端点保留 pi-ai 的 API、能力、兼容标志、上下文限制与推理映射。私有端点必须实现所选提供方的协议,模型 ID 也仍须存在于已安装的 pi-ai 目录中。
+
+适配器调用 pi-ai 的 `streamSimple()`,因此每个目录模型会选择其注册的 API 实现;描述符为 `openai-responses` 时使用 OpenAI Responses,而非 Chat Completions。Harness 的 temperature、最大 token 数、signal、session ID,以及提供方配置中的通用流选项均直接传递。配置 headers 与 Harness 强制归因 headers 合并;发生保留名称冲突时,以 Harness 归因为准。适配器不再维护 DeepSeek 专用 payload 重写或提供方协议矩阵。
+
+pi-ai 的通用流选项不支持停止序列。若 Harness `stop` 选项已定义,`dsh-llm-pi-ai` 会以 `UNSUPPORTED_OPTION` 拒绝请求,不会静默忽略,也不会增加第二套提供方专用 payload 实现。`dsh-llm-deepseek` 继续通过原生请求序列化器支持 `stop`。
+
+### 持久化助手来源信息与回放状态
+
+助手消息携带提供方无关的来源信息,其中包含请求的 `provider` 和 `model`,以及可选的 JSON 可序列化适配器回放状态。成功的 `assistant/message` 会话事件记录这些来源信息,`deriveMessages()` 返回助手消息时也会包含这些信息。用户、system、context 与工具结果消息不携带助手来源信息。provider/model 字段是 agent loop 的权威数据;适配器仅拥有其不透明回放状态 payload。
+
+成功的终止 `finish` 分片可以携带回放状态,`BlockAssembler` 会将其与 token 用量和结束原因一起保留。只有当 `agent/step-result` 处理后的内容与提供方组装输出在结构上相等时,agent loop 才会把回放状态附加到助手来源信息。监听器重写内容后,provider/model 来源信息仍会保留,但已经陈旧的回放状态会被移除。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。
+
+pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包含源 API/provider/model、响应 ID/model、停止原因,以及按索引对齐的文本、thinking 和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmService` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。适配器收到未知版本或块形状不匹配的回放状态时会显式失败;其他适配器只能收到提供方无关的内容与来源信息。
+
+该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](2026-07-05-reconstructable-requests.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 来源信息中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。
+
+### 在所有请求生产方中传播目标
+
+每个模型选择接口都同时携带 provider 与 model:声明式 agent、ACP(Agent Client Protocol)和 stdio 应用配置、JSON-RPC initialize 请求、subagent 覆盖与继承、工作流子 agent 覆盖,以及直接压缩摘要。subagent 先从父 agent 继承两个字段,再应用请求覆盖。系统提示词变量集合在 `model` 之外增加 `provider`。
+
+压缩配置在 `summarizationModel` 之外增加 `summarizationProvider`。两个值均为空时继承,均非空时选择显式目标;只配置其中一个会导致加载失败。继承优先使用最近一次记录的请求目标,没有时回退到 agent 创建选项。`compact/summary` 使用现有模型调用 envelope 记录两个字段。
+
+JSON-RPC 运行时显式接收 provider 与 model。仅当 `deepseek` 提供方没有注册所有者时,其便利回退才会挂载 `dsh-llm-deepseek`;其他缺失的提供方会直接失败,不会猜测适配器。
+
+磁盘会话格式仍使用预发布阶段固定的版本 `0`,且不承诺兼容性。seed/load 验证会拒绝缺少 provider 的请求头,以及缺少必需来源信息的助手消息,不会接受已无法重建请求的旧格式。
+
+## 考虑过的替代方案
+
+**继续以模型名称作为注册表键,并增加通配适配器。** 通配机制会在精确注册与兜底插件之间引入回退顺序,使重复所有权取决于监听器顺序;若不再增加其他约定,仍无法区分不同提供方中相同的模型 ID。
+
+**将提供方与模型编码到一个字符串中。** OpenRouter 的 `openai/gpt-*` 等值已经包含类似提供方的前缀和斜杠。分隔符约定会把路由语法泄漏到每个模型选择接口,并需要转义规则;两个显式字段更清晰,也可以分别记录日志。
+
+**增加 `backend + provider + model`。** backend 键可以让 `dsh-llm-deepseek` 与 pi-ai 的 DeepSeek 实现共存,并按请求切换。最终采用的部署规则是一个提供方对应一个适配器所有者:同一上游的不同实现属于由插件组合选定的替代项。第三个路由维度会增加每个请求与配置的负担,却没有当前消费方。
+
+**让 `dsh-llm-pi-ai` 自动注册所有 pi-ai 提供方。** 这种方式会占用部署无意暴露的环境凭据和提供方名称,并与 `dsh-llm-deepseek` 等原生适配器冲突。显式配置可以审查能力和凭据范围。
+
+**每个提供方挂载一个 pi-ai 插件实例。** 独立实例可以隔离配置,但会重复插件声明,也无法实现配置注册的原子性。每个请求本就向同一个适配器提供 provider,因此经过验证的配置映射具有更小的生命周期接口。
+
+**接受任意内联 pi-ai 模型描述符。** 这种方式可支持目录外的私有模型 ID,但会将 pi-ai 的模型与兼容性 schema 暴露为 Harness 配置,并要求适配器验证协议专用组合。当前版本通过覆盖目录模型的 `baseURL` 支持自定义端点;只有实际出现目录外部署需求后,才会另行决策是否支持自定义描述符。
+
+## 影响
+
+- 提供方名称是部署范围内的路由所有权键:两个提供方可以使用相同的模型字符串,但为同一个提供方挂载两个适配器会在加载时失败,不会形成回退顺序。
+- 模型选择不再改变 Cordis 插件图。目录型适配器可以接受启动后选择的任意已安装目录模型,原生 DeepSeek 适配器则会转发任意 DeepSeek 模型 ID。
+- 自定义 `baseURL` 会保留所选目录模型的协议与能力,但不会让目录外模型 ID 变为有效。私有端点必须实现该目录项对应的协议。
+- pi-ai 凭据、传输选项、SDK 超时,以及默认五分钟的 `streamIdleTimeoutMs` 空闲超时机制均按提供方配置隔离。系统禁用隐藏的提供方重试;有界重试由单独组合的 agent 恢复策略负责。
+- pi-ai 的通用流 API 无法表达停止序列,因此 `dsh-llm-pi-ai` 会拒绝停止序列;原生 DeepSeek 适配器仍支持停止序列。
+- 仅当历史提供方与目标提供方归同一个适配器实例所有时,回放状态才可移植。适配器负责跨提供方和跨模型恢复;其他适配器只接收不含不透明状态的提供方无关历史。
+- 当前预发布会话 JSONL 要求请求头包含 provider/model,助手消息包含来源信息。旧格式仍使用版本 `0`,但会被拒绝,不执行迁移。
+
+## 测试
+
+- 单元测试覆盖注册表冲突、请求重建、会话验证、配置解析、单次请求的选项转发、包括 OpenAI Responses 在内的原生 API 选择、转换、回放验证、错误映射、调用方取消、空闲超时导致的传输终止、内容重写,以及同一实例与不同实例间的回放分发。
+- 无密钥的 agent loop/会话测试和 ACP 快照覆盖持久化 provider/model 元数据、恢复与 fork 传播、工作流/subagent 覆盖,以及不变的用户可见 transcript(文本记录);密钥门控的 DeepSeek e2e 测试保留真实提供方的流式输出与工具后续调用覆盖率。
+- 公共 JSDoc、package README、架构与核心数据结构文档、生成目录、示例、会话 fixture(测试前置数据)和 Python SDK 配对文档统一使用 provider/model 目标,并由仓库文档与类型等价门禁校验。
+
+## 风险
+
+这是一次覆盖全仓库的预发布 API 破坏性变更:仅模型的请求构造、适配器注册、应用协议、fixture,以及持久化版本 0 事件格式会同时变化,不提供兼容别名。提供方排他规则有意禁止同一上游的两个实现共存于同一上下文。pi-ai 依赖升级可能改变可接受的提供方/模型目录,因此锁文件与适配器 e2e 矩阵定义已验证集合。自定义 `baseURL` 端点会继承所选目录模型的协议假设,无法修复不兼容的代理。目录外模型描述符与多模态内容仍不受支持。pi-ai 回放状态可能包含不透明的加密推理签名;提供方需要该信息维持连续性,因此系统会持久化该状态,但不会在现有会话记录之外渲染或记录它。
diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
new file mode 100644
index 0000000000..d5e16246fe
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
@@ -0,0 +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-15-agent-initiator-scope.md: 69648100e76cfc212469854188d664357fec22f1
+2026-07-15-agent-initiator-scope.zh.md: 835d7a5b2ab6d2d6fce7971de4fd9d6c69e50d77
diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md
new file mode 100644
index 0000000000..69648100e7
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md
@@ -0,0 +1,65 @@
+# Agent Note: Initiating Agent scope over AsyncLocalStorage
+
+Status: implemented
+
+English | [中文](2026-07-15-agent-initiator-scope.zh.md)
+
+## Problem
+
+The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. Changing a root `ctx.agent` to mean “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently.
+
+Deep process-local infrastructure sometimes needs a trusted initiating Agent below explicit loop, tool, and request parameters—for example, a host-aware transport, tracing helper, logger, or gateway client. Requiring every private helper to forward `agent` adds repetition, while a process-global mutable slot is incorrect across `await`. Model-visible arguments are unsuitable because a model must not choose a trusted Session or routing header. The carrier belongs to the Agent service rather than optional model-visible context.
+
+## Decision
+
+The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; a separate private run token records nested boundary lineage only for teardown bookkeeping and carries no identity. The [core-data catalog](../../../../docs/core-data-structures/core.md#initiating-agent) identifies the carried type.
+
+`currentInitiator()` reads optionally, `requireInitiator()` throws `no initiating agent is active`, and `withInitiator(agent, operation)` preserves the operation's exact synchronous value or Promise. `withoutInitiator(operation)` establishes a clearing boundary for work that must not inherit an Agent. Session remains derived as `agent.session`; turn, step, tool call, `signal`, model, `cwd`, sandbox, and authorization stay with their existing owners.
+
+`AgentLoop` already injects `ctx.agents` and wraps each concrete driver's complete `runLoop` lifetime in `agents.withInitiator(agent, ...)`. Its package-private loop, turn, step, and tool-call orchestration entries recover the exact Agent from `ctx.agents`, derive `agent.session` once, and let operation-local helpers capture it instead of forwarding the concrete driver or `Session` through shallow interfaces. A leaf helper keeps a narrow `Session` parameter when that is its actual interface rather than accepting a broader `Context` only for an ambient lookup.
+
+Concurrent drivers receive independent stores. A child driver's continuations carry the child, while the caller resumes in its prior store as soon as `withInitiator()` returns; active-run tracking keeps the returned Promise in the teardown drain until it settles. Creation, persistence load, and unpublished `setup(agentCtx)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while `agentCtx.agent` explicitly identifies the child.
+
+Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, `GenerateOptions.sessionId`, task ownership, parent/child requests, `ctx.agent`, `agentCtx.agent`, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local.
+
+`AgentRegistry` owns an ordered initiator lifecycle. Teardown first rejects new boundaries; removing `ctx.agents` then drains injected dependents such as AgentLoop, and the registry waits for active returned-Promise boundaries before calling `AsyncLocalStorage.disable()`. If a boundary's inherited async chain starts an owning Cordis fiber's unload, the private run-token lineage releases that nested boundary chain from the drain, which prevents teardown from waiting on itself while unrelated boundaries still drain. `currentInitiator()` and `requireInitiator()` remain usable through a retained in-flight service reference while the ordinary drain runs; after disposal, initiator methods throw `agent initiator scope is disposed`. Root Context disposal may start sibling fiber teardown concurrently, so active-boundary counting remains necessary in addition to Cordis dependency ordering.
+
+Initiator scope does not own detached work: registry drain tracks only the Promise returned by `withInitiator()` or `withoutInitiator()`. Asynchronous resources created inside a boundary inherit its store until they settle or ALS is disabled, so their owning seam must stop unreturned work explicitly. Agent-owned foreground work returns its lifetime and keeps its cancellation contract. Unrelated timers, queues, and deployment infrastructure start under `withoutInitiator(operation)`; queue, worker, process, and wire boundaries serialize identity rather than expecting ALS propagation.
+
+A host-aware transport may derive a deployment-owned header such as `X-Harness-Session-Id` from `ctx.agents.requireInitiator().session.id`; the header is absent from model-visible schema and arguments. No production MCP or Web transport adopts such a header in this decision. A test-double transport proves the trusted boundary without assigning host routing policy to an existing provider-neutral seam.
+
+This decision extends the [Agent registration-scope contract](2026-07-08-agent-scope-contexts.md) and its [runtime design](2026-07-12-agent-scope-runtime-design.md); it does not change their static `agent.ctx` meaning.
+
+## Verification
+
+Agent service tests pin optional and required reads, exact synchronous and cross-realm Promise identity, intrinsic Promise settlement observation, overlapping, nested, and cleared boundaries, restoration after throws or rejection, ordinary and reentrant drain ordering, and retained-reference errors. AgentLoop integration pins concurrent and nested drivers, agentless calls, AgentRegistry restart, root teardown, and package-private loop and tool scheduling through the ambient lookup. Composition, module-graph, build, and runtime-closure checks keep `ctx.agents` wired through the default bundle, SDK spine, Python runtime closure, and direct AgentLoop harnesses without another provider.
+
+A test-double host-aware transport derives `X-Harness-Session-Id` internally and verifies that tool schema and logged arguments contain no identity field. The service deliberately does not drain async work omitted from the Promise returned by the boundary operation; that work remains subject to its owner's explicit stop contract.
+
+## Alternatives considered
+
+**Pass Agent through every function.** Public, worker, process, persistence, and wire boundaries continue to do this, but requiring every process-local private helper to carry Agent adds repetitive forwarding without improving trust. ALS is confined to the asynchronous chain inside those explicit boundaries.
+
+**Make `ctx.agent` dynamic.** `ctx.agent` already means the static Agent associated with an Agent-scoped Cordis context. Changing the root meaning would mix registration and execution scopes and make concurrent behavior surprising.
+
+**Add a separate `ctx.agentExecution` service.** The carrier has no independent backend, configuration, or identity type: it stores the same `Agent` that `ctx.agents` already owns, and AgentLoop already depends on that service. A second mandatory provider would add package, composition, lifecycle, generated-catalog, and test-harness wiring without separating a real capability.
+
+**Store a named or complete runtime frame.** A one-field `{ agent }` frame only wraps the value, while Agent, Session, inbox, cancellation, turn, step, tool execution, and persistence already have authoritative owners. Adding more fields would create stale snapshots and another lifecycle; carrying `Agent` directly keeps the boundary named by its methods without duplicating state.
+
+**Include a step `AbortSignal`, `cwd`, sandbox, or authorization.** Their lifetimes and authority do not match the driver boundary, and their existing seams already pass them explicitly. Adding a control capability requires a separate decision and nested lifecycle contract.
+
+**Use a process-global `currentAgent`.** Concurrent Agents and subagents overwrite one another across awaited continuations, so a mutable global is correct only under a serialization guarantee the harness does not make.
+
+**Derive identity from model-visible arguments.** Model or user input cannot be trusted to select Session, tenant, or sandbox routing.
+
+**Add routing identity to every capability seam.** That spreads hosting concerns through provider-neutral APIs. A host-aware implementation owns its transport header while public boundaries remain explicit.
+
+## Consequences
+
+Deep infrastructure gains one trusted process-local initiating Agent without widening existing tool and capability requests. Concurrent and nested drivers isolate automatically, AgentLoop gains no additional mandatory service, and HMR/root disposal reaches quiescence before ALS is disabled.
+
+The dependency is implicit in function signatures and carries a capability-bearing Agent object. Consumers must restrict it to cross-cutting infrastructure, treat ambient presence as neither liveness nor authorization, and retain explicit cancellation and ownership checks. ALS also has an always-on propagation cost and does not cross worker, process, HTTP, or durable queue boundaries.
+
+The teardown design deliberately accepts Node's [Stability 1 (Experimental)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) `AsyncLocalStorage.disable()` dependency. Node requires `disable()` before an ALS instance can be garbage-collected, which matters when HMR replaces AgentRegistry-owned instances; the service state guard prevents a later boundary from re-entering the instance after disposal.
+
+The scope deliberately carries only the Agent, omitting turn, step, `signal`, `cwd`, sandbox, and authorization. A real consumer that cannot use existing explicit fields must justify any refinement separately; a stale copied field may at most mislabel telemetry, never grant control.
diff --git a/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
new file mode 100644
index 0000000000..835d7a5b2a
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
@@ -0,0 +1,65 @@
+# Agent Note: 基于 AsyncLocalStorage 的发起 Agent 作用域
+
+Status: implemented
+
+[English](2026-07-15-agent-initiator-scope.md) | 中文
+
+## 问题
+
+Harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 `ctx.agent` 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
+
+进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 `agent` 会造成重复,而进程级可变槽会在跨 `await` 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体归 Agent 服务所有,而非模型可见的可选上下文。
+
+## 决策
+
+必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../../docs/core-data-structures/core.md#initiating-agent)标明了所携带的类型。
+
+`currentInitiator()` 用于可选读取,`requireInitiator()` 抛出 `no initiating agent is active`,`withInitiator(agent, operation)` 保留操作返回的同步值或 Promise 本身。`withoutInitiator(operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。
+
+`AgentLoop` 已经注入 `ctx.agents`,并用 `agents.withInitiator(agent, ...)` 包裹每个具体驱动的完整 `runLoop` 生命周期。循环、轮次、步骤和工具调用的包内私有入口从 `ctx.agents` 恢复同一个 Agent,一次推导 `agent.session`,再由操作内辅助函数捕获该值,避免在浅层接口中转发具体驱动或 `Session`。若 `Session` 本身就是底层辅助函数的实际接口,该函数会保留狭窄的 `Session` 参数,而不会只为隐式查找而接收更宽泛的 `Context`。
+
+因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。
+
+隐式身份不会取代显式契约。`ToolExecution.agent`、`AssembleContext.agent`、`GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent`、`agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
+
+`AgentRegistry` 管理一个有序的发起方生命周期。teardown 会先拒绝新边界;移除 `ctx.agents` 后,AgentLoop 等注入方开始排空,注册表随后等待活动的返回 Promise 边界,最后调用 `AsyncLocalStorage.disable()`。如果某个边界继承的异步调用链启动所属 Cordis fiber 的卸载,私有运行标记谱系会从排空范围中释放该嵌套边界链,从而避免 teardown 等待自身完成,同时继续排空无关边界。在普通排空期间,进行中代码可通过保留的服务引用继续调用 `currentInitiator()` 和 `requireInitiator()`;dispose 后,发起方方法会抛出 `agent initiator scope is disposed`。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外仍必须统计活动边界。
+
+发起方作用域不负责管理脱离返回链的工作:注册表排空只跟踪 `withInitiator()` 或 `withoutInitiator()` 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所有前台工作会把完整生命周期纳入返回值,并保留显式取消契约。无关的定时器、队列和部署基础设施在 `withoutInitiator(operation)` 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。
+
+宿主感知的传输层可以从 `ctx.agents.requireInitiator().session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。
+
+本决策扩展 [Agent 注册作用域契约](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。
+
+## 验证
+
+Agent 服务测试锁定可选与必需读取、同步值和跨 realm Promise 的引用身份、内建 Promise 结束状态观察、并发、嵌套及清空边界、同步抛错或 Promise 拒绝后的恢复、普通与重入排空顺序及保留引用的错误。AgentLoop 集成测试锁定并发与嵌套驱动、无 Agent 调用、AgentRegistry 重启、根 Context 销毁,以及包内私有的循环和工具调度通过隐式查找完成。组合、模块图、构建及运行时闭包检查确保默认组合包、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 通过 `ctx.agents` 完成接线,无需其他提供方。
+
+测试替身形式的宿主感知传输层在内部推导 `X-Harness-Session-Id`,并验证工具 schema 与记录参数都不包含身份字段。服务有意不排空边界操作所返回 Promise 之外的异步工作;这类工作仍由所属方的显式停止契约管理。
+
+## 考虑过的替代方案
+
+**在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。
+
+**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。
+
+**新增独立的 `ctx.agentExecution` 服务。** 该载体没有独立后端、配置或身份类型:它存储的是 `ctx.agents` 已经管理的同一个 `Agent`,而 AgentLoop 本就依赖该服务。第二个必需提供方会增加包、组合、生命周期、生成目录及测试 harness 接线,却没有拆出真实能力。
+
+**保存命名帧或完整运行时帧。** 只有一个字段的 `{ agent }` 帧只是包装该值,而 Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。增加更多字段会产生陈旧快照和另一套生命周期;直接携带 `Agent`,由方法名标识边界,无需重复保存状态。
+
+**包含步骤级 `AbortSignal`、`cwd`、沙箱或授权。** 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。
+
+**使用进程级 `currentAgent`。** 并发 Agent 和 subagent 会在异步延续执行之间相互覆盖,因此可变全局值只在 Harness 不具备的串行保证下才正确。
+
+**从模型可见参数推导身份。** 不能信任模型或用户输入来选择会话、租户或沙箱路由。
+
+**给每个能力 seam 增加路由身份。** 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。
+
+## 后果
+
+深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,AgentLoop 不增加新的必需服务,HMR 或根 Context dispose 会在禁用 ALS 前完成排空。
+
+该依赖不会出现在函数签名中,并且携带一个具有控制能力的 Agent 对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。
+
+该销毁设计有意依赖 Node 的 [Stability 1(实验性)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) API `AsyncLocalStorage.disable()`。Node 要求在 ALS 实例可被垃圾回收前调用 `disable()`,这对 HMR 替换 AgentRegistry 所拥有的实例尤为重要;服务状态守卫会阻止 dispose 后通过后续边界重新进入该实例。
+
+该作用域有意只携带 Agent,省略轮次、步骤、`signal`、`cwd`、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。
diff --git a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml
new file mode 100644
index 0000000000..e83cfff95e
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml
@@ -0,0 +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-15-llm-model-catalog-and-acp-selection.md: 6cc8afc6c7431fbf3eb29fc358b432db4f72b529
+2026-07-15-llm-model-catalog-and-acp-selection.zh.md: 1cce7a58d0ec83dc01feaf72ccb61d294a78ddd5
diff --git a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
new file mode 100644
index 0000000000..6cc8afc6c7
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md
@@ -0,0 +1,66 @@
+# Agent Note: Advisory LLM catalogs and per-session ACP model selection
+
+Status: implemented
+
+English | [中文](2026-07-15-llm-model-catalog-and-acp-selection.zh.md)
+
+## Problem
+
+Provider-routed adapters let every request choose `provider + model`, but `LlmService` exposed only routing and streaming. A UI could not discover which providers were registered or which models an adapter was prepared to recommend. ACP clients therefore received no `model` session config option, so Zed, JetBrains, and VS Code integrations had no model list even though the request seam already supported runtime switching.
+
+Model discovery cannot become request validation. The hand-written DeepSeek adapter deliberately forwards arbitrary model ids to a public or private endpoint, while pi-ai has a finite installed catalog that is authoritative for its own request resolution. Treating one shared catalog as a whitelist would remove the private-endpoint behavior that provider routing was designed to preserve.
+
+ACP selection must also preserve the provider dimension. The same model id may appear under multiple routes, and switching a global adapter or agent template would leak one editor session's choice into every other session. Prompt variables and request routing must change together; a selection that lands during asynchronous prompt assembly cannot make `{{model}}` name one model while the request reaches another.
+
+## Decision
+
+### Provider-neutral advisory discovery
+
+`LlmAdapter` gains `providerInfo(provider)` and asynchronous `listModels(provider)` methods. Their provider-neutral results are `LlmProviderInfo { id, name }` and `LlmModelInfo { provider, id, name, description? }`. The defaults preserve existing adapter behavior by naming a provider after its route and advertising no models.
+
+`LlmService.listProviders()` returns detached metadata in registration order. `LlmService.listModels(provider)` delegates to the route owner, validates non-empty ids and names, rejects a mismatched provider or duplicate model id with `INVALID_CATALOG`, and returns detached values. Unknown providers still fail with `NO_ADAPTER`. Provider metadata is validated atomically during `registerAdapter()` so a malformed display record cannot leave a partial registration.
+
+Catalog membership is advisory. It drives selectors and diagnostics but never changes `stream()` routing and never rejects an otherwise valid request. Provider ownership remains exclusive and lifecycle-bound; model ids remain request-time adapter input.
+
+`dsh-llm-pi-ai` maps the configured provider's installed `getModels(provider)` entries into the neutral catalog. Its existing request-time catalog lookup remains authoritative and still rejects unknown models with `UNKNOWN_MODEL`. `dsh-llm-deepseek` accepts an optional `models` config containing display entries, defaulting to `deepseek-v4-flash` and `deepseek-v4-pro`. An explicit list replaces those defaults and an empty list disables discovery. The entries improve selector UX for known public or private models, while every unlisted model id continues to pass through unchanged.
+
+### ACP session config option
+
+The ACP bridge advertises one select with `id: model` and `category: model` in `session/new` and `session/load` when the session has a complete target whose provider is registered. Each opaque option value encodes the full provider/model pair. Models are grouped by provider when multiple non-empty provider groups exist; a single group is flattened for clients that render simple selects better.
+
+The session's current target is added to the displayed options when its adapter omits it. This preserves custom DeepSeek and private-endpoint models while keeping the adapter catalog advisory. A target with an unregistered provider is not advertised, and a model-less agent remains available to another `agent/request` supplier.
+
+`session/set_config_option` accepts only values from the current catalog snapshot and updates a target reference owned by that ACP session. No global `LlmService` or `AgentOptions` state changes, so concurrent sessions may select different providers and models. The existing permission select remains independent, and every response returns the complete refreshed option state.
+
+### Prompt/request consistency and durability
+
+Agent setup installs scoped `system-prompt/assemble` and `agent/request` listeners. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
+
+The request header remains the durable source of truth. When a selected target is actually used, the existing full `request/header` snapshot records it. `session/load` initializes the ACP selection from the folded last request header before falling back to bridge config. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
+
+ACP's experimental `providers/*` capability is not used. That draft surface configures provider base URLs, protocols, and headers, including secrets; it does not enumerate models and would give the UI authority to rewrite deployment-owned adapter configuration.
+
+## Alternatives considered
+
+**Return model strings only.** A model-only value loses the provider route and becomes ambiguous as soon as two providers expose the same id.
+
+**Make catalogs mandatory whitelists.** This conflicts with the hand-written adapter's arbitrary model pass-through and private deployments. The selected adapter already owns authoritative request validation.
+
+**Store selection in `AgentOptions` or `LlmService`.** Those are creation-wide or deployment-wide objects. Mutating them would couple concurrent ACP sessions and bypass the logged `agent/request` replacement path.
+
+**Persist a new model-selection session event immediately.** An unused UI selection has not affected a model request. Recording the existing request header when the target is consumed preserves the model-visible-if-and-only-if-logged rule without adding a second source of truth.
+
+**Use ACP `providers/*`.** That unstable API changes endpoint and authentication configuration rather than selecting a model for one session, and its lifecycle and secret-handling semantics do not match this feature.
+
+## Consequences
+
+- Any adapter can expose a dynamic model list without leaking provider-library types into the core seam.
+- Catalog consumers must treat absence as “not advertised,” never “invalid request.”
+- pi-ai-backed ACP deployments automatically inherit the installed pi-ai provider catalogs; hand-written DeepSeek deployments list known choices explicitly and retain arbitrary model support.
+- ACP clients receive a standard stable model config option, with provider-aware values and per-session isolation.
+- Request headers remain compatible with the provider-routed session shape; no new JSONL event or format version is required.
+- A catalog read can be asynchronous. ACP reads a detached snapshot before creating or resuming an agent, so discovery failure cannot leave a partially published session.
+
+## Testing
+
+Unit coverage validates catalog detachment and malformed metadata, pi-ai and DeepSeek catalog projection, ACP provider grouping, custom-current insertion, invalid values, provider/model request routing, prompt-variable alignment, concurrent-session isolation, model-less fallback, and load restoration from the request header. The existing ACP transport suites verify that the additional config option does not change prompt, cancellation, replay, approval, or tool-rendering behavior.
diff --git a/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md
new file mode 100644
index 0000000000..1cce7a58d0
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md
@@ -0,0 +1,66 @@
+# Agent Note: 建议性 LLM 目录与 ACP 会话级模型选择
+
+Status: implemented
+
+[English](2026-07-15-llm-model-catalog-and-acp-selection.md) | 中文
+
+## 问题
+
+基于提供方路由的适配器允许每次请求选择 `provider + model`,但 `LlmService` 只暴露路由和流式调用。UI 无法发现已注册的提供方,也无法知道适配器愿意推荐哪些模型。因此,ACP 客户端收不到 `model` 会话配置项;即使请求接缝已经支持运行时切换,Zed、JetBrains 和 VS Code 集成仍没有模型列表。
+
+模型发现不能变成请求校验。手写 DeepSeek 适配器会把任意模型 ID 原样转发给公开或私有端点,而 pi-ai 的有限安装目录则是其自身请求解析的权威依据。将共享目录视为白名单,会破坏提供方路由需要保留的私有端点能力。
+
+ACP 选择还必须保留提供方维度。同一个模型 ID 可能存在于多个路由下;切换全局适配器或 agent 模板会让一个编辑器会话的选择泄漏到其他会话。Prompt 变量与请求路由必须同时变化;如果选择发生在异步 prompt 组装期间,不能让 `{{model}}` 表示一个模型、实际请求却到达另一个模型。
+
+## 决策
+
+### 提供方中立的建议性发现
+
+`LlmAdapter` 增加 `providerInfo(provider)` 与异步 `listModels(provider)` 方法。其提供方中立结果分别为 `LlmProviderInfo { id, name }` 和 `LlmModelInfo { provider, id, name, description? }`。默认实现以路由名称作为提供方名称,并且不展示模型,从而保持现有适配器行为。
+
+`LlmService.listProviders()` 按注册顺序返回分离后的元数据。`LlmService.listModels(provider)` 委托给路由所有者,校验非空 ID 和名称,并在提供方不匹配或模型 ID 重复时以 `INVALID_CATALOG` 失败,最后返回分离后的值。未知提供方仍以 `NO_ADAPTER` 失败。提供方元数据在 `registerAdapter()` 期间进行原子校验,错误展示记录不会留下部分注册。
+
+目录成员关系仅提供建议。它驱动选择器与诊断,但不会改变 `stream()` 路由,也不会拒绝原本有效的请求。提供方所有权仍然具有排他性并绑定生命周期;模型 ID 仍是请求时传给适配器的输入。
+
+`dsh-llm-pi-ai` 将已配置提供方的安装目录 `getModels(provider)` 映射为中立目录。其现有请求时目录查询仍是权威依据,未知模型仍以 `UNKNOWN_MODEL` 失败。`dsh-llm-deepseek` 接受可选的 `models` 配置作为展示条目,默认包含 `deepseek-v4-flash` 和 `deepseek-v4-pro`。显式列表会替换这些默认值,空列表则关闭发现。这些条目改善已知公开或私有模型的选择体验,而所有未列出的模型 ID 仍会原样透传。
+
+### ACP 会话配置项
+
+当会话具有完整目标且目标提供方已注册时,ACP bridge 会在 `session/new` 与 `session/load` 中展示一个 `id: model`、`category: model` 的选择项。每个不透明选项值都编码完整的提供方/模型字段组合。存在多个非空提供方分组时按提供方分组;只有一个分组时将其展开,以便对简单选择器支持更好的客户端展示。
+
+如果适配器目录未包含会话当前目标,该目标仍会加入展示选项。这能保留自定义 DeepSeek 与私有端点模型,同时维持目录的建议性。提供方未注册的目标不会展示;缺少模型的 agent 仍可由其他 `agent/request` 提供者补齐。
+
+`session/set_config_option` 只接受当前目录快照中的值,并更新该 ACP 会话独占的目标引用。它不会修改全局 `LlmService` 或 `AgentOptions` 状态,因此并发会话可以选择不同的提供方和模型。现有权限选择项保持独立,每次响应都返回完整的刷新后配置项状态。
+
+### Prompt/请求一致性与持久化
+
+Agent setup 会安装作用域内的 `system-prompt/assemble` 与 `agent/request` 监听器。Prompt 组装为每个 step 只快照一次选中的字段组合,在下游 prompt 监听器完成后覆盖组装结果中的 `provider` 与 `model` 变量;请求监听器则在下游请求监听器完成后应用同一个快照。因此,异步组装期间发生的选择会从下一个 step 生效,不会导致 prompt 文本与路由分裂。其他调用配置字段保持不变。
+
+请求头仍是持久化事实来源。当选中目标被实际使用时,现有的完整 `request/header` 快照会记录它。`session/load` 先从折叠后的最后请求头初始化 ACP 选择,再回退到 bridge 配置。一个从未被请求使用的选择只保留在内存中,因为它从未成为模型可见状态。
+
+本功能不使用 ACP 的实验性 `providers/*` 能力。该草案接口配置提供方 base URL、协议和 headers,其中可能包含密钥;它不枚举模型,并且会赋予 UI 改写部署所有的适配器配置的权力。
+
+## 考虑过的替代方案
+
+**只返回模型字符串。** 仅模型值会丢失提供方路由;两个提供方暴露相同 ID 时立刻产生歧义。
+
+**将目录设为强制白名单。** 这与手写适配器的任意模型透传和私有部署冲突。请求的权威校验本就属于被选中的适配器。
+
+**将选择存入 `AgentOptions` 或 `LlmService`。** 这些对象分别面向创建过程或整个部署。修改它们会耦合并发 ACP 会话,并绕开带日志归因的 `agent/request` 替换路径。
+
+**立即写入新的模型选择会话事件。** 尚未使用的 UI 选择没有影响模型请求。目标被消费时记录现有请求头,既满足“模型可见当且仅当已记录”的规则,也不会引入第二个事实来源。
+
+**使用 ACP `providers/*`。** 该不稳定 API 用于修改端点与认证配置,而不是为单个会话选择模型;其生命周期和密钥处理语义都不适合本功能。
+
+## 结果
+
+- 任意适配器都能暴露动态模型列表,无需把提供方库类型泄漏到核心接缝。
+- 目录消费者必须把缺失理解为“未展示”,而不是“请求无效”。
+- 基于 pi-ai 的 ACP 部署会自动继承已安装的 pi-ai 提供方目录;手写 DeepSeek 部署显式列出已知选项,同时保留任意模型能力。
+- ACP 客户端会收到稳定标准的模型配置项,其中的值保留提供方信息,并按会话隔离。
+- 请求头继续使用基于提供方路由的会话结构;不需要增加 JSONL 事件或格式版本。
+- 目录读取可以是异步的。ACP 在创建或恢复 agent 前读取分离后的快照,因此发现失败不会留下部分发布的会话。
+
+## 测试
+
+单元测试覆盖目录分离与错误元数据、pi-ai 和 DeepSeek 目录投影、ACP 提供方分组、自定义当前模型补入、无效值、提供方/模型请求路由、prompt 变量一致性、并发会话隔离、无模型回退,以及从请求头恢复选择。现有 ACP 传输测试验证新增配置项不会改变 prompt、取消、回放、审批或工具展示行为。
diff --git a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
new file mode 100644
index 0000000000..d49aedb545
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
@@ -0,0 +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-15-replay-token-meter-service.md: 9bbc177f456e006179c466f8c245e4599db3dd5a
+2026-07-15-replay-token-meter-service.zh.md: 4437626c8651a80537d45197a93733271a592173
diff --git a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md
new file mode 100644
index 0000000000..9bbc177f45
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md
@@ -0,0 +1,60 @@
+# Agent Note: Replay token meter service
+
+Status: implemented
+
+English | [中文](2026-07-15-replay-token-meter-service.zh.md)
+
+## Problem
+
+Context pressure is useful outside compaction. A compaction backend, an overflow guard, or a future request-policy plugin can all need the same answer: how much of the configured context window does the durable request consume? Keeping that fold inside `dsh-compact-basic` duplicates replay logic, makes measurement unavailable without compaction, and encourages callers to reuse stale accounting.
+
+Provider usage is not a complete answer. It describes one successful call under one exact request envelope, while the current surface can grow, shrink, or be replaced afterward. Sessions also switch providers and models, old logs can lack chunk provenance, and usage fields separate input, cache-read, cache-write, output, and reasoning counts. A useful service therefore combines the latest exact anchor with conservative heuristic repricing and exposes the log revision consumed by each result.
+
+## Decision
+
+### One concrete LLM-family service
+
+`@deepseek-ai/dsh-token-meter` is one concrete package under `packages/llm/` and registers `ctx.tokenMeter`. It is not split into an interface and backend before a second implementation exists. `TokenMeterService` itself exposes `contextWindow`, `measure(session, requestHeader?)`, and `estimateMessage(message)`; consumers call the singleton service directly.
+
+The service has one `contextWindow`, defaulting to 128,000 tokens and configurable as a positive integer. Estimation uses a fixed four-characters-per-token heuristic plus structural overhead. There are no model profiles, density settings, tokenizer backends, or language-specific strategies.
+
+### Per-session replay folds
+
+Each session owns one isolated incremental fold. Active folds advance from `session/event`; every read catches up through the durable tail, so listener ordering, seeded sessions, and service reload do not change the answer. The fold tracks canonical full request-header snapshots, step boundaries, surface appends and replacements, assistant usage, and assistant-chunk provenance. A malformed next event fails transactionally and remains unread rather than partially mutating state.
+
+`measure(session, requestHeader?)` synchronizes the fold once and returns scalar pressure together with positional per-node prices. `totalTokens` remains request-and-response pressure; `surfaceTokens` is the surface-only heuristic total and equals the sum of `nodes[].tokens`. A `requestHeader` override changes pressure pricing only, while the surface fields always describe the current session. `estimateMessage(message)` applies the fixed heuristic without session state. Each result is one detached, deeply immutable snapshot carrying one `logRevision`. Every measurement clones the current nodes and is therefore O(surface).
+
+Provider usage is reused only when the measured canonical request envelope equals the latest successful-call anchor. Any provider, model, system, prefix, tool, or call-config change causes complete heuristic repricing. Surface changes remain a signed delta from a matching anchor, including negative values after a shrinking replacement. A later successful request replaces the earlier anchor, including across provider or model switches.
+
+Usage sums the disjoint input, cache-read, cache-write, and output buckets. Reasoning is not added a second time. Every successful model call records an `assistant/message`, including content-less and max-token calls, with its exact earlier chunk seqs. An explicit empty provenance list means a known empty provider stream; absent legacy provenance conservatively treats the durable assistant output as provider output.
+
+### Compact-basic consumes, but does not own, measurement
+
+`dsh-compact-basic` requires `ctx.tokenMeter`; `CompactService` gains no token methods or types. Configuration, the region transaction, and summarization stay in separate modules; the service registers automatic listeners itself, while `summarize()` remains its sole subclass hook. The singleton meter consistently prices pressure, retention, shadowed content, provenance, and non-shrinking-summary rejection.
+
+Automatic compaction uses one unified measurement for each threshold-and-retention decision. The region transaction measures after appending its durable `compact/start` lock and again after asynchronous summarization; any intervening durable append changes `logRevision` and prevents replacement.
+
+Compact policy has service-wide defaults: threshold ratio `0.8`, retained tail `floor(contextWindow × 0.16)`, `summarizationProvider: ''`, `summarizationModel: ''`, `maxTokens: 8192`, `compactionRetries: 1`, `maxOverflowRetries: 1`, and `auto: true`. Top-level `thresholdRatio` and `retainTokens` override the pressure policy; retention must remain below the resulting threshold. The summarization provider and model must both be set or both be empty; an empty pair resolves the latest logged request target, then the `AgentOptions` pair.
+
+Automatic pressure runs at `agent/post-step` and measures the canonical durable envelope produced under the provider/model actually selected by `agent/request`. A headerless session has no completed routed request to assess and produces no work; any routed target can use the singleton estimator. Canonical overflow recovery uses the same measurement for forced range selection and retries only after a proven surface replacement.
+
+## Testing
+
+Unit tests cover fixed estimation, envelope invalidation and anchor replacement, replay boundaries, immutable snapshots, routed pressure, convergence, overflow generation proof, and rollback. A real Loader/Include fixture verifies the zero-config token-meter and compact-basic load path in dependency order.
+
+## Alternatives considered
+
+- **Keep estimation inside `CompactService`** — rejected because measurement has consumers and replay semantics independent of compaction; it would also force every compactor to expose the same unrelated API.
+- **Split a token-meter interface from a heuristic backend immediately** — rejected because only one implementation exists. One concrete service preserves the future seam without speculative packages or configuration.
+- **Keep model-keyed windows and density profiles** — rejected because the deployment currently has one context policy and one estimator. Model registries, unknown-model failures, and configurable density add branches without a second behavior to select.
+- **Keep separate scalar and surface measurements** — rejected because callers would need two reads and revision matching for one decision. A scalar-only read could avoid cloning nodes below threshold, but the split API introduces a caller-side race window; the unified snapshot accepts O(surface) cloning in exchange for coherence.
+- **Treat provider usage as portable between envelopes** — rejected because model, tools, prefixes, and call config are request facts. Mismatch reprices the whole current request.
+
+## Consequences
+
+- Token pressure has one replay-aware owner that compaction and future plugins can share.
+- The default makes the bundled composition usable with two zero-config plugin entries; deployments override one context capacity when needed.
+- Fixed heuristic pricing remains an estimate of provider behavior and is not an exact tokenizer or request serializer.
+- Every measurement clones the current positional surface and therefore costs O(surface), including pressure checks that finish below threshold.
+- Measurements fail loudly on malformed durable boundaries. This turns corrupted replay into a named integration failure instead of silently drifting pressure.
+- Post-step pressure reads the exact logged routing/tools/prefix boundary; provider overflow classification remains the adapter-maintained backstop for requests rejected before a successful usage anchor.
diff --git a/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
new file mode 100644
index 0000000000..4437626c86
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
@@ -0,0 +1,60 @@
+# Agent Note: 回放式 token 计量服务
+
+Status: implemented
+
+[English](2026-07-15-replay-token-meter-service.md) | 中文
+
+## 问题
+
+上下文压力并不只对压缩有用。压缩后端、溢出保护或未来的请求策略插件都可能需要回答同一个问题:持久请求占用了已配置上下文窗口的多少容量?如果把该折叠逻辑留在 `dsh-compact-basic` 内部,就会重复实现回放逻辑,使未加载压缩的调用方无法使用计量,并诱使调用方复用陈旧的核算结果。
+
+提供方 usage 也不是完整答案。它只描述某个精确请求信封下的一次成功调用,而当前表层之后还可能增长、缩小或被替换。会话也可能切换提供方与模型,旧日志可能缺少分片来源,usage 字段还会分别报告输入、缓存读取、缓存写入、输出与推理计数。因此,可用的服务必须把最新精确锚点与保守的启发式重新定价结合起来,并公开每个结果已经消费的日志修订号。
+
+## 决策
+
+### 一个具体的 LLM 家族服务
+
+`@deepseek-ai/dsh-token-meter` 是 `packages/llm/` 下的单个具体包,并注册 `ctx.tokenMeter`。在第二种实现出现之前,它不会被拆成接口与后端。`TokenMeterService` 本身公开 `contextWindow`、`measure(session, requestHeader?)` 与 `estimateMessage(message)`;消费方直接调用这个单例服务。
+
+服务只有一个 `contextWindow`,默认值为 128,000 token,并允许配置为正整数。估算采用固定的每 token 四个字符启发式规则,并加上结构开销。服务不提供模型 profile、密度设置、分词器后端或语言专用策略。
+
+### 逐会话回放折叠
+
+每个会话都有一个隔离的增量折叠。活跃折叠通过 `session/event` 前进;每次读取都会追到持久日志尾部,因此监听器顺序、种子会话与服务重载不会改变答案。折叠跟踪规范的完整请求头快照、步骤边界、表层追加与替换、assistant usage,以及 assistant 分片来源。下一个畸形事件会以事务方式失败并保持未读,不会让状态只修改一半。
+
+`measure(session, requestHeader?)` 只同步一次折叠,并在返回标量压力的同时给出逐位置节点价格。`totalTokens` 仍表示请求与响应压力;`surfaceTokens` 是仅针对表层的启发式总量,并等于 `nodes[].tokens` 之和。`requestHeader` 覆盖只改变压力定价,表层字段始终描述当前会话。`estimateMessage(message)` 不依赖会话状态,直接应用固定启发式规则。每个结果都是一个分离且深度不可变的快照,只携带一个 `logRevision`。每次计量都会复制当前节点,因此成本为 O(surface)。
+
+只有当待计量的规范请求信封等于最近一次成功调用的锚点时,服务才复用提供方 usage。提供方、模型、系统提示词、前缀、工具或调用配置任一变化都会触发完整的启发式重新定价。表层变化相对匹配锚点保留有符号增量,包括缩小替换后的负值。后续成功请求会替换先前锚点,提供方或模型切换时也一样。
+
+Usage 会对互不重叠的输入、缓存读取、缓存写入与输出 bucket 求和,不会再次加入推理计数。每次成功模型调用都会记录 `assistant/message`,包括无内容调用与达到 token 上限的调用,并带上精确的更早分片 seq。显式空来源列表表示已知为空的提供方流;旧日志中缺失的来源则保守地把持久 assistant 输出视为提供方输出。
+
+### compact-basic 消费计量,但不拥有计量
+
+`dsh-compact-basic` 要求 `ctx.tokenMeter`;`CompactService` 不增加 token 方法或类型。配置、区域事务与摘要器分别保留在独立模块中,服务自身注册自动监听器,而 `summarize()` 仍是唯一的子类 hook。单例计量器一致用于压力、保留、被遮蔽内容、来源以及非缩小摘要拒绝的定价。
+
+自动压缩的每次阈值与保留联合决策只使用一次统一计量。区域事务先追加持久 `compact/start` 锁,再执行一次计量,并在异步摘要完成后再次计量;期间任何持久追加都会改变 `logRevision`,从而阻止替换。
+
+压缩策略采用服务级默认值:阈值比例 `0.8`、保留尾部 `floor(contextWindow × 0.16)`、`summarizationProvider: ''`、`summarizationModel: ''`、`maxTokens: 8192`、`compactionRetries: 1`、`maxOverflowRetries: 1` 与 `auto: true`。顶层 `thresholdRatio` 与 `retainTokens` 覆盖压力策略;保留值必须小于最终阈值。摘要提供方与模型必须同时设置或同时为空;空组合先解析最近记录的请求目标,再使用 `AgentOptions` 中的组合。
+
+自动压力检查运行在 `agent/post-step`,并计量 `agent/request` 实际所选提供方/模型产生的规范持久信封。没有请求头的会话尚无已完成的路由请求可供判断,因此不执行工作;任意路由目标都可使用这个单例估算器。规范化溢出恢复使用同一计量结果强制选择范围,并且只有在表层替换得到证明后才重试。
+
+## 测试
+
+单元测试覆盖固定估算、信封失效与锚点替换、回放边界、不可变快照、已路由压力、收敛、溢出 generation 证明与回滚。真实 Loader/Include fixture 验证零配置 token-meter 与 compact-basic 按依赖顺序加载的路径。
+
+## 考虑过的替代方案
+
+- **把估算保留在 `CompactService` 内**——不予采纳,因为计量拥有独立于压缩的消费方与回放语义;它还会强迫每个压缩器暴露同一套无关 API。
+- **立即把 token meter 拆成接口与启发式后端**——不予采纳,因为目前只有一种实现。单个具体服务保留未来接缝,同时避免推测性的包与配置。
+- **保留模型键控的窗口与密度 profile**——不予采纳,因为当前部署只有一种上下文策略与一个估算器。模型注册表、未知模型错误和可配置密度只增加分支,却没有第二种行为可供选择。
+- **保留独立的标量与表层计量**——不予采纳,因为消费方必须为一次决策执行两次读取并匹配修订号。仅读取标量可以避免在低于阈值时复制节点,但拆分 API 会在消费方引入竞态窗口;统一快照接受 O(surface) 复制成本,以换取结果一致性。
+- **在不同信封之间移用提供方 usage**——不予采纳,因为模型、工具、前缀与调用配置都是请求事实。不匹配时会重新定价完整当前请求。
+
+## 后果
+
+- Token 压力拥有一个可供压缩与未来插件共享的回放感知所有者。
+- 默认值让内置组合只需两个零配置插件条目即可使用;部署需要时只覆盖一个上下文容量。
+- 固定启发式定价仍然只是提供方行为的估计,并不是精确分词器或请求序列化器。
+- 每次计量都会复制当前的位置表层,因此成本为 O(surface),低于阈值即可结束的压力检查也不例外。
+- 遇到畸形持久边界时,计量会明确失败。这会把损坏的回放转化为具名集成错误,而不是让压力静默漂移。
+- post-step 压力检查读取精确记录的路由、工具与前缀边界;对于在成功 usage 锚点出现前就被拒绝的请求,提供方溢出分类仍是由适配器维护的兜底路径。
diff --git a/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml
new file mode 100644
index 0000000000..c19fec81ae
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.i18n.yaml
@@ -0,0 +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-19-zstandard-jsonl-session-logs.md: 09d30594fe31eed138a128dabc1947b15857808d
+2026-07-19-zstandard-jsonl-session-logs.zh.md: 131531d9dba7cb01407191bf937f8b0ee3c6860a
diff --git a/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md
new file mode 100644
index 0000000000..09d30594fe
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.md
@@ -0,0 +1,57 @@
+# Agent Note: Zstandard JSONL session logs
+
+Status: implemented
+
+English | [中文](2026-07-19-zstandard-jsonl-session-logs.zh.md)
+
+## Problem
+
+The JSONL persistence backend keeps every `SessionEvent` verbatim, including high-volume `assistant/chunk` records. Raw text makes logs inspectable but spends storage and I/O on repeated JSON keys and model text. Compression must retain the existing append/fsync commit boundary, collision-safe first materialization, crash repair, and metadata-only listing; rewriting a whole compressed file after every turn would discard those properties.
+
+The encoding also has to remain explicit at the deployment boundary. Snapshot fixtures and external line readers require raw JSONL, while a backend cannot safely guess between compressed and raw artifacts in one root or silently migrate pre-release session data.
+
+## Decision
+
+### Configuration and suffix ownership
+
+`dsh-session-persistence-jsonl` accepts `compression?: 'zstd' | 'none'` and explicitly resolves omission to `'zstd'`. Zstandard artifacts end in `.jsonl.zstd`; `'none'` retains the original newline-delimited UTF-8 `.jsonl` representation. `SessionLocation.kind` remains `'jsonl'`, because both encodings carry the same logical record format, and `SESSION_FORMAT_VERSION` remains `0` under the repository's pre-release reject-without-migration policy.
+
+Each persistence root belongs to one encoding. A one-time discovery preflight rejects any opposite suffix, and targeted load, live-adoption, listing, and materialization paths repeat the relevant suffix check after an initially empty preflight. The error names the incompatible artifact and directs the deployment to the matching configuration or a separate root. There is no migration, dual read, dual write, or extension-based fallback.
+
+### Frame and write path
+
+The compressed artifact is a standard concatenation of independent [Zstandard frames](https://datatracker.ietf.org/doc/html/rfc8878): one checksummed frame containing exactly the header line, followed by one checksummed frame for every durable append batch. Normal loop batches are turn commits, so frame boundaries preserve the existing persistence checkpoint without making the storage layer depend on turn event types.
+
+Compression uses Node's built-in [`zstdCompress` and `zstdDecompress`](https://nodejs.org/download/release/v22.19.0/docs/api/zlib.html), available at the repository's Node 22.19 floor. The backend enables `ZSTD_c_checksumFlag`, otherwise accepts Node's defaults, and exposes neither a compression-level knob nor a new dependency. The API is marked experimental by Node, so the Node 22.19, 24, and 26 compatibility gate exercises the exact helper.
+
+First materialization compresses the two initial frames before opening the temporary file, then keeps the existing write, file `fsync`, collision-safe hard-link publication, and directory `fsync` sequence. Later batches are compressed before opening the destination and appended at EOF. A caught write or file-sync failure truncates to the prior byte length, syncs the rollback, and rethrows so the coordinator can retry the unchanged batch.
+
+### Read, listing, and crash recovery
+
+A frame-boundary scanner reads the standard magic, variable header fields, block headers and payload sizes, and optional checksum trailer. It does not interpret compressed blocks. Complete frames are decompressed independently and sequentially, which validates their checksums, and their plaintext is passed to the existing JSONL scanner. A checksum/decompression failure in any complete frame, a malformed complete-frame JSONL tail, or invalid frame structure is corruption and rejects.
+
+Listing reads in bounded chunks only until the first complete frame is available, validates and decompresses that header frame, and never reads an event frame. The dedicated header frame therefore preserves metadata-only listing even for very large session logs.
+
+EOF inside the final frame is a recoverable torn tail. Node's decoder is given the available frame prefix; every complete newline-terminated event it emits is retained. Repair truncates from that frame's starting byte and appends one new checksummed frame containing the recovered complete events followed by the coordinator's synthetic tool, step, and turn closers. If the tear occurs before any complete event is decodable, repair drops the partial frame and retains all prior complete frames.
+
+### Consumers and verification
+
+The CLI, ACP, and stdio app bundles expose symmetric `persistenceCompression` pass-through configuration. Snapshot recording and replay compositions select `'none'` explicitly because committed fixtures are raw JSONL inputs to replay and normalization; ordinary runtime compositions use the compressed default.
+
+The shared persistence and coordinator contracts run against both encodings. Backend tests cover standard framing and checksum interoperability, header-only listing, append rollback, encoding mismatch rejection, complete-frame corruption, and final-frame tears through headers, blocks, and checksum trailers. Default runtime, built-bin, headless, ACP, and Python smokes assert the compressed suffix and Zstandard magic or decode the header; raw-content tests opt out explicitly.
+
+## Alternatives considered
+
+- **One frame per JSONL record** — rejected because it multiplies frame headers and checksums for high-volume chunk events and makes a physical boundary unrelated to the durable append batch.
+- **Rewrite one whole compressed stream after every append** — rejected because cost grows with log size and replacement would give up append/fsync rollback and the established collision-safe materialization mechanics.
+- **Use a streaming compressor across appends** — rejected because an interrupted encoder state does not leave independently checksummed append units, complicating bounded listing and frame-start repair.
+- **Add an external native Zstandard dependency** — rejected because the supported Node floor already provides the required codec; another native artifact would enlarge installation and executable-packaging risk without adding a required behavior.
+- **Expose compression level or keep raw JSONL as the default** — rejected because there is no deployment evidence for a second tuning policy, while `'none'` preserves the line-readable path for fixtures and integrations that need it.
+
+## Consequences
+
+- Ordinary session roots store `.jsonl.zstd` and retain append-only, fsync, rollback, and interrupted-turn recovery semantics.
+- Raw JSONL remains a deliberate configuration, but changing encoding requires a fresh/separate root or selecting the mode that matches existing artifacts.
+- One frame per durable batch adds bounded framing/checksum overhead and allows header-only listing plus repair from an exact append boundary.
+- External tools must understand concatenated Zstandard frames or consume raw-mode artifacts; generic one-shot Node decompression reads only the first independent frame, so backend reads walk frames explicitly.
+- The implementation depends on Node's experimental built-in Zstandard API without an npm dependency; the supported-version compatibility gate makes drift visible.
diff --git a/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md
new file mode 100644
index 0000000000..131531d9db
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-07-19-zstandard-jsonl-session-logs.zh.md
@@ -0,0 +1,57 @@
+# Agent Note: Zstandard JSONL 会话日志
+
+Status: implemented
+
+[English](2026-07-19-zstandard-jsonl-session-logs.md) | 中文
+
+## 问题
+
+JSONL 持久化后端会逐字保留每个 `SessionEvent`,其中包括数量庞大的 `assistant/chunk` 记录。原始文本便于检查,但重复的 JSON 键和模型文本会增加存储与 I/O 开销。压缩编码必须保留既有的 append/fsync 提交边界、首次物化时的无冲突发布、崩溃修复以及仅元数据列举;如果每轮都重写整个压缩文件,就会失去这些属性。
+
+编码还必须在部署边界上保持显式。快照 fixture 与外部逐行读取器需要原始 JSONL,而后端无法在同一根目录中安全猜测压缩产物与原始产物,也不能静默迁移预发布会话数据。
+
+## 决策
+
+### 配置与后缀归属
+
+`dsh-session-persistence-jsonl` 接受 `compression?: 'zstd' | 'none'`,并将省略值显式解析为 `'zstd'`。Zstandard 产物使用 `.jsonl.zstd` 后缀;`'none'` 保留原有的换行分隔 UTF-8 `.jsonl` 表示。`SessionLocation.kind` 仍为 `'jsonl'`,因为两种编码承载同一逻辑记录格式;按照仓库的预发布拒绝且不迁移策略,`SESSION_FORMAT_VERSION` 仍为 `0`。
+
+每个持久化根目录只归属于一种编码。一次性的发现预检会拒绝任何相反后缀,而针对性的加载、活跃采用、列举与物化路径会在最初空目录预检之后再次执行对应后缀检查。错误会指出不兼容产物,并要求部署选择匹配配置或单独根目录。系统不提供迁移、双重读取、双重写入或基于扩展名的兜底。
+
+### 帧与写入路径
+
+压缩产物是标准独立 [Zstandard 帧](https://datatracker.ietf.org/doc/html/rfc8878)的串联:第一个带校验和的帧只包含头部行,后续每个持久追加批次各占一个带校验和的帧。正常 agent loop 批次就是轮次提交,因此帧边界保留既有持久化检查点,同时不让存储层依赖轮次事件类型。
+
+压缩使用 Node 内置的 [`zstdCompress` 与 `zstdDecompress`](https://nodejs.org/download/release/v22.19.0/docs/api/zlib.html),仓库最低支持的 Node 22.19 已提供这些 API。后端启用 `ZSTD_c_checksumFlag`,其余采用 Node 默认值,不公开压缩级别调节项,也不增加依赖。Node 将该 API 标记为实验性,因此 Node 22.19、24 与 26 兼容性门禁会执行同一个辅助实现。
+
+首次物化会在打开临时文件之前压缩两个初始帧,然后保留既有的写入、文件 `fsync`、避免冲突的硬链接发布与目录 `fsync` 顺序。后续批次也会先压缩,再打开目标并在 EOF 追加。捕获到写入或文件同步失败时,后端会截断到原有字节长度,同步回滚结果,再重新抛出错误,让协调器重试未变化的批次。
+
+### 读取、列举与崩溃恢复
+
+帧边界扫描器会读取标准魔数、可变头字段、块头与负载长度,以及可选校验和尾部,但不会解释压缩块。后端独立且按顺序解压完整帧,由此验证各帧校验和,再把明文交给既有 JSONL 扫描器。任何完整帧的校验和或解压失败、完整帧中畸形的 JSONL 尾部,或者无效帧结构都属于损坏并拒绝加载。
+
+列举只按有界分片读取到第一个完整帧可用为止,验证并解压该头部帧,绝不读取事件帧。因此,即使会话日志很大,专用头部帧仍能维持仅元数据列举。
+
+最终帧内部遇到 EOF 属于可恢复的撕裂尾部。后端把已有帧前缀交给 Node 解码器,并保留其产出的每个完整、以换行结束的事件。修复从该帧起始字节截断,再追加一个新的带校验和帧,其中依次包含恢复出的完整事件,以及协调器生成的工具、步骤与轮次闭合事件。如果撕裂位置尚不足以解码任何完整事件,修复会丢弃该不完整帧并保留此前全部完整帧。
+
+### 消费方与验证
+
+CLI、ACP 与 stdio 应用包公开对称的 `persistenceCompression` 透传配置。快照录制与回放组合显式选择 `'none'`,因为提交的 fixture 是回放与规范化过程使用的原始 JSONL 输入;普通运行时组合使用压缩默认值。
+
+共享持久化契约与协调器契约会针对两种编码运行。后端测试覆盖标准帧与校验和互操作性、仅头部列举、追加回滚、编码不匹配拒绝、完整帧损坏,以及横跨头部、块和校验和尾部的最终帧撕裂。默认运行时、构建后二进制、headless、ACP 与 Python 冒烟测试会断言压缩后缀与 Zstandard 魔数,或解码头部;读取原始内容的测试则显式退出压缩。
+
+## 考虑过的替代方案
+
+- **每条 JSONL 记录一个帧**——不予采纳,因为它会让大量分片事件各自承担帧头与校验和开销,并让物理边界脱离持久追加批次。
+- **每次追加都重写一个完整压缩流**——不予采纳,因为成本会随日志大小增长,而且替换操作会放弃追加/fsync 回滚和既有的无冲突物化机制。
+- **跨追加使用流式压缩器**——不予采纳,因为编码器状态中断后不会留下可独立校验的追加单元,从而使有界列举与按帧起点修复更复杂。
+- **增加外部原生 Zstandard 依赖**——不予采纳,因为受支持的 Node 最低版本已经提供所需编解码器;另一个原生产物会增加安装与可执行文件打包风险,却不增加必需行为。
+- **公开压缩级别或继续默认使用原始 JSONL**——不予采纳,因为没有部署证据支持第二种调节策略,而 `'none'` 已为需要逐行读取的 fixture 与集成保留路径。
+
+## 后果
+
+- 普通会话根目录存储 `.jsonl.zstd`,并保留仅追加、fsync、回滚与中断轮次恢复语义。
+- 原始 JSONL 仍是显式配置,但切换编码需要使用全新或单独根目录,或者选择与既有产物匹配的模式。
+- 每个持久批次一个帧会增加有界的帧与校验和开销,同时支持仅头部列举和从精确追加边界开始修复。
+- 外部工具必须理解串联的 Zstandard 帧,或者消费原始模式产物;Node 通用的一次性解压只读取第一个独立帧,因此后端读取会显式遍历各帧。
+- 实现依赖 Node 的实验性内置 Zstandard API,但不增加 NPM 依赖;受支持版本兼容性门禁会暴露 API 漂移。
diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml
new file mode 100644
index 0000000000..4798f54960
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.i18n.yaml
@@ -0,0 +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-20-error-cause-chain-diagnostics.md: 391e35997bb1bb050dd2ca620920961d77bb1c46
+2026-07-20-error-cause-chain-diagnostics.zh.md: 90d6559a9410e8a4e5475db9560a2a177ba7a1a7
diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md
new file mode 100644
index 0000000000..391e35997b
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.md
@@ -0,0 +1,37 @@
+# Agent Note: Render error cause chains at every diagnostic seam
+
+Status: implemented
+
+English | [中文](2026-07-20-error-cause-chain-diagnostics.zh.md)
+
+## Problem
+
+A TUI run against an unreachable DeepSeek endpoint failed with the single notice `fetch failed` and no further detail. Two independent gaps produced that dead end:
+
+1. undici's `fetch` wraps every transport failure (DNS, refused connection, TLS, proxy) in a bare `TypeError: fetch failed` whose actionable detail — `ECONNREFUSED`, `bad port`, the Happy Eyeballs AggregateError — lives on `error.cause`. Every diagnostic seam in the harness rendered only `error.message` (or `String(error)`, which is equivalent for Errors), so the wrapper masked the diagnosis in the TUI notice, the durable `turn/end` reason, and every logger line.
+2. The readline front door (`dsh-stdio`) rendered no failure reason at all: a `turn/end` with `reason.kind === 'error'` printed nothing but the next `> ` prompt, so the same failure in `demo:repl` was pure silence.
+
+## Decision
+
+- `dsh-llm` exports `errorChain(value)`: renders a thrown value with its full `cause` chain (`outer: inner: …`) and AggregateError members (`msg [m1; m2]`), with circular-cause and hostile-coercion containment. It is a diagnostic-surface renderer only; routing stays on `HarnessError.code`.
+- The DeepSeek adapter wraps a pre-response transport failure in `LlmError('TRANSPORT')` naming the configured `baseURL` and chaining the original rejection as `cause`. An aborted request becomes `LlmError('ABORTED')`; because the turn signal is already aborted, the loop still classifies the turn as cancellation rather than recovery.
+- Every diagnostic seam renders through `errorChain` instead of `error.message`/`String(error)`: the agent-loop's durable `turn/end` error message (`errorData`), its logger warnings, the TUI's `agent/error` notice and startup-failure line, and `dsh-stdio`'s startup-failure log lines. The per-package `renderThrown` copies in `dsh-agent-loop`, `dsh-stdio`, and `dsh-tui` are deleted in favor of the one shared renderer.
+- `dsh-stdio` renders failure `turn/end` reasons: `[turn failed ] `, `[turn aborted] `, `[turn rejected] `, `[turn interrupted by a previous process exit]`, and the output-token-limit notice. Unknown merge-extended kinds fall through as ordinary turn ends.
+
+`errorChain` lives in `dsh-llm` beside `HarnessError` for the same reason the base class does: it is the leaf package every consumer already imports, so sharing costs no new dependency edge.
+
+## Alternatives considered
+
+**Chain rendering inside each error's constructor (bake the cause into `message`).** Rejected: it double-renders once consumers also walk `cause` (the first draft of the adapter fix produced `… fetch failed: bad port: fetch failed: bad port`), and it destroys the structured chain for consumers that want to route on the inner error.
+
+**A `cause`-aware logger exporter only.** Rejected: the durable `turn/end` reason and the TUI notice are not logger lines; the masked message would persist in the session log — the single durable record of an in-turn failure — and in the primary UI surface.
+
+**Per-package `renderThrown` upgrades.** Rejected: three packages already carried near-identical private copies; upgrading each separately entrenches the duplication the shared renderer removes.
+
+## Consequences
+
+- A transport failure now reads `DeepSeek API request to failed: fetch failed: connect ECONNREFUSED …` in the TUI notice, the readline transcript, and the persisted session log, at the cost of longer diagnostic strings.
+- Durable `turn/end` error messages include cause detail. Existing snapshot fixtures replay byte-identically because their scripted errors carry no `cause` (for such errors `errorChain(err)` equals `err.message`); only unit-test expectation strings changed. A fixture recorded from a real transport failure would carry the chain.
+- `errorChain` renders `message` without the class name (`String(error)` rendered `Error: `), so a bare `TypeError` in a log line loses its type label unless its message is empty (then the name is the fallback). The chain detail was judged worth more than the class name at these seams.
+- `dsh-stdio` output for failed turns is no longer silent; piped consumers that parsed the transcript see new `[turn …]` lines.
+- Remaining `renderThrown` copies in `dsh-subagent`, `dsh-workflow`, `dsh-skill`, `dsh-workflow-workerthread`, and `cli-demo` still render without the chain; they wrap package-local errors that carry their own messages, and can adopt `errorChain` when their diagnostics prove insufficient.
diff --git a/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md
new file mode 100644
index 0000000000..90d6559a94
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-07-20-error-cause-chain-diagnostics.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 在每个诊断接缝处渲染错误 cause 链
+
+Status: implemented
+
+[English](2026-07-20-error-cause-chain-diagnostics.md) | 中文
+
+## Problem
+
+TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed` 通知,没有任何进一步细节。两个独立缺口共同造成了这个死胡同:
+
+1. undici 的 `fetch` 把所有传输层失败(DNS、连接被拒、TLS、代理)包装成裸的 `TypeError: fetch failed`,可操作的细节——`ECONNREFUSED`、`bad port`、Happy Eyeballs 的 AggregateError——都在 `error.cause` 上。harness 里的每个诊断接缝都只渲染 `error.message`(或对 Error 等价的 `String(error)`),于是包装层在 TUI 通知、持久化的 `turn/end` reason 和所有日志行里都掩盖了诊断信息。
+2. readline 前门(`dsh-stdio`)完全不渲染失败原因:`reason.kind === 'error'` 的 `turn/end` 只打印下一个 `> ` 提示符,同样的失败在 `demo:repl` 里就是纯粹的沉默。
+
+## Decision
+
+- `dsh-llm` 导出 `errorChain(value)`:渲染抛出值及其完整 `cause` 链(`outer: inner: …`)与 AggregateError 成员(`msg [m1; m2]`),并容错循环 cause 和恶意强制转换。它只是诊断表面的渲染器;路由仍然基于 `HarnessError.code`。
+- DeepSeek 适配器把拿到响应之前的传输失败包装成 `LlmError('TRANSPORT')`,写明配置的 `baseURL` 并把原始拒绝值链为 `cause`。被中止的请求变为 `LlmError('ABORTED')`;由于轮次信号已处于中止状态,循环仍将该轮次归类为取消而非恢复。
+- 每个诊断接缝改用 `errorChain` 而非 `error.message`/`String(error)`:agent-loop 的持久化 `turn/end` 错误消息(`errorData`)、其日志警告、TUI 的 `agent/error` 通知与启动失败行、以及 `dsh-stdio` 的启动失败日志行。`dsh-agent-loop`、`dsh-stdio`、`dsh-tui` 里各自的 `renderThrown` 副本被删除,统一使用这一个共享渲染器。
+- `dsh-stdio` 渲染失败的 `turn/end` reason:`[turn failed ] `、`[turn aborted] `、`[turn rejected] `、`[turn interrupted by a previous process exit]` 以及输出 token 上限通知。未知的 merge 扩展 kind 按普通 turn 结束处理。
+
+`errorChain` 与 `HarnessError` 一样放在 `dsh-llm` 里,理由相同:它是每个消费者都已导入的叶子包,共享不增加新的依赖边。
+
+## Alternatives considered
+
+**在每个错误的构造函数里渲染链(把 cause 烤进 `message`)。** 否决:当消费者同时遍历 `cause` 时会双重渲染(适配器修复的第一版产出了 `… fetch failed: bad port: fetch failed: bad port`),并且破坏了想按内层错误路由的消费者所需的结构化链。
+
+**只做一个感知 `cause` 的日志导出器。** 否决:持久化的 `turn/end` reason 和 TUI 通知不是日志行;被掩盖的消息会留在会话日志——回合内失败的唯一持久记录——以及主要 UI 表面里。
+
+**逐包升级 `renderThrown`。** 否决:三个包已经各自持有几乎相同的私有副本;分别升级只会固化共享渲染器所要消除的重复。
+
+## Consequences
+
+- 传输失败现在在 TUI 通知、readline transcript 和持久化会话日志里显示为 `DeepSeek API request to failed: fetch failed: connect ECONNREFUSED …`,代价是更长的诊断字符串。
+- 持久化的 `turn/end` 错误消息包含 cause 细节。现有 snapshot fixture 字节级一致地回放,因为其脚本化错误不带 `cause`(对这类错误 `errorChain(err)` 等于 `err.message`);只有单元测试的期望字符串有变化。从真实传输失败录制的 fixture 会携带完整链。
+- `errorChain` 渲染 `message` 而不带类名(`String(error)` 会渲染 `Error: `),因此日志行里的裸 `TypeError` 会丢失类型标签,除非消息为空(此时回退到类名)。在这些接缝上,链细节被判断为比类名更有价值。
+- `dsh-stdio` 对失败回合的输出不再沉默;解析 transcript 的管道消费者会看到新的 `[turn …]` 行。
+- `dsh-subagent`、`dsh-workflow`、`dsh-skill`、`dsh-workflow-workerthread`、`cli-demo` 里剩余的 `renderThrown` 副本仍不渲染链;它们包装的是自带消息的包内错误,等诊断信息证明不足时再采用 `errorChain`。
diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md b/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
similarity index 94%
rename from docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md
rename to .agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
index bfd0e6a10e..7c47fc78e9 100644
--- a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md
+++ b/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
@@ -1,4 +1,4 @@
-# RFC: Agent Client Protocol (ACP) support — drive the coding agent from external editors
+# Agent Note: Agent Client Protocol (ACP) support — drive the coding agent from external editors
Status: implemented
@@ -48,7 +48,7 @@ The precise supported and deferred protocol rows live in [`packages/ui/acp/acp-f
Editors can create, load, prompt, cancel, render, ask, and reconfigure multiple harness sessions over one ACP connection without a loop-specific dependency. The session event log remains the durable source for replay, prompt settlement, cwd, and per-session configuration. Tool presentation and human-answer channels remain extensible plugin contracts instead of ACP-specific behavior.
-The bridge deliberately does not implement session list/delete/resume/close capabilities, MCP passthrough, additional directories, image/audio/embedded-resource prompts, runtime model selection, plans, slash commands, usage updates, editor filesystem delegation, or the ACP terminal execution sub-protocol. The feature checklist records these as unsupported rather than silently accepting them.
+The bridge deliberately does not implement session list/delete/resume/close capabilities, MCP passthrough, additional directories, image/audio/embedded-resource prompts, plans, slash commands, usage updates, editor filesystem delegation, or the ACP terminal execution sub-protocol. Runtime model selection was added later through standard session config options by the [LLM catalog and ACP selection Agent Note](../architecture/2026-07-15-llm-model-catalog-and-acp-selection.md).
An idle config selection is truthful in the live response but not durable until the next `agent/prompt-submit` anchors it inside the open turn. Crashing before that boundary loses the pending selection; this is the cost of keeping session events turn-enclosed and replay-safe.
diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md b/.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md
similarity index 76%
rename from docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md
rename to .agents/notes/implemented/feature/2026-06-14-acp-multi-session.md
index 77fa2b4669..def0604df9 100644
--- a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md
+++ b/.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md
@@ -1,4 +1,4 @@
-# RFC: Multiplex concurrent ACP sessions over one connection
+# Agent Note: Multiplex concurrent ACP sessions over one connection
Status: implemented
@@ -8,11 +8,11 @@ An ACP editor can keep several conversations alive over one agent subprocess. A
## Decision
-The ACP bridge stores live sessions in `Map` and keeps a `WeakMap` reverse index for agent-scoped callbacks. A record owns its agent handle, in-flight prompt, live tool-call presentation state, pending idle config switches, session cwd, and client capability snapshot. A separate loading-id set reserves each id before asynchronous resume so two pipelined loads cannot construct duplicate agents; distinct ids may load concurrently.
+The ACP bridge stores live sessions in `Map`. Agent-scoped callbacks use `ownedRecord`: look up `agent.session.id` in that forward map and accept the record only when it owns the exact agent object, so a foreign same-id object cannot claim the session. A record owns its agent handle, in-flight prompt, live tool-call presentation state, pending idle config switches, session cwd, and client capability snapshot. A separate loading-id set reserves each id before asynchronous resume so two pipelined loads cannot construct duplicate agents; distinct ids may load concurrently.
Every `session/event` and `agent/status` callback resolves the owning record before sending or settling anything. Each session permits one in-flight prompt independently. The prompt records a log watermark, captures its own `turn/start`, and settles only on the matching `turn/end`; a late end from a cancelled prior turn cannot resolve a newer prompt. `session/cancel` addresses one record and calls only that agent's queue-aware cancel path.
-Permission ownership uses the same reverse index. The ACP `approval/request` answerer prompts only the editor session that owns the requesting agent and delegates foreign requests. User-interaction elicitations likewise route by agent ownership. Per-session sandbox and approval config values fold only that session's events, with pending idle switches stored on that record until the next turn anchors them.
+Permission ownership uses the same exact-agent check against the forward map. The ACP `approval/request` answerer prompts only the editor session that owns the requesting agent and delegates foreign requests. User-interaction elicitations likewise route by agent ownership. Per-session sandbox and approval config values fold only that session's events, with pending idle switches stored on that record until the next turn anchors them.
Background bash tasks carry an opaque owner token equal to the owning session id. `bash_output` and `bash_kill` compare the caller's token with the executor's task ownership before reading or killing; a predictable task id alone grants no access. Ownership is stored with the executor task, so a tool plugin reload does not erase it.
diff --git a/docs/rfc/implemented/feature/2026-06-15-code-mode.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.md
similarity index 89%
rename from docs/rfc/implemented/feature/2026-06-15-code-mode.md
rename to .agents/notes/implemented/feature/2026-06-15-code-mode.md
index 3bc07b839f..329eaf2d0a 100644
--- a/docs/rfc/implemented/feature/2026-06-15-code-mode.md
+++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.md
@@ -1,23 +1,23 @@
-# RFC: Code Mode — the model writes TypeScript against the tool registry
+# Agent Note: Code Mode — the model writes TypeScript against the tool registry
Status: implemented
## Problem
-In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and the loop dispatches each call through `ctx.tools.execute()` **sequentially** (parallel tool execution is an explicit open TODO in `dsh-tools` and [docs/architecture.md](../../../architecture.md)), with **every** intermediate `tool-result` re-entering the model's context on the next request.
+In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and the loop dispatches each call through `ctx.tools.execute()` **sequentially** (parallel tool execution is an explicit open TODO in `dsh-tools` and [docs/architecture.md](../../../../docs/architecture.md)), with **every** intermediate `tool-result` re-entering the model's context on the next request.
For multi-step tool work this is token-heavy and serial. The model cannot compose tools — loop over a result set, branch on an intermediate value, fan out, post-process — without a full model round-trip per call, and each round-trip drags the entire intermediate result back into context whether the model needs it or not.
Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) proposes an alternative grounded in a simple observation: LLMs are better at writing code than at emitting tool calls, because they have seen millions of lines of real code and comparatively few contrived tool-calling traces. Instead of one tool call per step, the model writes a TypeScript program against a generated API over the tools, the program executes in a sandboxed runtime, and the model curates what comes back — only what it prints or returns — instead of every intermediate result.
-Tool presentation belongs to the registry that owns tool visibility: implementing a second presentation as an after-the-fact waterfall transform would make correctness depend on listener order and fight [reconstructable requests](../../implemented/architecture/2026-07-05-reconstructable-requests.md). The execution substrate is also part of the foundation rather than a placeholder: Node `worker_threads` provides a separate isolate, an empty environment, heap caps, and termination of a hot synchronous loop, while fitting the harness's existing trust model (§Trust posture).
+Tool presentation belongs to the registry that owns tool visibility: implementing a second presentation as an after-the-fact waterfall transform would make correctness depend on listener order and fight [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md). The execution substrate is also part of the foundation rather than a placeholder: Node `worker_threads` provides a separate isolate, an empty environment, heap caps, and termination of a hot synchronous loop, while fitting the harness's existing trust model (§Trust posture).
## Decision
Three decisions, each elaborated in its own section below:
1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
-2. **Code execution is a capability seam** — `packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../../implemented/architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign.
+2. **Code execution is a capability seam** — `packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign.
3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority.
### The registry owns the mode
@@ -46,7 +46,7 @@ Under `'code'` and `'both'` the registry owns `run_code` as a reserved presentat
**Concurrency is serialized.** Each run owns a dispatch queue, so even `Promise.all` executes tool calls in submission order. Settlement abandons queued calls that have not started. Parallelism requires per-tool concurrency-safety metadata.
-**Presentation.** `run_code`'s render intent is decided here per the [render-intent RFC](../../implemented/architecture/2026-07-02-tool-render-intent-union.md): `presentCall` → a `generic` card, `kind: 'execute'`, title = the program text, `rawInput` = the same program text; `presentResult` → a `generic` card whose content is the captured output (from `meta`). The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not.
+**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` → a `generic` card, `kind: 'execute'`, title = the program text, `rawInput` = the same program text; `presentResult` → a `generic` card whose content is the captured output (from `meta`). The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not.
### Observability: `tool/code-dispatch`
@@ -60,7 +60,7 @@ Each sub-dispatch appends a log-only `tool/code-dispatch` event containing paren
- `CodeBindingNamespace = { global: string; functions: Record Promise> }` — the runtime exposes each namespace as a global object of async functions inside the program; binding arguments and resolutions must be structured-cloneable (a runtime may cross a serialization boundary; ours does).
- `CodeRunResult = { value?: unknown; logs: CodeLogEntry[]; error?: CodeRunFailure }` — program execution outcomes, including exception, timeout, abort, and worker exit, resolve as the `error` field. `run()` may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary.
- `CodeLogEntry = { source: 'console' | 'stdout' | 'stderr'; level?: 'log' | 'info' | 'warn' | 'error' | 'debug'; text: string }`
-- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout.
+- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../../docs/defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout.
- Two readonly backend descriptors, informational not gating: `language` (what the program must be written in — `'typescript'` for the shipped backend; a Python backend would say so, and pair with its own SDK generator on the presentation side) and `isolation` (`'worker-thread'` for the shipped backend; `'process'`, `'container'`, … for future ones). `dsh-tools` requires `language === 'typescript'` in the MVP — its codegen emits TS — and fails the assembly loudly otherwise, the same misconfiguration idiom as `toolOrder` violations (as when `mode` is non-native with no `ctx.codeRuntime` loaded at all).
Requests contain every runtime input; implementations own validated timeout and cap defaults. The registry looks up the optional runtime only when Code Mode is assembled, so native mode does not depend on one. Missing or language-incompatible runtimes fail loudly. Alternate substrates or languages can replace the implementation behind the same seam, paired with the appropriate SDK generator.
@@ -74,7 +74,7 @@ Requests contain every runtime input; implementations own validated timeout and
3. **Execute** in the bootstrap: the stripped program becomes the body of an `AsyncFunction` whose parameters are the binding globals and a capturing `console` shim, so top-level `await` and `return` work and the program's completion value is the run's `value` (structured-cloneable values cross as-is; anything else is replaced by its `util.inspect` rendering, documented).
4. **Bridge bindings over the message port**: each binding function in the worker posts `{ id, global, name, args }` and awaits the reply; the host validates the name against the request's bindings, invokes, and replies `{ id, ok, value }` or `{ id, ok: false, message }` (a host-side binding rejection becomes a program-side rejection). The worker-side namespace objects are built null-prototype via `defineProperty`, so a binding named `__proto__`, `constructor`, or `toString` is an ordinary own property, not a prototype collision. Unknown names, duplicate ids, and post-settlement messages are rejected or ignored — the port protocol assumes a hostile peer, because the peer runs model code.
5. **Enforce independent budgets.** `computeMs` meters worker busy time, allowing slow awaited tools without excusing a hot loop. `maxWallMs` bounds total elapsed time, including unresolved waits. Expiry, cancellation, and completion terminate the worker. Heap exits and truncation are reported explicitly; compute, wall, heap, log, and return-value caps are validated configuration.
-6. **Dispose to quiescence**: the service's own disposal terminates in-flight workers and *awaits* their exits before resolving, per [defensive patterns](../../../defensive-patterns.md).
+6. **Dispose to quiescence**: the service's own disposal terminates in-flight workers and *awaits* their exits before resolving, per [defensive patterns](../../../../docs/defensive-patterns.md).
### Trust posture
@@ -97,7 +97,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
## Alternatives considered
-**An add-on consumer plugin with zero core changes.** Rejected because `agent/request` is call-config-only under [reconstructable requests](../../implemented/architecture/2026-07-05-reconstructable-requests.md), while transforming an assembled tool list would have to undo `toolOrder` canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store.
+**An add-on consumer plugin with zero core changes.** Rejected because `agent/request` is call-config-only under [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md), while transforming an assembled tool list would have to undo `toolOrder` canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store.
**`node:vm` as the reference runtime, with hardening deferred.** Rejected: `node:vm` is not isolation (prototype-chain escapes reach the host realm) and cannot interrupt a hot loop. A worker thread provides a separate isolate, empty environment, `resourceLimits`, and reliable `terminate()` at bash-equivalent trust, so the reference and production implementation are one package without an unsafe-acknowledgement ceremony.
@@ -119,7 +119,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
**`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite pins the behaviors relied on (position preservation, erasable-only rejection message shape loosely), the call sits behind one private function, and `amaro`/`sucrase` are drop-in replacements if the API shifts. The erasable-only subset is a model-facing contract line, and the error path is a working feedback loop, not a dead end.
-**Prompt cost of the SDK, especially under `'both'`.** The `.d.ts` can rival the native schemas it complements; `'both'` carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the RFC makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning.
+**Prompt cost of the SDK, especially under `'both'`.** The `.d.ts` can rival the native schemas it complements; `'both'` carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning.
**Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Contained by module boundaries inside the package (`ts-types.ts`, `code-mode.ts` beside `schema.ts`/`json-schema.ts`/`presentation.ts`) and by the seam: everything substrate-shaped lives behind `ctx.codeRuntime`.
diff --git a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md b/.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md
similarity index 86%
rename from docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md
rename to .agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md
index adad17472d..a318956ddf 100644
--- a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md
+++ b/.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md
@@ -1,12 +1,12 @@
-# RFC: Filesystem tool schemas — model-facing read/write/edit shapes
+# Agent Note: Filesystem tool schemas — model-facing read/write/edit shapes
Status: implemented
## Problem
-[The filesystem capability-seam RFC](../architecture/2026-06-17-filesystem-capability-seam.md) defines the filesystem capability seam (`ctx.fs`), the package split (`dsh-fs`, `dsh-fs-local`, `dsh-tool-fs`, plus the `dsh-fs-policy` policy plugin), and the observed-file/stale-version policy for read-before-write/edit checks — which the [split-fs-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](../architecture/2026-06-26-file-context-as-event-gate.md) RFCs moved off `ctx.fs` into the `dsh-fs-policy` plugin on the `fs/*` event gate. The remaining decision for the first filesystem tool delivery is the model-facing schema surface: what arguments the model sees for `read`, `write`, and `edit`.
+[The filesystem capability-seam Agent Note](../architecture/2026-06-17-filesystem-capability-seam.md) defines the filesystem capability seam (`ctx.fs`), the package split (`dsh-fs`, `dsh-fs-local`, `dsh-tool-fs`, plus the `dsh-fs-policy` policy plugin), and the observed-file/stale-version policy for read-before-write/edit checks — which the [split-fs-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](../architecture/2026-06-26-file-context-as-event-gate.md) Agent Notes moved off `ctx.fs` into the `dsh-fs-policy` plugin on the `fs/*` event gate. The remaining decision for the first filesystem tool delivery is the model-facing schema surface: what arguments the model sees for `read`, `write`, and `edit`.
-The schema should be small enough to implement in the first `dsh-tool-fs` pass, but stable enough that future local/remote/sandboxed filesystem backends do not require model-facing churn. It should also avoid importing every option from reference systems. Claude Code and OpenCode expose similar core file tools but differ in naming style and extra flags; this RFC chooses the minimal shared surface for the prototype.
+The schema should be small enough to implement in the first `dsh-tool-fs` pass, but stable enough that future local/remote/sandboxed filesystem backends do not require model-facing churn. It should also avoid importing every option from reference systems. Claude Code and OpenCode expose similar core file tools but differ in naming style and extra flags; this Agent Note chooses the minimal shared surface for the prototype.
## Decision
@@ -103,8 +103,8 @@ Schema tests pin the required/optional argument set per tool, empty-`old_string`
## Consequences
-**The first schema is intentionally smaller than Claude Code's.** Dropping PDF pages, multimodal read, rich grep/list flags, and expected hash fields keeps the implementation focused, but users may ask for those quickly. They arrive as separate RFCs or focused follow-ups rather than overloads of the initial schema.
+**The first schema is intentionally smaller than Claude Code's.** Dropping PDF pages, multimodal read, rich grep/list flags, and expected hash fields keeps the implementation focused, but users may ask for those quickly. They arrive as separate Agent Notes or focused follow-ups rather than overloads of the initial schema.
**No explicit model-facing stale guard in v1.** The schema does not ask the model to provide an expected hash/version. That is intentional: stale checks come from backend-produced versions and the `dsh-fs-policy` plugin's observed state, not from fragile model-copied tokens. Filesystem safety failures surface through structured `FsError` codes owned by `dsh-fs`, not through model-supplied version fields.
-**Naming becomes public surface.** Once shipped, changing `file_path` to `filePath` or `old_string` to `oldString` would churn prompts, examples, and downstream clients. This RFC chooses snake_case up front and treats it as the stable model-facing contract.
+**Naming becomes public surface.** Once shipped, changing `file_path` to `filePath` or `old_string` to `oldString` would churn prompts, examples, and downstream clients. This Agent Note chooses snake_case up front and treats it as the stable model-facing contract.
diff --git a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md b/.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
similarity index 90%
rename from docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
rename to .agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
index 3b8cd810a8..8d9c0197eb 100644
--- a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
+++ b/.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
@@ -1,10 +1,10 @@
-# RFC: Rich ACP bash rendering — the terminal card via the `_meta` convention
+# Agent Note: Rich ACP bash rendering — the terminal card via the `_meta` convention
Status: implemented
## Problem
-The ACP bridge lets each tool own its call rendering via `presentCall`/`presentResult` (see [tool-call UI presentation](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) and `packages/core/tools`). For `bash` we surface the exact command as the `tool_call` title, the model's `description` as a content text block, `kind: 'execute'`, and the completed output wrapped in a fenced ` ```console ` text block.
+The ACP bridge lets each tool own its call rendering via `presentCall`/`presentResult` (see [tool-call UI presentation](2026-06-14-acp-agent-client-protocol.md) and `packages/core/tools`). For `bash` we surface the exact command as the `tool_call` title, the model's `description` as a content text block, `kind: 'execute'`, and the completed output wrapped in a fenced ` ```console ` text block.
Reference editors render terminal metadata as a dedicated card with cwd, command, live-style output, and exit status; plain text loses that structure. The command is the title because execute cards hide raw input, while the human-readable description remains a separate block above the card.
@@ -43,4 +43,4 @@ Keep `dsh-bash` agent-side execution; render the terminal card via the `_meta` c
## Out of scope / non-goals
-The text-block baseline stays the no-capability default. Two follow-ups are deliberately NOT built here and would each warrant their own RFC when someone takes them on: **live incremental streaming** (`_meta.terminal_output_delta` as chunks arrive, which needs an incremental-output seam on `dsh-bash`), and **command classification** (parsing a `cat`/`sed` as a `read` card with a file location, a `grep` as a `search`, etc., falling back to the terminal card — display-only, must never change what executes).
+The text-block baseline stays the no-capability default. Two follow-ups are deliberately NOT built here and would each warrant their own Agent Note when someone takes them on: **live incremental streaming** (`_meta.terminal_output_delta` as chunks arrive, which needs an incremental-output seam on `dsh-bash`), and **command classification** (parsing a `cat`/`sed` as a `read` card with a file location, a `grep` as a `search`, etc., falling back to the terminal card — display-only, must never change what executes).
diff --git a/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md b/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
new file mode 100644
index 0000000000..99313866ed
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
@@ -0,0 +1,126 @@
+# Agent Note: Compaction as a capability seam (abstract contract + basic backend)
+
+Status: implemented
+
+## Problem
+
+A long-running agent conversation grows without bound. As the event log accumulates turns, the derived message history eventually approaches the model's context window — the model then truncates mid-response (`max-tokens`) or degrades. **Compaction** is the mitigation: replace a run of older history with a concise summary, keeping recent context intact.
+
+The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` recording provenance so the decision replays deterministically. What remained was the plugin that *decides what to compact and produces the summary*.
+
+Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to five event types (`user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler rejects `surfaceOp` on it and the invariants plugin rejects it at runtime.
+
+## Decision
+
+### Compaction is a capability seam, split interface / implementation
+
+Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md), compaction ships as separate packages so the contract, the algorithm, and (later) the consumer surface evolve independently:
+
+1. **Interface** — `@deepseek-ai/dsh-compact`: an abstract `CompactService` owning the `ctx.compact` key, the `CompactionResult` vocabulary, and the `compact/*` session events. It declares `compactIfNeeded()` and `compactRegion()` as **abstract** — the contract states *what* compaction does, not *how*.
+2. **Implementation** — `@deepseek-ai/dsh-compact-basic`: a concrete `BasicCompactService` that consumes `ctx.tokenMeter` and owns the tail→head retention walk, summarization via `ctx.llm.stream()`, the surface replacement, the lock, post-step pressure, and canonical context-overflow recovery. `summarize()` is its sole subclass hook; pricing and replay stay with the meter.
+3. **Model-free companion** — `@deepseek-ai/dsh-compact-tool-result-prune`: a concrete optional service that rewrites oversized current `tool/result` nodes before the backend selects a summary range. It is not a second compaction implementation and does not implement `CompactService`.
+4. **Consumer** — deferred. A `/compact` tool and slash command will `inject: ['compact']` and call the contract; they are intentionally out of scope here so the seam settles first.
+
+### The contract depends on `dsh-session` and `dsh-llm` — a deliberate deviation
+
+The capability-seams Agent Note states the interface package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs act on an agent-owned `Session` (`compactRegion(start, end, agent)`) and its output uses the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`).
+
+This is not a coupling smell — it is the contract's domain. The "only cordis" guidance was always shorthand for "the interface depends only on what the contract genuinely names, and never on an implementation." `dsh-session` and `dsh-llm` are themselves interface/vocabulary packages, not implementations; `dsh-compact` still imports no backend. The seam's real invariant — *consumers and implementations evolve independently behind an abstract service* — holds intact.
+
+### Abstract `compactIfNeeded` / `compactRegion`, algorithm in the backend
+
+An earlier draft put the full algorithm (the retention walk, token-summing, text extraction) as concrete methods on the interface. That recouples the contract to one strategy: a backend that wants a different retention policy or event sequence would have to fight inherited concrete code. Making both core methods abstract puts every *how* decision in the backend and keeps the interface a statement of *what*. Token measurement is not a compaction hook at all; the singleton service lets multiple consumers share one per-session replay fold.
+
+`compactIfNeeded(agent, trigger, signal)` takes an explicit `'pressure' | 'context-overflow'` trigger and cancellation. It reads only the latest durable routed request; no header means no work, while any routed provider/model target uses the singleton estimator. `compactRegion(start, end, agent, signal?)` uses `agent.session` as its single session identity and keeps an optional signal for manual callers. The default summarizer resolves its target from explicit config, the latest logged routed target, then agent options, and records the provider/model pair after any `llm/stream` routing.
+
+### Automatic pressure runs after successful durable step work
+
+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).
+
+```
+assistant/message → tool/result/context/steering
+await serial agent/post-step ⟵ pressure compaction inside the successful step
+step/end
+
+provider overflow → step/end
+await waterfall agent/request-error ⟵ forced compaction between attempts
+retry → next numbered step/start ⟵ derives from the replacement surface
+```
+
+### Retention is turn-agnostic; tool-pairing balance is the only structural guard
+
+Auto-compaction checks after **every successful** step, not once per turn. This is load-bearing for runaway-turn survival: a tool-heavy ReAct turn appends an `assistant/message` + a `tool/result` per step, so the surface grows within a turn. The post-step check can compact early closed tool pairs before continuation opens the next step, and provider-confirmed overflow remains the backstop when a request crosses the limit first.
+
+`compactIfNeeded` retains the smallest tail of whole surface units whose estimated size reaches `retainTokens` and compacts older nodes. A unit is a complete closed step or one no-step message. If the token cutoff lands inside a step, retention expands until the cut is tool-pairing balanced. Balance is checked on surface order, not log sequence, because replacement summaries have new sequence numbers at old surface positions. `dsh-compact` exports the before/after edge helpers; their per-session cache folds only appended surface-tail nodes while `replaceGeneration` is unchanged, does no event reads for log-only growth, and rebuilds current membership and balances after replacement. `compactRegion` rejects boundaries that split a tool call from its result. The in-flight turn receives no special retention.
+
+A runaway turn thus compacts exactly like any other history: its early *closed* steps get summarized while its recent steps stay verbatim. When the only compactable content left is an un-splittable open tail step (its tool-calls have no results yet), compaction declines (`null`) and retries once that step closes.
+
+**Some single-unit overflow remains out of scope.** Summary range selection cannot split an indivisible unit. The optional pruner can repair a closed tool pair when removable text-bearing tool-result content is the bulk and the pruned remainder fits. Envelope-only pressure, an oversized indivisible non-tool node such as a pasted `user/message`, and a tool unit whose non-prunable remainder is still oversized remain outside compaction; bounding those units is a separate concern.
+
+### Head-anchoring: one auto checkpoint, always at the head
+
+Auto-compaction always starts at the surface head, merging the prior checkpoint with newly compacted history so only one automatic checkpoint remains. `shadowedRange` is therefore positional rather than a numeric sequence interval: a newer summary sequence may occupy an older surface position. `shadowedSeqs` records the authoritative surface order. Manual mid-range compaction may leave multiple checkpoints.
+
+### Approximate convergence invariant
+
+`resolveConfig` supplies usable defaults: threshold ratio `0.8`, retained tail `floor(contextWindow × 0.16)`, empty summarization provider/model overrides, `maxTokens: 8192`, `compactionRetries: 1`, `maxOverflowRetries: 1`, and `auto: true`. Optional top-level `thresholdRatio` and `retainTokens` override the policy for the token meter's single context window; retention must remain below the resulting threshold. Convergence remains dynamic because provider output caps can be spent on hidden or surfaced reasoning tokens and summary size is unpredictable. If pressure remains over threshold, `compactIfNeeded()` re-compacts the head checkpoint up to the configured retry count, but each committed summary must be smaller than what it shadows. Overflow bypasses threshold and retained-tail policy for one maximal balanced head reduction, leaving the newest indivisible unit.
+
+### Surface replacement: `compact/*` events are log-only; one `user/message` carries the summary
+
+Because `SurfaceEventType` is closed, the summary cannot ride on a `compact/*` event. The backend instead appends a **single `user/message`** with `surfaceOp: { op: 'replace', start, end }` whose `content` is the (framed) summary and whose `sourceEventSeqs` covers the shadowed entries *and* the bookkeeping events. The `compact/*` events are pure log records (lock + provenance). The surface mutation sits **inside** the lock — `compact/end` is the last event appended:
+
+```
+compact/start → log-only. Acquires the lock.
+[summarize older range via the backend]
+compact/summary → log-only. Provenance: raw summary, range, shadowed seqs, token count.
+user/message → surfaceOp { op:'replace', start, end }. THE surface mutation (framed summary).
+ deriveMessages() renders it as a user-role message.
+compact/end → log-only. Releases the lock (carries `error` on a recoverable failure).
+```
+
+`deriveMessages()` then yields `[summary_as_user_message, ...retained_entries]`. Reusing `user/message` is honest rather than a workaround: a summary genuinely *is* user-role context.
+
+### Checkpoint framing + incremental merge (backend-private)
+
+The basic backend wraps the summary as established checkpoint context and tags it for incremental merging on the next cycle. The raw summary remains on `compact/summary`. Framing is backend policy; the seam promises only that one replacement user message carries the possibly framed summary.
+
+### Blocking via a log-recorded lock, plus a crash/recoverable failure taxonomy
+
+The `compact/start … compact/end` bracket is justified, in order of what now does the work:
+
+1. **Crash-detectable orphan + provenance** (primary). Summarization is a slow model call persisted *after* `compact/start`. A crash mid-summarization leaves a `compact/start` with no matching `compact/end` — a detectable orphan. Releasing the lock last (rather than first) converts the crash window from *silent corruption* into that detectable orphan.
+2. **Prevents concurrent compaction.** `compactRegion` refuses to start if the current turn holds an unmatched `compact/start`. (The loop is single-threaded across either awaited automatic seam, so this is also a re-entry tripwire — a thrown "already in progress" signals a real bug.)
+
+Two failure paths, both documented:
+
+- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — no summary replacement lands. The derived surface remains the durable surface present at `compact/start`: full history when pruning made no replacement, or the already-pruned history when it did. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction.
+- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and lands no summary replacement. Post-step pressure warns and continues from the latest durable surface — full history if no replacement preceded the attempt, or the pruned surface if pruning already landed. Overflow recovery delegates only before any replacement; generation progress from earlier pruning authorizes a retry from that durable surface unless cancellation or disposal wins.
+
+`compact/end` keeps its `error?` field (mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling). There is no separate `compact/error` event.
+
+**Core session repair stays compaction-agnostic — deliberately.** `interruptedTurnClosers` is never taught about `compact/*`. Teaching it would force every future `xxx/start … xxx/end` plugin pair to patch a core module — exactly the coupling the capability-seam architecture exists to avoid. Because the log-only orphan is inert, no special repair is needed: generic turn-repair plus the inertness of an un-landed surface mutation is sufficient.
+
+## Alternatives considered
+
+- **The full algorithm as concrete interface methods** — rejected because it recouples the contract to one retention strategy. Both core methods are abstract; reusable measurement is a separate LLM-family service and `summarize()` is basic's sole hook.
+- **Compaction on `agent/request` or provisional `agent/pre-step` inputs** — rejected because neither proves the final durable request and both couple generic lifecycle to compaction-specific envelope data. Post-step replay plus canonical overflow recovery covers both successful and rejected calls.
+- **A separate `compact/error` event** — rejected: `compact/end` keeps an `error?` field, mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling.
+- **Teaching core turn-repair about `compact/*`** — rejected: the log-only orphan is inert, and a core module patched for every future `xxx/start … xxx/end` plugin pair is exactly the coupling the capability-seam architecture exists to avoid.
+
+## Consequences
+
+- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, and `compact-tool-result-prune` supplies optional deterministic rewriting. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred.
+- **Automatic seams**: `agent/post-step` (`@mode serial`) handles successful-call pressure and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. Generic `agent/pre-step` remains a four-argument checkpoint with no compaction-only prompt/prefix payload.
+- **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry.
+- **`dsh-compact`** owns `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)`, the cached surface-edge checks that `compactRegion` and `compactIfNeeded` use to avoid splitting a tool-call/result pair. The cache validates current membership by seq and answers both edges from one per-cut balance sequence; stale or missing seqs and orphan results reject.
+- **`dsh-session`** validates positional replacement, complete provenance, and content-only single-node `tool/result` rewrites through its one surface manager. `dsh-invariants` treats fresh appended tool results as executions that require an open step and pending call; validated replacements remain turn-enclosed rewrites.
+- **Wiring**: `examples/tui-agent/cordis.yml` loads zero-config `dsh-token-meter`, `dsh-compact-tool-result-prune`, then `dsh-compact-basic`; service-wide defaults make the composition usable without repeated numeric policy.
+
+## Testing
+
+- **Unit:** Real Loader and invariant plugins cover whole-unit retention, pruning configuration and replay, rich-block ordering, metadata preservation, convergence, both `compact/end` outcomes, open-tail refusal, pruning-only and summarized overflow recovery, generation proof, caps, and original-error preservation.
+- **Loop:** Tests pin post-step after durable tool results and before `step/end`, actual `agent/request` routing, closed failed steps, fresh retry numbering, and complete thrown/in-band overflow → compaction → reconstructed retry composition.
+- **With-key e2e:** A real model and bash session with lowered limits triggers compaction, records a complete `compact/start…end` pair, shrinks the surface, and finishes the task.
+- **Snapshot gap:** Runaway-turn compaction cannot yet replay because the summarization call records no `assistant/chunk` events or `sessionId`; interleaved summarization-call replay remains follow-up work.
diff --git a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
similarity index 73%
rename from docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md
rename to .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
index 83c7beb440..3ed2090b22 100644
--- a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md
+++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
@@ -1,16 +1,16 @@
-# RFC: Subagent capability seam
+# Agent Note: Subagent capability seam
Status: implemented
-> The full seam is shipped: the `dsh-subagent` interface, the `dsh-subagent-mock` test backend, and the `dsh-tool-subagent` consumer; the two in-process backends (`dsh-subagent-spawn`, `dsh-subagent-fork`); the nested-agent snapshot infrastructure ([per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md)); and the out-of-process `dsh-subagent-acp` backend ([its RFC](2026-06-22-acp-subagent-backend.md)).
+> The full seam is shipped: the `dsh-subagent` interface and `dsh-tool-subagent` consumer; the two in-process backends (`dsh-subagent-spawn`, `dsh-subagent-fork`); the nested-agent snapshot infrastructure ([per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md)); and the out-of-process `dsh-subagent-acp` backend ([its Agent Note](2026-06-22-acp-subagent-backend.md)).
## Problem
-The harness has a long-deferred seam for **subagents** — an agent delegating work to another agent. The intent was sketched in the `Agent`/`AgentLoop` interfaces ([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts), [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)): a creation option referencing a parent agent (fork = seed the child session with the parent's event log; spawn = fresh session), with the child returned as an `Agent` handle so steering and event subscription work uniformly. This RFC realizes that seam; the banner above lists what shipped.
+The harness has a long-deferred seam for **subagents** — an agent delegating work to another agent. The intent was sketched in the `Agent`/`AgentLoop` interfaces ([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts), [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)): a creation option referencing a parent agent (fork = seed the child session with the parent's event log; spawn = fresh session), with the child returned as an `Agent` handle so steering and event subscription work uniformly. This Agent Note realizes that seam; the banner above lists what shipped.
The distinctive requirement — the one that shapes the whole design — is that **multiple subagent implementations must coexist at runtime**. A parent may want a cheap in-process child for a scoped subtask AND an isolated out-of-process child (over ACP) in the same session. The transports we foresee:
-- **in-process** — a child `ReactLoopAgent` on the same `Context` (the cheapest, and nearly free given the existing agent factory);
+- **in-process** — a child concrete `Agent` on the same `Context` (the cheapest, and nearly free given the existing agent factory);
- **ACP** — act as an ACP *client* driving another agent process (which can be another instance of ourselves);
- later: **A2A**, the **Codex app-server**, and the **Claude Code Agent SDK** — each the same out-of-process "start a child, prompt it, stream updates, cancel" shape as the ACP backend.
@@ -18,7 +18,7 @@ The distinctive requirement — the one that shapes the whole design — is that
### Why not the bash seam shape
-The bash seam ([capability seams](../../implemented/architecture/2026-06-13-capability-seams.md)) registers exactly one `BashExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmService.registerAdapter`), not the single-service bash executor. The seam is still three-package (interface / implementation / consumer); only the "one vs. many implementations" axis differs.
+The bash seam ([capability seams](../architecture/2026-06-13-capability-seams.md)) registers exactly one `BashExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmService.registerAdapter`), not the single-service bash executor. The seam is still three-package (interface / implementation / consumer); only the "one vs. many implementations" axis differs.
## Decision
@@ -32,7 +32,6 @@ A new package group `packages/subagent/`:
| `@deepseek-ai/dsh-subagent-spawn` | implementation: a fresh in-process child via `ctx.agents.create` |
| `@deepseek-ai/dsh-subagent-fork` | implementation: an in-process child seeded with a snapshot of the parent's log |
| `@deepseek-ai/dsh-subagent-acp` | implementation: an ACP client driving a configured child process |
-| `@deepseek-ai/dsh-subagent-mock` | support: a scripted provider for testing the seam through the real load path |
| `@deepseek-ai/dsh-tool-subagent` | consumer: the model-facing `subagent` tool over `ctx.subagents` |
### The primitive: async `start → SubagentRun`
@@ -62,11 +61,11 @@ Each subagent runs in its **own `Session`** (own id, `parentSession` lineage), p
## Testing
-The seam is tested through the real Cordis Loader/export path, which catches the export-shape failure described in [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md). Registry tests cover reload safety, duplicate names, and start-time capability rejection; nested-agent scenarios replay keylessly through [per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md); in-process backends also have real-loop unit tests and a with-key e2e.
+Registry and tool tests replace only the nondeterministic child boundary with a package-local scripted provider while exercising the real `SubagentService`, lifecycle, task integration, and model-facing tool. Provider and consumer export shapes retain their Loader regression coverage for the failure described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md). Registry tests cover reload safety, duplicate names, and start-time capability rejection; nested-agent scenarios replay keylessly through [per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md); in-process backends also have real-loop unit tests and a with-key e2e.
## Consequences
-- **Recursion.** Without a bound, an in-process child can see the delegation tool and recurse. The in-process backends implement the optional absolute depth limit and scoped live-global `toolFilter`; ACP advertises both capabilities off and rejects such a request. The [subagent composition-controls RFC](2026-07-12-subagent-persona-tool-filter-and-depth.md) owns their exact semantics and security limits.
+- **Recursion.** Without a bound, an in-process child can see the delegation tool and recurse. The in-process backends implement the optional absolute depth limit and scoped live-global `toolFilter`; ACP advertises both capabilities off and rejects such a request. The [subagent composition-controls Agent Note](2026-07-12-subagent-persona-tool-filter-and-depth.md) owns their exact semantics and security limits.
- **Blocking the parent turn.** Foreground collection holds the parent's step open for the child's full duration. Background delegation uses the shared `ctx.tasks` runtime and generic `task_*` tools, the same collection mechanism as background bash; the subagent seam itself remains task-agnostic.
- **Live progress.** This cut surfaces only lifecycle + final result; a per-chunk child→parent update stream is deferred with the background redesign.
- **ACP client surface.** Proxying `fs`/`terminal` from the ACP child back to the parent (a shared-workspace mode) is future work; the first cut advertises neither, so the child self-serves in its own process.
diff --git a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.md b/.agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md
similarity index 84%
rename from docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.md
rename to .agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md
index 0b98adb0de..1c5b5b533d 100644
--- a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.md
+++ b/.agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md
@@ -1,10 +1,10 @@
-# RFC: ACP subagent backend (out-of-process delegation)
+# Agent Note: ACP subagent backend (out-of-process delegation)
Status: implemented
## Problem
-The subagent seam ([the seam RFC](2026-06-21-subagent-capability-seam.md)) was built so multiple backends coexist by name on `ctx.subagents`. The in-process backends (`-spawn`/`-fork`) run a child as a second `Agent` on the SAME cordis context — cheap, but the child shares the parent's process, model client, and tools. The seam's whole point was to also support an OUT-OF-PROCESS child reached over a protocol, proving the abstraction generalizes across a process boundary. This RFC adds the first such backend: an Agent Client Protocol (ACP) client.
+The subagent seam ([the seam Agent Note](2026-06-21-subagent-capability-seam.md)) was built so multiple backends coexist by name on `ctx.subagents`. The in-process backends (`-spawn`/`-fork`) run a child as a second `Agent` on the SAME cordis context — cheap, but the child shares the parent's process, model client, and tools. The seam's whole point was to also support an OUT-OF-PROCESS child reached over a protocol, proving the abstraction generalizes across a process boundary. This Agent Note adds the first such backend: an Agent Client Protocol (ACP) client.
## Decision
@@ -16,7 +16,7 @@ Each `start` spawns a new child, runs exactly one ACP session (`initialize` →
### Minimal client stub
-The client advertises NO optional capabilities (no `fs`, no `terminal`): the child self-serves file/terminal access in its own process. `session/update` notifications are consumed — the backend accumulates `agent_message_chunk` text as the result output and ignores the rest (thoughts, tool-call cards) in this cut, which surfaces only the child's final answer. `session/request_permission` is auto-answered by a configured policy (`reject` declines every prompt, `allow` approves via the first allow-shaped option) — the first cut surfaces no prompt to a human. Proxying `fs`/`terminal` back to the parent (a shared-workspace mode) remains future work, as the seam RFC noted.
+The client advertises NO optional capabilities (no `fs`, no `terminal`): the child self-serves file/terminal access in its own process. `session/update` notifications are consumed — the backend accumulates `agent_message_chunk` text as the result output and ignores the rest (thoughts, tool-call cards) in this cut, which surfaces only the child's final answer. `session/request_permission` is auto-answered by a configured policy (`reject` declines every prompt, `allow` approves via the first allow-shaped option) — the first cut surfaces no prompt to a human. Proxying `fs`/`terminal` back to the parent (a shared-workspace mode) remains future work, as the seam Agent Note noted.
### No start-time capabilities
@@ -52,4 +52,4 @@ Every run pays a fresh subprocess (spawn + `initialize` + `newSession`). The par
## Future providers
-The same out-of-process spawn/prompt/stream/cancel shape generalizes to other transports named in the seam RFC — A2A, the Codex app-server, and the Claude Code Agent SDK — each a sibling provider registered by name. The ACP backend is the proof that the seam supports the boundary; those are mechanically similar.
+The same out-of-process spawn/prompt/stream/cancel shape generalizes to other transports named in the seam Agent Note — A2A, the Codex app-server, and the Claude Code Agent SDK — each a sibling provider registered by name. The ACP backend is the proof that the seam supports the boundary; those are mechanically similar.
diff --git a/docs/rfc/implemented/feature/2026-06-24-workspace-context.md b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md
similarity index 95%
rename from docs/rfc/implemented/feature/2026-06-24-workspace-context.md
rename to .agents/notes/implemented/feature/2026-06-24-workspace-context.md
index cc11fd19ff..a3eec63b8f 100644
--- a/docs/rfc/implemented/feature/2026-06-24-workspace-context.md
+++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md
@@ -1,4 +1,4 @@
-# RFC: Workspace context instruction files
+# Agent Note: Workspace context instruction files
Status: implemented
@@ -40,7 +40,7 @@ After a successful first-party `read`, `write`, or `edit` call, the `tools/post-
A content edit appends `Updated instructions from: `, 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: ` and states that the previously loaded instructions no longer apply.
-Dynamic messages use a raw `context/message` envelope because the plugin owns the complete system-reminder framing. Core context injection therefore supports `envelope: 'raw'`; callers that omit it retain the canonical `` wrapper. `context/message.meta` carries opaque JSON state that is persisted but never rendered to the model.
+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.
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.
@@ -76,7 +76,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc
## Consequences
-Workspace guidance is isolated per session and shared by both product front doors 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 includes optional raw framing and JSON metadata, both propagated through prompt-submit and post-tool `additionalContexts` arrays without flattening entries.
+Workspace guidance is isolated per session and shared by both product front doors 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.
Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, delimiter escaping, and symlink rejection reduce risk but do not eliminate prompt injection. Permission and sandbox layers treat workspace files as data rather than authority.
diff --git a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.md b/.agents/notes/implemented/feature/2026-06-25-ask-user-question.md
similarity index 75%
rename from docs/rfc/implemented/feature/2026-06-25-ask-user-question.md
rename to .agents/notes/implemented/feature/2026-06-25-ask-user-question.md
index d7c6631b94..e1c5d03c08 100644
--- a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.md
+++ b/.agents/notes/implemented/feature/2026-06-25-ask-user-question.md
@@ -1,4 +1,4 @@
-# RFC: Ask-user question capability
+# Agent Note: Ask-user question capability
Status: implemented
@@ -20,9 +20,9 @@ Providers return `{ answers: [{ id, selected, custom? }] }`. `selected` is alway
## UI mappings
-`dsh-stdio-demo`'s in-package readline module renders each question, shows each option's `description` on the next line, supports comma/space-separated numeric choices for `multi_select`, accepts free-form custom answers, and rejects pending questions on abort, provider disposal, or stdin EOF. A batched request is asked in order and resolved as one answer object. The stdio provider serializes simultaneous requests with an internal queue so only one prompt owns stdin at a time.
+`dsh-tui` renders each question as a keyboard overlay, shows option descriptions, supports single- and multi-select choices plus free-form custom answers, and rejects pending questions on abort, provider disposal, or terminal shutdown. Batched and simultaneous requests are queued so one overlay owns keyboard focus at a time.
-`dsh-acp` provides the same seam for ACP sessions. It routes an ask request from the calling `Agent` through the bridge's `agent→sessionId` reverse map and calls ACP `unstable_createElicitation` with a session-scoped form for each question. Single-select options become a `choice` string enum; `multi_select` options become a `choice` array enum; optionless questions use a required `custom` text field. If the client returns both `choice` and non-empty `custom`, the custom answer wins. ACP `decline`/`cancel`, a missing answer, a missing session, and a client without elicitation support all become structured `UserInteractionError`s.
+`dsh-acp` provides the same seam for ACP sessions. It resolves the calling `Agent` through `ownedRecord`, requiring the forward session-map record at `agent.session.id` to own that exact agent object, and calls ACP `unstable_createElicitation` with a session-scoped form for each question. Single-select options become a `choice` string enum; `multi_select` options become a `choice` array enum; optionless questions use a required `custom` text field. If the client returns both `choice` and non-empty `custom`, the custom answer wins. ACP `decline`/`cancel`, a missing answer, a missing session, and a client without elicitation support all become structured `UserInteractionError`s.
The ACP mapping deliberately uses elicitation, not `session/request_permission`. `request_permission` is still reserved for the separate permission gate: it is a yes/no-or-policy authorization protocol around tool execution. `ask_user_question` is a general information-gathering tool with optional free-form answers, so ACP form elicitation is the closer protocol fit. The bridge's session routing is shared with the future permission gate, but the user intent is different.
@@ -42,8 +42,8 @@ ACP elicitation is currently marked unstable in the SDK. The fallback is still s
The feature gives the model a powerful pause primitive, so prompt guidance matters. The tool description tells the model to ask concise questions and use options when possible. Product policy can later wrap `tools/execute` to restrict when the tool is allowed, but the loop should not special-case it.
-`dsh-user-interaction` and `dsh-tool-ask-user` both live in `packages/ui` because they form one product-facing human-interaction capability. `agent-core` does not load either the tool or a provider. `stdio-agent` opts into the seam, its readline provider, and the model-facing tool. `acp-agent` keeps only the `userInteraction` seam/provider by default: ACP elicitation support is still client-dependent, so an ACP leaf must opt into the model-facing tool deliberately once its client can complete elicitation requests.
+`dsh-user-interaction` and `dsh-tool-ask-user` both live in `packages/ui` because they form one product-facing human-interaction capability. `agent-core` does not load either the tool or a provider. `dsh-tui-demo` opts into the seam, TUI provider, and model-facing tool. `acp-agent` keeps only the `userInteraction` seam/provider by default: ACP elicitation support is still client-dependent, so an ACP leaf must opt into the model-facing tool deliberately once its client can complete elicitation requests.
## Testing
-Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. `dsh-stdio-demo` tests cover option descriptions, queued requests, EOF/abort cleanup, optionless free-form input, invalid option reprompts, duplicate multi-select numbers, and batched question flows. ACP bridge tests drive a real in-memory ACP connection with the real `ask_user_question` tool and verify selected-option, custom-overrides-choice, multi-select, and optionless free-form elicitation paths continue the agent loop.
+Unit coverage pins provider registration/disposal, duplicate-provider rejection, abort-before-provider, empty-question rejection, structured tool errors through `ctx.tools.execute()`, batched answers, multi-select answers, custom answers, and the model schema including the removal of `value`, `recommended`, `allow_custom`, and `desc`. TUI tests cover option descriptions, queued requests, shutdown/abort cleanup, optionless free-form input, invalid choices, duplicate multi-select selections, and batched question flows. ACP bridge tests drive a real in-memory ACP connection with the real `ask_user_question` tool and verify selected-option, custom-overrides-choice, multi-select, and optionless free-form elicitation paths continue the agent loop.
diff --git a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.md b/.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md
similarity index 92%
rename from docs/rfc/implemented/feature/2026-06-29-todo-write-tool.md
rename to .agents/notes/implemented/feature/2026-06-29-todo-write-tool.md
index 126c69f1c0..ab9421c2ed 100644
--- a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.md
+++ b/.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md
@@ -1,4 +1,4 @@
-# RFC: The `todo_write` tool — model task list as event-sourced session state
+# Agent Note: The `todo_write` tool — model task list as event-sourced session state
Status: implemented
@@ -20,7 +20,7 @@ The list is appended as a `todo/write` event carrying the full `{ todos }` snaps
### NOT a surface event
-`todo/write` is deliberately excluded from `SurfaceEventType`. The surface is the projection that produces the LLM message history (`deriveMessages()`); a todo write produces no conversation message. So it carries no `surfaceOp`, never joins the surface linked list, and never reaches `deriveMessages()` — it is durable, replayable *UI* state that travels alongside the conversation without being part of it. (The dev-mode invariants still require it to sit inside an open turn, which it always does: it is appended mid-step during a tool call.)
+`todo/write` is deliberately excluded from `SurfaceEventType`. The surface is the projection that produces the LLM message history (`deriveMessages()`); a todo write produces no conversation message. So it carries no `surfaceOp`, never joins the ordered surface, and never reaches `deriveMessages()` — it is durable, replayable *UI* state that travels alongside the conversation without being part of it. (The dev-mode invariants still require it to sit inside an open turn, which it always does: it is appended mid-step during a tool call.)
### Priority synthesized only at the ACP boundary
@@ -49,7 +49,7 @@ Four tiers, designed up front:
- **Real-Loader path** — the plugin run through `Loader.unwrapExports`, asserting the namespace export shape survives (it HAS `inject`, so a stray default would crash at load — postmortem/0001).
- **Full-loop integration** — a scripted mock model calls `todo_write` through the real agent loop; the `todo/write` event lands and a second call replaces it.
- **`session/load` replay** — a persisted `todo/write` re-emits the `plan` update when a fresh ACP bridge loads the session.
-- **With-key e2e + snapshot** — a real prompt induces a `todo_write`; the snapshot golden gains the `plan` notification and the log event.
+- **With-key e2e + snapshot** — a real prompt induces a `todo_write`; the snapshot expected output gains the `plan` notification and the log event.
## Alternatives considered
diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md
similarity index 88%
rename from docs/rfc/implemented/feature/2026-06-30-hook-bridges.md
rename to .agents/notes/implemented/feature/2026-06-30-hook-bridges.md
index af5c7e928d..0c3c1e13ef 100644
--- a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.md
+++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md
@@ -1,16 +1,16 @@
-# RFC: dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges
+# Agent Note: dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges
Status: implemented
## Problem
-The harness's extension surface is its typed interception seams ([the interception-seams RFC](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 RFC 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 RFC](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-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 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.
## Decision
-Two independent plugins in the `packages/hooks/` group, each a function/namespace plugin (`name`/`inject`/`Config`/`apply`, NO default export — see [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)) injecting only `bash`:
+Two independent plugins in the `packages/hooks/` group, each a function/namespace plugin (`name`/`inject`/`Config`/`apply`, NO default export — see [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)) injecting only `bash`:
- **`dsh-hooks-claude`** — the CC dialect. Seven of Claude Code's current hook points: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStart`, and `SubagentStop`. Owns CC-shaped per-event stdin payloads (a base of `session_id`/`transcript_path`/`cwd`/`hook_event_name` plus per-event fields), `CLAUDE_PROJECT_DIR` plus `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` substitution, and the literal-or-regex matcher mode. `transcript_path` is the persistence locator result or `''`; stdin carries a **trailing newline**.
- **`dsh-hooks-codex`** — five of Codex's current hook points: `PreToolUse`, `PostToolUse`, `SessionStart`, `UserPromptSubmit`, and `Stop`. It uses an always-regex matcher, Codex-shaped snake_case payloads with `turn_id`/`model`/`permission_mode` extras written WITHOUT a trailing newline, no Codex plugin-env injection or config-time placeholder substitution, and no pre-tool approval or rewrite path. `transcript_path` is the same locator result or `null`; tool payloads carry the real `tool_name` in the reduced `tool_input: { command }` shape.
@@ -53,7 +53,7 @@ Hooks run in the agent's session workspace, so relative paths target the user's
## Deferred compatibility gaps
-- **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 RFC](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + ACP/tool-bash presentation, so an honest rewrite is a design unit, not a field.
+- **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 + ACP/tool-bash 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.
- **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)`).
diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.md b/.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md
similarity index 90%
rename from docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.md
rename to .agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md
index ac28345791..bb1822504a 100644
--- a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.md
+++ b/.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md
@@ -1,4 +1,4 @@
-# RFC: dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core
+# Agent Note: dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
The hooks subsystem ships two bridge plugins: one that runs a user's existing Claude Code (CC) hooks, one for Codex hooks. Studying the reference implementations (`~/repos/refs/claude-code`, `~/repos/refs/codex`) surfaced a decisive fact: **Codex deliberately reimplements a SUBSET of the CC hook protocol.** Its engine reads the same `hooks.json`, uses the same matcher-group shape, the same exit-code/structured-stdout output contract, and the same command-hook execution model — Codex's source even names the engine after Claude's and comments where it "intentionally diverges." So the two bridges would otherwise duplicate the bulk of the protocol.
-This RFC introduces `@deepseek-ai/dsh-hook-protocol`, a **library** (not a plugin — it registers and injects nothing) holding the genuinely-identical primitives both bridges build on. The split between shared and per-dialect is the design's center of gravity.
+This Agent Note introduces `@deepseek-ai/dsh-hook-protocol`, a **library** (not a plugin — it registers and injects nothing) holding the genuinely-identical primitives both bridges build on. The split between shared and per-dialect is the design's center of gravity.
## Decision
@@ -15,7 +15,7 @@ A new `packages/hooks/` group with `hook-protocol` as a pure library. It owns fo
**Shared (here):**
- **Matcher** — `matchesMatcher(pattern, query, mode)`. The ONE axis the dialects differ on is collapsed to the `mode` parameter: `claude` treats a pure `[A-Za-z0-9_|]+` pattern as a literal (pipe = exact-match alternation) and anything else as a regex; `codex` is always an unanchored regex. Match-all on absent/`''`/`'*'`; an invalid regex matches nothing (never throws into the loop).
- **Execution** — `runHook(bash, hook, options)`. Runs a command hook through the `ctx.bash` seam rather than a bespoke `spawn`: the executor already provides the scrubbed-but-overridable env, process-group kills, and timeout the protocol needs, and `dsh-bash`'s `stdin`/`env` fields (added for exactly this) are the trusted-plugin surface an in-process bridge is allowed to use. It serializes the bridge-built payload to stdin (trailing newline iff CC), honors the hook's `timeoutSec` (else `DEFAULT_HOOK_TIMEOUT_MS`, the 10-minute reference default both dialects share), and never throws (an executor rejection becomes a non-blocking-error `HookOutput`).
-- **Decode** — `parseHookOutput(exit, stdout, stderr)`, the exit-code + structured-stdout codec, producing a dialect-neutral `HookOutput`. Exit `0` → lenient JSON parse of stdout; exit `2` → blocking error with `stderr` as the reason (surfaced as `decision: 'block'` so no caller needs a separate exit-code branch); other → non-blocking error. Parses the CC structured-stdout fields that have a consumer on some path (`continue`/`stopReason`/`decision`/`hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}`/`systemMessage`); the bridge honors only the subset meaningful for its dialect. Fields with no consumer on any path are not parsed at all (CC's `suppressOutput` — hook stdout never enters a transcript here, so there is nothing to suppress; see [the tighten-hook-protocol-contract RFC](../simplification/2026-07-04-tighten-hook-protocol-contract.md)).
+- **Decode** — `parseHookOutput(exit, stdout, stderr)`, the exit-code + structured-stdout codec, producing a dialect-neutral `HookOutput`. Exit `0` → lenient JSON parse of stdout; exit `2` → blocking error with `stderr` as the reason (surfaced as `decision: 'block'` so no caller needs a separate exit-code branch); other → non-blocking error. Parses the CC structured-stdout fields that have a consumer on some path (`continue`/`stopReason`/`decision`/`hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}`/`systemMessage`); the bridge honors only the subset meaningful for its dialect. Fields with no consumer on any path are not parsed at all (CC's `suppressOutput` — hook stdout never enters a transcript here, so there is nothing to suppress; see [the tighten-hook-protocol-contract Agent Note](../simplification/2026-07-04-tighten-hook-protocol-contract.md)).
- **Merge** — `mergeHookOutputs(outputs)`, folding multiple matched hooks into one most-restrictive `MergedHookOutcome`: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined `\n\n`, context/system-messages accumulated in order.
- **`hook/*` session events** — `hook/invoked` / `hook/result`, declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT `SurfaceEventType`s), with `appendHookInvoked`/`appendHookResult` helpers so the invoked/result pairing and turn-enclosure stay consistent across bridges. `appendHookResult` also owns the durable record's semantics — the decision string (the hook's parsed decision, else `'stop'` on `continue:false`, else `'pass'`) and the 500-character `stderrSummary` truncation derive from the `HookOutput` here, not per-bridge.
diff --git a/docs/rfc/implemented/feature/2026-06-30-interception-seams.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md
similarity index 73%
rename from docs/rfc/implemented/feature/2026-06-30-interception-seams.md
rename to .agents/notes/implemented/feature/2026-06-30-interception-seams.md
index 8a2af04494..2535c1c564 100644
--- a/docs/rfc/implemented/feature/2026-06-30-interception-seams.md
+++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md
@@ -1,4 +1,4 @@
-# RFC: Interception seams — the typed-Decision surface a hook programs against
+# Agent Note: Interception seams — the typed-Decision surface a hook programs against
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
The harness needs a hooks subsystem: users extend or gate the agent at lifecycle points the way Claude Code (CC) and Codex do. The key reframe driving this design is that **"native hooks" are not a package** — a native hook is just an ordinary Cordis plugin subscribing to the canonical lifecycle events. So the real product is a *powerful, well-typed canonical event surface*; the CC/Codex bridges (the `dsh-hooks-claude` / `dsh-hooks-codex` packages) are merely translators that map an external shell-hook protocol onto that same surface. Anything a bridge can do, a plain plugin can do directly — more powerfully (no serialization boundary, full `ctx`, typed returns).
-The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubmit`), session-start observation (CC's `SessionStart`), pre-tool policy, around-dispatch control, post-tool transformation, final-result observation, and continuation with a model-facing reason. Conflating those phases gives plugins mutation channels they do not need and makes finality depend on listener ordering. The [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) supplies the three-domain rule and the typed-Decision idiom; this RFC applies them to the lifecycle seams.
+The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubmit`), session-start observation (CC's `SessionStart`), pre-tool policy, around-dispatch control, post-tool transformation, final-result observation, and continuation with a model-facing reason. Conflating those phases gives plugins mutation channels they do not need and makes finality depend on listener ordering. The [event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md) supplies the three-domain rule and the typed-Decision idiom; this Agent Note applies them to the lifecycle seams.
## Decision
@@ -14,9 +14,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, next) → PromptDecision` — waterfall, fired per drained queued message inside the open turn, before the `user/message` append. `allow` (optionally rewriting the prompt `content` or attaching separately sourced `additionalContexts[]`) or `block` (dropping the prompt; the loop appends a durable `prompt/blocked` in its place — see the dispatch note below).
+- `agent/prompt-submit(agent, content, source, next) → PromptDecision` — waterfall, fired for the turn's single claimed queued message before the `user/message` append. `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`; `block` appends a durable `prompt/blocked` and rejects that zero-step turn.
-**`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 a context envelope or durable context metadata.
+**`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.
### The tool pipeline gives each phase one kind of authority
@@ -30,13 +30,13 @@ Every call follows `tools/pre-execute` → guards → `tools/execute` → dispat
Core dispatch and the tool body sit inside normalization boundaries, so tool, listener, malformed-result, non-JSON result, 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, and a final observer sees exactly what the caller receives and the session log can persist.
-**`TurnEndReason.rejected`** (`dsh-session`): a turn whose entire prompt batch was blocked by `prompt-submit`.
+**`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 fully blocked batch becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. Every veto also records `prompt/blocked` with the original prompt and reason, so mixed batches retain blocked inputs. Every allowed `additionalContexts` entry is injected into the open turn.
+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.
-2. **Post-tool `additionalContexts` are buffered and appended AFTER all `tool/result`s.** `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 appends every entry only after every `tool/result` in the step. 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 `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.
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).
@@ -55,4 +55,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](../../../architecture.md), package READMEs, [core interception decisions](../../../core-data-structures/core.md#interception-decisions), and [tool structures](../../../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, 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.
diff --git a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md b/.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md
similarity index 83%
rename from docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md
rename to .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md
index 8d8813563b..ee67af7d96 100644
--- a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md
+++ b/.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md
@@ -1,4 +1,4 @@
-# RFC: SessionStore fork API
+# Agent Note: SessionStore fork API
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
The event-sourced session log already has the primitive a fork needs: create a new session with a seed event prefix, then derive model history from that seeded log exactly as replay does. That primitive is intentionally low-level: `ctx.sessions.create(id, { seed, meta })` accepts any valid seed, but ordinary live-session branching needs policy around which prefix can be copied, which metadata is stamped on the child, and how errors are classified.
-The semantic hazard is the fork boundary. A valid user-visible fork seed must be contiguous and turn-enclosed. Forking inside an active turn would copy an open `turn/start`, possibly an open `step/start`, and possibly dangling tool calls. That violates turn-enclosure and provider-transcript invariants, and it creates a misleading child history that appears to have participated in an unfinished parent turn. The existing [subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) deliberately solves a different problem: tool-triggered subagent forks usually happen while the parent turn is open, so `dsh-subagent-fork` clips the seed to the parent's last completed-turn prefix. A general session fork should not silently clip; it should either fork the requested boundary or reject it.
+The semantic hazard is the fork boundary. A valid user-visible fork seed must be contiguous and turn-enclosed. Forking inside an active turn would copy an open `turn/start`, possibly an open `step/start`, and possibly dangling tool calls. That violates turn-enclosure and provider-transcript invariants, and it creates a misleading child history that appears to have participated in an unfinished parent turn. The existing [subagent seam](2026-06-21-subagent-capability-seam.md) deliberately solves a different problem: tool-triggered subagent forks usually happen while the parent turn is open, so `dsh-subagent-fork` clips the seed to the parent's last completed-turn prefix. A general session fork should not silently clip; it should either fork the requested boundary or reject it.
## Decision
@@ -38,4 +38,4 @@ An empty prefix is forkable; any non-empty boundary must be a safe existing sequ
The public surface stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so existing backends persist that seed once and preserve `parentSession` / `seedLength` in the header.
-The v1 scope still excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has transcript/snapshot coverage; this RFC adds no editor-facing updates, so no ACP snapshot is required now. Fork-child replay remains covered by the existing [seed-boundary testing RFC](../../implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md), while this API gets focused `dsh-session` unit tests plus JSONL persistence coverage.
+The v1 scope still excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has transcript/snapshot coverage; this Agent Note adds no editor-facing updates, so no ACP snapshot is required now. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md), while this API gets focused `dsh-session` unit tests plus JSONL persistence coverage.
diff --git a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md b/.agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md
similarity index 57%
rename from docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md
rename to .agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md
index aa853edd24..861779fc50 100644
--- a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md
+++ b/.agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md
@@ -1,12 +1,12 @@
-# RFC: Subagent lifecycle enrichment — lastAssistantMessage (observe-only)
+# Agent Note: Subagent lifecycle enrichment — lastAssistantMessage (observe-only)
Status: implemented
## Problem
-The hooks subsystem ([interception seams RFC](2026-06-30-interception-seams.md)) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose **SubagentStart / SubagentStop** hooks, and CC's carry the subagent's final message. The harness already emits `subagent/start` and `subagent/end` lifecycle events ([the subagent capability-seam](2026-06-21-subagent-capability-seam.md)), but their payloads were minimal (`provider`, `id`, and on end `stopReason`) — not enough for a hooks bridge to report WHAT a subagent produced without separately reaching for the live run.
+The hooks subsystem ([interception seams Agent Note](2026-06-30-interception-seams.md)) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose **SubagentStart / SubagentStop** hooks, and CC's carry the subagent's final message. The harness already emits `subagent/start` and `subagent/end` lifecycle events ([the subagent capability-seam](2026-06-21-subagent-capability-seam.md)), but their payloads were minimal (`provider`, `id`, and on end `stopReason`) — not enough for a hooks bridge to report WHAT a subagent produced without separately reaching for the live run.
-This RFC enriches the end payload. It is deliberately **observe-only**: no control-flow change and no waterfall. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope.
+This Agent Note enriches the end payload. It is deliberately **observe-only**: no control-flow change and no waterfall. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope.
## Decision
@@ -16,14 +16,14 @@ Both events stay plain **`emit`s**. Async `SubagentService.start()` attaches res
## Alternatives considered
-**An `agentType` subagent-kind label** (the harness analogue of CC's `subagent_type`) on the request + both lifecycle payloads — an earlier draft shipped it; dropped in review because it is a Claude-Code concept that does not fit our own seam (nothing here interprets it, and the only consumer was a CC-dialect bridge). The CC bridge instead feeds Claude Code's own default matcher value `"general-purpose"` for its SubagentStart/Stop `agent_type` matcher, so this RFC ships ONE enrichment: `lastAssistantMessage`.
+**An `agentType` subagent-kind label** (the harness analogue of CC's `subagent_type`) on the request + both lifecycle payloads — an earlier draft shipped it; dropped in review because it is a Claude-Code concept that does not fit our own seam (nothing here interprets it, and the only consumer was a CC-dialect bridge). The CC bridge instead feeds Claude Code's own default matcher value `"general-purpose"` for its SubagentStart/Stop `agent_type` matcher, so this Agent Note ships ONE enrichment: `lastAssistantMessage`.
**A control-flow `subagent/end`** — deferred; see below.
## Why observe-only, and what is deferred
-A control-flow `subagent/end` (an awaited waterfall returning a stop/continue decision, like the other interception seams) would require: reshaping `subagent/end` from emit to waterfall, restructuring `SubagentService.start` to await listeners before settling, and implementing the `resume` capability in the in-process provider so a "continue" can actually re-run the child. That belongs to the background/steering subagent redesign the [capability-seam RFC](2026-06-21-subagent-capability-seam.md) already defers (the same redesign that unifies long-running-tool handling across subagents and bash). This RFC ships the observe-only enrichment a hooks bridge needs today; `FIXME(subagent-continuation)` / `TODO` anchors mark where the control-flow version would land if and when that redesign happens.
+A control-flow `subagent/end` (an awaited waterfall returning a stop/continue decision, like the other interception seams) would require: reshaping `subagent/end` from emit to waterfall, restructuring `SubagentService.start` to await listeners before settling, and implementing the `resume` capability in the in-process provider so a "continue" can actually re-run the child. That belongs to the background/steering subagent redesign the [capability-seam Agent Note](2026-06-21-subagent-capability-seam.md) already defers (the same redesign that unifies long-running-tool handling across subagents and bash). This Agent Note ships the observe-only enrichment a hooks bridge needs today; `FIXME(subagent-continuation)` / `TODO` anchors mark where the control-flow version would land if and when that redesign happens.
## Consequences
-A hooks bridge (or a native plugin) can now forward the child's `lastAssistantMessage` to a SubagentStop handler by subscribing to the existing emits — no new control-flow surface. The vocabulary addition is documented in [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md) (the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with one more (optional) field on the end payload — so no snapshot or e2e change is needed.
+A hooks bridge (or a native plugin) can now forward the child's `lastAssistantMessage` to a SubagentStop handler by subscribing to the existing emits — no new control-flow surface. The vocabulary addition is documented in [docs/core-data-structures/subagent.md](../../../../docs/core-data-structures/subagent.md) (the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with one more (optional) field on the end payload — so no snapshot or e2e change is needed.
diff --git a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md
similarity index 96%
rename from docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md
rename to .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md
index 6302584103..599c895ae2 100644
--- a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md
+++ b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md
@@ -1,4 +1,4 @@
-# RFC: Dynamic workflows — a script-driven multi-agent orchestration seam
+# Agent Note: Dynamic workflows — a script-driven multi-agent orchestration seam
Status: implemented
@@ -18,7 +18,7 @@ One deliberate strictness DIVERGENCE from CC: hook misuse — unknown or deferre
### The seam (dsh-workflow)
-`ctx.workflows` is an abstract `WorkflowService` in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). `start(request)` throws synchronously for a script that cannot begin; a returned `WorkflowRun`'s `result` NEVER rejects (failures resolve as `stopReason: 'error' | 'cancelled'`). The `workflow/*` events are observe-only emits carrying DATA SNAPSHOTS (id + meta; `workflow/end` omits the result value), per-listener contained, mirroring `subagent/start`/`subagent/end` — control stays with the run's holder. Vocabulary details: [core-data-structures/workflow.md](../../../core-data-structures/workflow.md).
+`ctx.workflows` is an abstract `WorkflowService` in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). `start(request)` throws synchronously for a script that cannot begin; a returned `WorkflowRun`'s `result` NEVER rejects (failures resolve as `stopReason: 'error' | 'cancelled'`). The `workflow/*` events are observe-only emits carrying DATA SNAPSHOTS (id + meta; `workflow/end` omits the result value), per-listener contained, mirroring `subagent/start`/`subagent/end` — control stays with the run's holder. Vocabulary details: [core-data-structures/workflow.md](../../../../docs/core-data-structures/workflow.md).
### The engine (dsh-workflow-workerthread): one worker thread per run
@@ -26,7 +26,7 @@ One deliberate strictness DIVERGENCE from CC: hook misuse — unknown or deferre
**Why `node:worker_threads`**: each run gets one unpooled worker. A vm context limits the documented script surface, while message-port RPC bridges `agent()` to host-side child loops. The worker prevents synchronous script work from blocking the host, provides a serialization boundary, and permits forced termination after cancellation. `isolated-vm` was rejected because of its maintenance state and deployment requirements.
-The host validates metadata and parses the body before publication. Private enum-keyed payload maps define the wire protocol; pending starts, published child records, one cancellation signal, worker-death reaping, result precedence, and disposal quiescence preserve the subagent run contract across it. The [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) owns those race algorithms.
+The host validates metadata and parses the body before publication. Private enum-keyed payload maps define the wire protocol; pending starts, published child records, one cancellation signal, worker-death reaping, result precedence, and disposal quiescence preserve the subagent run contract across it. The [agent-scope runtime-design Agent Note](../architecture/2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) owns those race algorithms.
The engine exposes an in-process `MessageChannel` test path because main-process V8 coverage cannot see worker execution.
@@ -44,7 +44,7 @@ A `workflow` tool mirroring `dsh-tool-subagent`'s synchronous shape: start, awai
An output schema makes a schema-valid committed capture mandatory for successful child completion. The scoped runtime presents the capture tool and instruction, commits only a successful final outcome—including the enclosing `run_code` outcome for an SDK call—denies later side effects after capture becomes pending, and stops the child without another model step after commit. A validation failure remains a retryable tool error; clean completion without a committed capture settles as an error.
-`StructuredOutputSchema` is the raw enforceable JSON-Schema subset in `dsh-tools` (single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`), and unsupported keywords fail loudly because that wire data becomes the capture tool's parameters verbatim. The [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) owns the assembly, commit, guard, and terminal-stop correctness algorithms.
+`StructuredOutputSchema` is the raw enforceable JSON-Schema subset in `dsh-tools` (single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`), and unsupported keywords fail loudly because that wire data becomes the capture tool's parameters verbatim. The [agent-scope runtime-design Agent Note](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) owns the assembly, commit, guard, and terminal-stop correctness algorithms.
## Testing
diff --git a/docs/rfc/implemented/feature/2026-07-05-skill-system.md b/.agents/notes/implemented/feature/2026-07-05-skill-system.md
similarity index 96%
rename from docs/rfc/implemented/feature/2026-07-05-skill-system.md
rename to .agents/notes/implemented/feature/2026-07-05-skill-system.md
index 0b74aa00ae..e59013c0a8 100644
--- a/docs/rfc/implemented/feature/2026-07-05-skill-system.md
+++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.md
@@ -1,4 +1,4 @@
-# RFC: Skill system — progressive disclosure instructions for agents
+# Agent Note: Skill system — progressive disclosure instructions for agents
Status: implemented
@@ -24,7 +24,7 @@ Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is lo
The `skill({ name })` tool loads one full skill for the current agent cwd and returns a tool result containing ``, ``, and ``. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills marked `disableModelInvocation` retain distinct tool errors. The tool result is the model-visible disclosure path.
-The data structures and catalog/tool contract are documented in [skills.md](../../../core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../cordis-catalog/services.md).
+The data structures and catalog/tool contract are documented in [skills.md](../../../../docs/core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../../docs/cordis-catalog/services.md).
## Alternatives considered
diff --git a/docs/rfc/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md
similarity index 62%
rename from docs/rfc/implemented/feature/2026-07-06-approval-seam.md
rename to .agents/notes/implemented/feature/2026-07-06-approval-seam.md
index 4ac066e393..af063895e2 100644
--- a/docs/rfc/implemented/feature/2026-07-06-approval-seam.md
+++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md
@@ -1,10 +1,10 @@
-# RFC: The approval seam — one-shot permission decisions over a waterfall of answerers
+# Agent Note: The approval seam — one-shot permission decisions over a waterfall of answerers
Status: implemented
## Problem
-Two callers need to put one question — "may this specific action proceed?" — to a human: `tools/pre-execute`'s `ask` decision (including the Claude-Code hook bridge's `permissionDecision: ask`) and the [sandbox RFC](2026-07-06-sandbox.md)'s post-denial one-shot escalation retry. A shared seam keeps them from inventing separate outcome vocabularies, UI routing, cancellation, and audit trails, while guaranteeing that a deployment with no UI can never grant an unanswerable request.
+Two callers need to put one question — "may this specific action proceed?" — to a human: `tools/pre-execute`'s `ask` decision (including the Claude-Code hook bridge's `permissionDecision: ask`) and the [sandbox Agent Note](2026-07-06-sandbox.md)'s post-denial one-shot escalation retry. A shared seam keeps them from inventing separate outcome vocabularies, UI routing, cancellation, and audit trails, while guaranteeing that a deployment with no UI can never grant an unanswerable request.
The routing problem is ownership: an approval prompt must reach the editor session that owns the asking agent (the ACP bridge multiplexes N sessions over one connection), fail closed for agents nobody owns (in-process subagents, tests), and stay out of deployments that compose no UI (headless, CI).
@@ -25,7 +25,7 @@ One `cordis.yml` entry mounts the seam. Not loading it is the fail-closed opt-ou
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the ACP app (`@deepseek-ai/dsh-acp-demo`, as in [the acp-agent example's default tree](../../../../examples/acp-agent/README.md)) completes the loop: its bridge registers an answerer that prompts the owning editor session via `session/request_permission`, so a hook's `ask` or an escalation request surfaces as a one-shot Allow/Reject prompt attached to the already-streamed tool call. `policy: never` is the unattended stance — every ask auto-rejects deterministically, stated in the system prompt, no human in the loop. `policy` is validated against the closed list at plugin load; anything else throws.
-What a composed deployment observes: `allowed-once` lets exactly that call proceed; rejection, dismissal, and channel absence deny with three distinct reasons the model can tell apart; every ask lands a durable `approval/asked`/`approval/decided` pair on the asking agent's session log; nothing about a grant persists past the call that asked.
+What a composed deployment observes: `allowed-once` lets exactly that call proceed; rejection, dismissal, and channel absence deny with three distinct reasons the model can tell apart; a successful in-turn request lands a durable `approval/asked`/`approval/decided` pair on the asking agent's session log; nothing about a grant persists past the call that asked. An idle request or audit append failure rejects instead of returning an unaudited decision.
One ask under this composition, verbatim from the sandbox example's recorded `escalation-approved` scenario — the model requests a sandbox escalation, the gate asks, the bridge prompts the owning editor, the user clicks Allow once:
@@ -49,45 +49,44 @@ The `escalation-rejected` twin ends in `{"outcome": "rejected"}` instead: nothin
#### The seam: mechanism and policy split
-After validation and an `approval/asked` append, `request()` resolves to `allowed-once`, `rejected`, `cancelled`, or `unavailable`. The service borrows the readonly request, runs the answerer waterfall, races cancellation, and normalizes thrown or invalid answers to `unavailable`. It then appends the matching `approval/decided`, paired by `ApprovalRequestId`.
+After validation and a successful `approval/asked` append, the service resolves the `approval/request` waterfall to `allowed-once`, `rejected`, `cancelled`, or `unavailable`. It borrows the readonly request identity and signal, treats abort as `cancelled`, contains answerer failures and invalid returns as `unavailable`, discards late answers, and appends the paired `approval/decided` event. Pre-commit audit failures reject; post-append observer failures cannot undo an authoritative event. `allowed-once` authorizes only the asked action, and `request()` rejects outside an open turn so the audit pair remains inside the durable commit boundary.
-Both audit events must be inside an open turn; acceptance or a pre-commit append failure rejects the request. Post-commit observers are contained by the session. `allowed-once` grants only the requested action, and the service retains no grant state.
+Answerers are `approval/request` waterfall listeners. Zero listeners fall through to `unavailable`; a recognizing listener occupies the first-wins decision slot, while an unrecognized agent must delegate with `next()`. Listeners dispose with their fibers, so an unloaded channel fails closed. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and reserves `prepend` for decide-or-delegate gates.
-Answerers are `approval/request` waterfall listeners. A listener returns an outcome for an agent it owns and calls `next()` otherwise. With no answerer, the default is `unavailable`; unloading a UI therefore fails closed without leaving a channel. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and uses `prepend` only for decide-or-delegate gates.
-
-`ApprovalRequest` carries the agent, tool name, optional `callId`, reason, and signal. The agent routes both the prompt and audit events. The request uses `dsh-llm`'s `CallId` without importing `dsh-tools`, avoiding a package cycle. Tool arguments are omitted because UI answerers attach to the already-rendered call.
+`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `CallId` brand without importing `dsh-tools`, which depends on this seam. Tool arguments stay on the already-streamed call that a UI references by `callId`.
#### Ask routing in dsh-tools
-`ToolRegistry.execute()` sends `ask` through the approval seam before the deny path. Only `allowed-once` proceeds; rejection, cancellation, and an unavailable channel produce distinct model-visible reasons. The registry looks up the optional service per call, so an absent or unloaded service fails closed without gating the registry fiber. Agent-less execution also fails closed because it cannot be routed or audited.
+`ToolRegistry.execute()` resolves `ask` before dispatch: `allowed-once` proceeds, while rejection, cancellation, and channel absence produce distinct deny reasons. Opportunistic `ctx.get('approval')` consumption lets an absent or unmounted service fail closed without gating the registry fiber. Agent-less execution also fails closed because it has neither an audit session nor a UI owner.
#### The per-session policy tier
-The seam owns the session policy `'ask' | 'never'`, following the switching contract in the [sandbox RFC](2026-07-06-sandbox.md). The effective session or config policy is applied before answerers: `'never'` rejects inside `request()`, while `'ask'` dispatches and falls through to `unavailable` when unanswered. The prompt states only deterministic `'never'`; the narrator reports switches, and every request still receives its audit pair.
+The seam also owns the session-scoped `'ask' | 'never'` policy described by [the sandbox Agent Note](2026-07-06-sandbox.md). Effective policy is folded from logged switches over the deployment default. `'never'` resolves to `rejected` inside `request()` before any answerer can run; `'ask'` dispatches and otherwise falls through to `unavailable`. The prompt states only deterministic `'never'`, switch narration is coalesced, and every request still records the audit pair.
#### The ACP answerer
-The ACP bridge finds the owning session, sends `session/request_permission` for the `callId`, and maps one-shot allow, reject, and cancel responses to the seam vocabulary. Unknown selections never grant. Foreign agents and requests without a `callId` delegate via `next()`; RPC failure becomes `unavailable`. The bridge answers requests but does not decide which calls require approval.
+The ACP bridge answers only for an exact agent object owned by its forward session map. It attaches `session/request_permission` to the existing `callId`, advertises one-shot allow/reject options, maps cancellation separately, and never grants an unknown option. Foreign or call-less requests delegate; a failed client RPC becomes `unavailable`. Hooks and `tools/pre-execute` decide whether a call asks at all.
-The answerer routes through the bridge's reverse-map ownership seam described by [the ACP support RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md), implementing the per-session permission ownership required by [the multi-session RFC](../../implemented/feature/2026-06-14-acp-multi-session.md).
+The answerer routes through the bridge's exact-agent ownership check described by [the ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md), implementing the per-session permission ownership required by [the multi-session Agent Note](2026-06-14-acp-multi-session.md).
#### Audit, and what the model sees
-`approval/asked` and `approval/decided` are durable log-only events. The model sees only the asker's logged `tool/result`. Every accepted request appends one matching decision, including cancellation and contained answerer failures.
+`approval/asked` and `approval/decided` are durable log-only events; the model sees only the ordinary tool result derived from the outcome. Successful completion commits one `decided` per `asked`, including cancellation and contained answerer failure. Idle requests append neither event; a pre-commit failure rejects, while failure of the second append can leave an already-committed `asked` unmatched.
#### Entities and dependencies
-`dsh-user-approval` owns the fixed dispatch-and-audit mechanism; `dsh-tools` asks and `dsh-acp` answers. Replaceable answerers remain listeners in their channel-owning plugins, so a three-package capability split would add an empty implementation layer. Sandbox executors remain transport-only, and static capability grants remain separate from interactive approval.
+`dsh-user-approval` depends on Cordis plus the session, agent, and branded-call contracts; `dsh-tools` and `dsh-acp` consume it. The sandbox executor stays independent because `dsh-tool-bash` owns escalation requests. The fixed dispatch-and-audit service remains one package; replaceable answerers live with their channel owners. Static capability grants and `subagent-acp` child-side permission answers remain separate concerns.
### Testing
-- **Unit/integration:** cover first-wins delegation, fail-closed defaults, malformed and throwing answerers, cancellation races and late-answer discard, audit pairing despite observer failures, unbypassable `'never'`, distinct tool-denial reasons, and ACP per-session routing/outcome mapping.
-- **Snapshot:** script permission answers through both sandbox escalation branches and pin the `'never'` prompt plus policy-switch notice. Hook-produced asks without a composed answerer remain covered as fail-closed denial.
+Unit tests pin outcomes, first-wins delegation, containment, cancellation, scoped routing, audit pairing, the unbypassable `'never'` policy, tool deny reasons, and ACP ownership/outcome mapping through a real scripted bridge.
+
+Snapshots record allowed and rejected sandbox escalation through `session/request_permission`, plus the `'never'` prompt and policy-switch notice. Unscripted permission prompts cancel and fail closed.
## Deferred
-- **`allow_always` grant storage** — honoring a persistent grant means designing storage, scope identity (call? path? prefix? session? time window?), and revocation; until designed, only the one-shot options are advertised ([the sandbox RFC](2026-07-06-sandbox.md) § Escalation records the open scope question).
-- **A recorded hook-produced ask with a composed answerer** — escalation records the human-prompt wire, while the current hook fixture pins the no-service denial; their combined producer/answerer path remains unit-covered.
+- **`allow_always` grant storage** — honoring a persistent grant means designing storage, scope identity (call? path? prefix? session? time window?), and revocation; until designed, only the one-shot options are advertised ([the sandbox Agent Note](2026-07-06-sandbox.md) § Escalation records the open scope question).
+- **A recorded hook-driven `ask` through a composed answerer** — the human-prompt wire is recorded through the sandbox example's escalation branches. The hook matrix's `hook-cc-pretool-ask` pins the no-ApprovalService fallback denial, while the hook-producer-plus-answerer composition remains on the unit tier.
- **Routing a child agent's approvals to the parent session** — `subagent-acp`'s child auto-answers its own `permission` requests; surfacing them to the parent's editor is its own design.
## Alternatives considered
@@ -101,16 +100,18 @@ The answerer routes through the bridge's reverse-map ownership seam described by
## Consequences
-- Only `allowed-once` dispatches an asked-about action; absent, rejected, cancelled, or failed answer paths deny.
-- Session ownership routes prompts, policy, and audit events without crossing editor sessions.
-- Accepted requests append one durable audit pair; the model sees only the resulting tool result.
-- A deployment without the service emits no approval prompt or audit events and denies every `ask` at the tool boundary.
+The implemented contract is pinned by the suites in Testing:
+
+- `allowed-once` dispatches one action; every other outcome denies with a distinct reason, and `'never'` rejects before prompting.
+- Missing, foreign, agent-less, throwing, invalid, and disconnected answer paths fail closed.
+- Successful requests route by exact agent ownership and append one replayable, model-invisible audit pair; idle and pre-commit failures reject.
+- ACP ownership keeps prompts inside their session, while a deployment without the service emits no prompt or audit events.
Costs and accepted limits:
- **Two decide-eager answerers race for the slot.** Sibling-plugin listener order is not deterministic, so the seam cannot referee competing terminal answerers — mitigated by convention (one terminal answerer per deployment; `prepend` only for decide-or-delegate gates) rather than a priority mechanism the event bus does not have.
- **Production exercise rests on one composition.** `ask` has two producer families — the hook bridges through `tools/pre-execute`, and sandbox escalation through its own gate — with the wire recorded in the sandbox example's snapshot suite, so the seam's real-world coverage is that one composition until more deployments compose it.
-- **Ownership keys on `Agent` object identity.** The answerer resolves sessions through the bridge's existing WeakMap; every current path hands the same object through the loop and the seams, but a future boundary that clones or proxies agents would make the bridge delegate and fail closed — safe, but silently UI-less — and would need session-id matching instead.
+- **Ownership keys on `Agent` object identity.** The answerer resolves the forward session-map record at `agent.session.id`, then requires that record to own the exact agent object; every current path hands the same object through the loop and the seams, but a future boundary that clones or proxies agents would make the bridge delegate and fail closed — safe, but silently UI-less — and would need a different ownership contract.
## FAQ
@@ -118,10 +119,10 @@ Costs and accepted limits:
- **Can a grant persist — "always allow this"?** No. `allowed-once` authorizes the single asked-about action and the service stores nothing between requests; `allow_always` is deliberately not advertised until grant storage is designed (§ Deferred).
- **What does the model see of an approval?** Only the tool result the asker derives from the outcome — the audit pair never enters the transcript. The three non-grant reasons are distinct, so the model can tell a human "no" from a dismissed prompt from a missing channel.
- **Who decides whether a call asks in the first place?** Policy producers: a hook returning `permissionDecision: ask`, any `tools/pre-execute` listener, or the sandbox escalation gate. The seam and the bridge only route and answer; neither injects its own judgment about what deserves a prompt.
-- **What happens when the user dismisses the prompt, or the turn aborts mid-ask?** Dismissal maps to `cancelled` with its own deny text. An already-aborted signal settles `cancelled` without dispatching; an abort during the ask discards the late answer — one audit pair either way, never two.
+- **What happens when the user dismisses the prompt, or the turn aborts mid-ask?** Dismissal maps to `cancelled` with its own deny text. An already-aborted signal settles `cancelled` without dispatching; an abort during the ask discards the late answer. When both audit appends commit, either path records one pair, never two.
- **What if the client answers with an option the harness never offered?** Any selection other than the offered `allow_once` maps to `rejected` — an unknown optionId from a non-conforming client can never grant.
- **How do subagents' approvals route?** An agent no answerer owns delegates through the whole waterfall and fails closed — in-process subagents are deliberately unanswerable. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent's editor is deferred (§ Deferred).
-- **What does `policy: 'never'` actually change at runtime?** The service resolves every ask for that session to `rejected` before dispatching any answerer (in-service, so no registration order can bypass it); the system prompt states the policy; switches are narrated at boundaries; the audit pair still lands for every auto-rejection.
+- **What does `policy: 'never'` actually change at runtime?** The service resolves every ask for that session to `rejected` before dispatching any answerer (in-service, so no registration order can bypass it); the system prompt states the policy; switches are narrated at boundaries; each successful auto-rejection records the audit pair.
- **What happens across a hot reload, or when the UI plugin unloads mid-session?** Answerers dispose with their owning fiber, so the next ask degrades to `unavailable` instead of hanging on a dead channel; remounting re-registers the answerer with no catch-up state.
- **Where does the user see what they are approving?** On the tool call itself: the prompt attaches to the already-streamed call via `callId` — arguments included — and adds the asker's human-readable `reason`; the request carries no argument copy of its own.
@@ -130,7 +131,7 @@ Costs and accepted limits:
In-repo precedents this design copies or contrasts with:
- The `fs/write-intent` gate (`packages/fs/fs/`) — the documented single-occupancy decision-slot waterfall semantics (first answer wins, delegate via `next()`) the answerer contract reuses.
-- `hook/invoked`/`hook/result` — the log-only audit-pair precedent `approval/asked`/`approval/decided` follows; [the hook-bridges RFC](2026-06-30-hook-bridges.md) ships `permissionDecision: ask`, the first producer.
-- [The interception-seams RFC](2026-06-30-interception-seams.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services.
-- [The ACP support RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) — the `WeakMap` ownership seam the answerer routes through; [the multi-session RFC](../../implemented/feature/2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements.
+- `hook/invoked`/`hook/result` — the log-only audit-pair precedent `approval/asked`/`approval/decided` follows; [the hook-bridges Agent Note](2026-06-30-hook-bridges.md) ships `permissionDecision: ask`, the first producer.
+- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services.
+- [The ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md) — the exact-agent ownership check against the forward session map that the answerer routes through; [the multi-session Agent Note](2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements.
- The opportunistic `ctx.get()` consumption pattern (`tool-bash`'s owner-token lookup, the loop's persistence probe) — how `dsh-tools` consumes the seam without gating its fiber on it.
diff --git a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md b/.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md
similarity index 85%
rename from docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md
rename to .agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md
index c56e1eb0ba..c78126e92a 100644
--- a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md
+++ b/.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md
@@ -1,4 +1,4 @@
-# RFC: Explicit model-facing tool order
+# Agent Note: Explicit model-facing tool order
Status: implemented
@@ -21,7 +21,7 @@ The system-prompt assembly owns the canonical model-facing tool order, exactly w
Scope is deliberately narrow: this fixes the REGISTRATION-ORDER race, not plugin behavior. A `system-prompt/assemble` listener may still add, remove, or rearrange tools — same as it may edit sections after their sort — and owns the determinism of what it emits; the waterfall contract already demands deterministic listeners (the reconstructability invariant would catch a listener that diverges between build and replay).
-Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it: the app configs (`dsh-stdio-demo`, `dsh-acp-demo`) accept the key and forward it through `dsh-agent-spine-demo` (whose schema is the intersection of the owners' schemas) to the `SystemPrompt` child. One schemastery footnote is load-bearing: a schemastery array defaults to `[]`, but an omitted `toolOrder` must stay ABSENT (= lexicographic) rather than become an explicitly-configured empty list (invalid — it lacks the rest entry), so every schema on the chain forces the default to `undefined`.
+Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it: the TUI, Headless, and ACP app configs accept the key and forward it through `dsh-agent-spine-demo` (whose schema is the intersection of the owners' schemas) to the `SystemPrompt` child. One schemastery footnote is load-bearing: a schemastery array defaults to `[]`, but an omitted `toolOrder` must stay ABSENT (= lexicographic) rather than become an explicitly-configured empty list (invalid — it lacks the rest entry), so every schema on the chain forces the default to `undefined`.
## Alternatives considered
@@ -32,13 +32,14 @@ Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it:
- **A `LlmService` config + `orderTools()` method the loop calls before logging the header** — works, but adds a public service method and a loop edit solely to apply a policy at a distance; every future request composer must remember the call. Canonicalizing where the list is born makes an unordered list unrepresentable, with zero new surface.
- **Normalizing inside `llm.stream()`** — runs after the header event is logged (the flake survives) and rebuilds the deep-frozen envelope, silently disarming the reconstruction invariant.
- **An exhaustive list (no rest entry)** — every newly loaded tool plugin would break boot; the mandatory rest entry keeps unlisted tools deterministic and their position explicit.
-- **A boot-time validation pass (a `SystemPrompt.assertToolOrderSatisfied()` called by `dsh-app-boot` after `loader.await()`)** — would turn the misconfiguration into a startup death instead of a first-turn failure, but costs a public service method plus a structural coupling from the generic boot glue to one service, and cannot replace the assembly-time check anyway (embedded callers never run app boot; registrations change after boot). No existing event can host the check either: cordis v4 has no ready-like event, `loader/entry-init`/`internal/status` fire mid-load (racy against tool registration, the very entropy this RFC kills), and the agent lifecycle events are no earlier than the assembly. One enforcement point at `assemble()` was judged worth the later failure moment.
+- **A boot-time validation pass (a `SystemPrompt.assertToolOrderSatisfied()` called by `dsh-app-boot` after `loader.await()`)** — would turn the misconfiguration into a startup death instead of a first-turn failure, but costs a public service method plus a structural coupling from the generic boot glue to one service, and cannot replace the assembly-time check anyway (embedded callers never run app boot; registrations change after boot). No existing event can host the check either: cordis v4 has no ready-like event, `loader/entry-init`/`internal/status` fire mid-load (racy against tool registration, the very entropy this Agent Note kills), and the agent lifecycle events are no earlier than the assembly. One enforcement point at `assemble()` was judged worth the later failure moment.
## Consequences
- Every registry-built assembly starts with a deterministic tool order on every host; absent an expert listener that deliberately changes it, every `request/header` event and model request inherits that order. The CI-vs-local registration-order flip is structurally gone, and the default is lexicographic.
- The initial `PromptAssembly.tools` is canonical, so waterfall listeners start from the model-facing order; provider registration order is observable nowhere before that cooperative seam.
-- A pure tool reordering between steps is representable only as a `request/header` `'fallback'` snapshot (the name-keyed `ToolsDelta` cannot express it); with a stable canonical order such reorders no longer occur in practice, so the fallback path stays a safety valve.
+- The snapshot suite's single pinned request-header fixture (`text-turn`) carries the new canonical tool order; every other ACP snapshot keeps the header bulk scrubbed as `{{system}}`/`{{tools}}`, per the pinned-header design.
+- A pure tool reordering between steps is logged like any other header change: a full `request/header` snapshot with reason `'change'`. Stable canonical order prevents registration timing from creating such changes in the ordinary path.
- The `toolOrder` key rides the app → `agent-core` → `SystemPrompt` forwarding chain, so deployments set it next to `persona` in the app config; `dsh-llm` and the agent loop are untouched.
- A misspelled or unloaded tool name in `toolOrder` fails the turn at prompt assembly, not the boot: the loop assembles inside the turn (after `turn/start`, before `step/start`), so the rejection reaches the turn's outer catch — the turn closes balanced with an `error` reason carrying the message, `agent/error` mirrors it, no step opens, no `request/header` is logged, no request reaches the adapter, and the agent returns to idle. Every turn fails identically until the config is fixed; the process itself stays up (matching the repo rule that explicit config references must not be silently ignored — the enforcement point is the assembly because no earlier universal moment exists).
- A tool provider that returns the reserved rest-entry name has the same prompt-assembly failure shape as an unknown listed name. This keeps the sentinel from becoming an ambiguous real tool and preserves the "never drops a tool" ordering contract.
diff --git a/docs/rfc/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md
similarity index 88%
rename from docs/rfc/implemented/feature/2026-07-06-sandbox.md
rename to .agents/notes/implemented/feature/2026-07-06-sandbox.md
index 68208b1d66..2cc6516523 100644
--- a/docs/rfc/implemented/feature/2026-07-06-sandbox.md
+++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md
@@ -1,4 +1,4 @@
-# RFC: The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes
+# Agent Note: The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes
Status: implemented
@@ -12,7 +12,7 @@ Confinement alone leaves two gaps. A denial with no escalation path is terminal
## Decision
-One seam, one per-platform chain of local backends, one consumer, and two levers on top: a per-call escalation path and per-session runtime modes. Everything below composes from the leaf `cordis.yml`; nothing touches `agent-loop`. The scope is deliberately bounded: the phases this RFC names but does not design — per-session workspace root, cross-family fs enforcement, the `subagent-acp` consumer, more environments, a Windows chain — are listed under § Deferred phases, each a follow-up design, not a config knob.
+One seam, one per-platform chain of local backends, one consumer, and two levers on top: a per-call escalation path and per-session runtime modes. Everything below composes from the leaf `cordis.yml`; nothing touches `agent-loop`. The scope is deliberately bounded: the phases this Agent Note names but does not design — per-session workspace root, cross-family fs enforcement, the `subagent-acp` consumer, more environments, a Windows chain — are listed under § Deferred phases, each a follow-up design, not a config knob.
### How a deployment uses it
@@ -27,7 +27,7 @@ Four `cordis.yml` entries turn an unconfined coding agent into the sandboxed pro
mode: workspace-write # the deployment default every session starts from
workspaceRoot: !!js process.cwd() # the boundary workspace-write may write under
- id: approval
- name: '@deepseek-ai/dsh-user-approval' # the escalation gate's channel (the approval RFC)
+ name: '@deepseek-ai/dsh-user-approval' # the escalation gate's channel (the approval Agent Note)
config:
policy: ask
- id: permission
@@ -62,9 +62,7 @@ Left open, for the phase that needs them: whether network restriction arrives as
The launcher is a ~300-line C program (plain C11 over the raw Landlock UAPI — no libraries beyond a statically linked musl, so the audit surface is that one file plus the kernel's stable syscall contract): `--ro ` / `--rw ` grants, `--`, the wrapped argv; it installs the ruleset on itself and `exec`s (rulesets are inherited across `execve`, and it sets `no_new_privs` before restricting); `--probe` enforces a maximal ruleset in a short-lived child and exits 0 only when the kernel actually enforces; launcher failures exit 125 without exec'ing.
-The Landlock launcher ships through [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run), with platform binaries selected by npm. That package owns path resolution, probing, and CLI flags; the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned.
-
-FIXME: Revisit the separate-repository boundary and try to maintain the launcher source and its platform package family inside this monorepo, so the native release surface and harness contract evolve together.
+The Landlock launcher source and package workspace live at `native/landlock-run`, next to the harness consumers. The standalone [`node-addon-landlock-run`](https://github.com/deepseek-harness/node-addon-landlock-run) repository is the release mirror used to pack and publish the npm package family; `native/README.md` owns the export procedure. Platform binaries are selected by npm, and the entry package owns path resolution, probing, and CLI flags while the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned.
Backend profiles share the mode contract but differ in necessary host grants. Landlock and Seatbelt allow only `/dev/null` in read-only mode; workspace-write also permits their required host temp roots. Each wrap carries backend-specific denial signatures. Landlock reports partial enforcement on older ABIs that cannot govern every operation, while successful bwrap and Seatbelt profiles report full enforcement.
@@ -98,12 +96,12 @@ The default is composition config (`cordis.yml`) — operator-owned, process-wid
```ts
interface SessionEventMap {
- 'bash/sandbox-mode': { mode: 'read-only' | 'workspace-write' | 'danger-full-access' }
+ 'sandbox/mode': { mode: 'read-only' | 'workspace-write' | 'danger-full-access' }
'approval/policy': { policy: 'ask' | 'never' }
}
```
-Each owner exports the same three-piece kit: the event declaration, a pure fold (`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)` — a `findLast`, typed to the domain's closed union), and THE write path (`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)` — a switch IS its event; nothing mutates state out of band). No shared owner service, no generic facts map, no registry: a third knob copies the ~40-line pattern into its own package. Execution follows the fold on both sides — the bash tool's per-call stamp reads it as the middle rung of the § Escalation precedence chain, and the approval seam's `'never'` gate is [the approval RFC](2026-07-06-approval-seam.md)'s side of the same pattern.
+Each owner exports the same three-piece kit: the event declaration, a pure fold (`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)` — a `findLast`, typed to the domain's closed union), and THE write path (`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)` — a switch IS its event; nothing mutates state out of band). No shared owner service, no generic facts map, no registry: a third knob copies the ~40-line pattern into its own package. Execution follows the fold on both sides — the bash tool's per-call stamp reads it as the middle rung of the § Escalation precedence chain, and the approval seam's `'never'` gate is [the approval Agent Note](2026-07-06-approval-seam.md)'s side of the same pattern.
Sandbox mode is not narrated in the prompt; denial results report the mode when it matters, avoiding preemptive refusal based on a standing label. Approval policy is different: only `'never'` is stated because automatic rejection otherwise looks like a user decision. Policy-change notices are coalesced and delivered by the next pre-step, with log-derived fallback after restart. The notice source is inferred from event position: a knob event after the last request header is user-driven; unlogged drift is operator or config driven.
@@ -113,9 +111,7 @@ Sandbox mode is not narrated in the prompt; denial results report the mode when
#### In-process tools
-fs/web/todo execute in-process, so their sandbox semantics are policy at their seams: the fs intent gates deciding by the shared mode vocabulary (§ Deferred phases, cross-family) make `read-only` a real boundary instead of a bash-only approximation — until then the contract says so honestly. No generic per-tool sandbox runtime: a host-mediated tool leaves the process only by returning declarative effects the host validates, which is a rewrite, not a wrapper.
-
-FIXME: Revisit this tool-local boundary. The follow-up design needs to determine whether sandboxing becomes a global harness capability that applies uniformly to every tool, instead of expressing in-process enforcement independently at each tool seam.
+fs/web/todo execute in-process, so their sandbox semantics are policy at their seams. The fs seam now enforces the shared mode vocabulary through a sandboxed provider (`dsh-fs-sandbox` fences write/edit by mode; see [the cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)), so `read-only`/`workspace-write` are real boundaries for the filesystem tools, not a bash-only approximation. web/todo remain unfenced (web's only effect is network, outside the file-effect mode vocabulary). No generic per-tool sandbox runtime: a host-mediated tool leaves the process only by returning declarative effects the host validates, which is a rewrite, not a wrapper — the follow-up settled on one shared policy home (`ctx.sandboxPolicy`) with per-seam enforcement, not a uniform wrapper.
### Testing
@@ -128,8 +124,7 @@ FIXME: Revisit this tool-local boundary. The follow-up design needs to determine
Each phase gets its full design when picked up, validated against the code at that time, and lands with unit, real-API e2e, and snapshot coverage at the tiers it touches.
-- **Per-session workspace root** — the executor's write boundary stays config-fixed for its lifetime while each ACP session has its own cwd; a per-session root rides the same per-call policy carrier once designed.
-- **Cross-family boundary** — the fs intent gates decide by the shared mode, making `read-only`/`workspace-write` real boundaries beyond bash.
+- **Per-session workspace root** — the executor's write boundary stays config-fixed for its lifetime while each ACP session has its own cwd; a per-session root rides the same per-call policy carrier once designed. Centralizing the root on `ctx.sandboxPolicy` (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)) is the groundwork.
- **Second consumer** — `subagent-acp` optionally confines child agents (per-call policy; unconfined default — a child agent must write its own persistence).
- **More environments** — an environment-coherent capability group example (e.g. bash+fs against one container).
- **Windows chain** — `PLATFORM_CHAINS.win32` is reserved and empty (fail-closed); filling it means a confinement runner from the AppContainer/restricted-token family, shipped from its own repository on the `node-addon-landlock-run` template, plus its profile dialect and denial/runner-failure signatures.
@@ -155,7 +150,7 @@ Each phase gets its full design when picked up, validated against the code at th
- **A generic `env/state` facts map with an owner service** — rejected: approval and sandbox compose independently, so neither's state may drag in a third package; single-key folds are one `findLast` each, dissolving the owner service; no invariant spans the knobs, so atomic multi-key patches bought nothing.
- **Narrate via `agent/user-message` + a bus event** — rejected: it presupposes a turn-entry seam that does not exist (the real seam is `agent/prompt-submit`), and pre-step's position serves both the coalesced turn-entry notice and the mid-turn immediacy bound with one listener.
- **A standing prompt statement of the sandbox mode (+ a switch narrator)** — shipped first, then removed on live evidence: with `Bash commands run under the "read-only" file sandbox.` in every request, the model refused to ATTEMPT denied-then-escalatable work (five of twelve turns in the first manual session ended with zero tool calls), turning the sandbox into a soft lockout. The denial marker names the mode at the moment it matters and the escalation fields carry the recovery; the approval knob keeps its statement because an auto-rejection is behaviorally indistinguishable from a human "no".
-- **Track "last told" with its own bookkeeping events** — rejected: the `request/header*` fold already records the exact prompt the model saw; parsing the closed candidate sentences back replaces a second bookkeeping stream — events are needed only where they ARE the store.
+- **Track "last told" with its own bookkeeping events** — rejected: the `request/header` fold already records the exact prompt the model saw; parsing the closed candidate sentences back replaces a second bookkeeping stream — events are needed only where they ARE the store.
- **ACP session modes instead of config options** — rejected: the preset is already one deployment-defined config-option select, and modes are slated for removal in ACP v2.
## Consequences
@@ -173,7 +168,7 @@ What shipped pins — the tiers in Testing hold each:
Costs and accepted limits:
- **The one-wrapper illusion is given up knowingly.** A `tools/pre-execute` wrapper plus prompt conventions does not solve sandbox approval — the correct design costs structured denials, native runner probes, per-call policy carriage, and consistent cross-family enforcement, and this design pays it.
-- **`read-only` is not yet a cross-family boundary.** Until the fs intent gates decide by the shared mode, the claim holds for bash only; the contract says so honestly (§ In-process tools).
+- **`read-only` became a cross-family boundary through a follow-up.** This RFC shipped bash-only enforcement; the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md) extends the same mode vocabulary to the filesystem tools through a sandboxed `ctx.fs` provider and relocates the mode/root config and the `sandbox/mode` override to `ctx.sandboxPolicy` (§ In-process tools).
- **Windows has no backend.** Its chain slot is reserved empty — fail-closed, never a fallthrough; filling it is a deferred phase.
- **The Seatbelt rung leans on Apple's deprecated-but-shipped `sandbox-exec` CLI.** As darwin's sole candidate it is selected without probing, so a future removal surfaces at execution as the runner-failure classification — re-thrown `SANDBOX_UNAVAILABLE`, the command never runs; fail closed, never open.
- **Landlock confinement is only as complete as the running kernel's ABI.** Reported as `enforcement: 'partial'` rather than refused — the deliberate trade that keeps the fallback available on older-kernel hosts.
@@ -193,9 +188,9 @@ Costs and accepted limits:
- **What happens on a platform with no backend — Windows today?** `confine()` throws the fail-closed `SANDBOX_UNAVAILABLE` and the command never spawns; `win32` is a reserved EMPTY chain, pinned by test to fail closed identically until a Windows runner fills it (§ Deferred phases).
- **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the registry-installed Landlock launcher, and the verdict is cached for the provider's lifetime.
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
-- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively. fs/web/todo execute in-process, where an `execve` wrapper is mechanically meaningless; their `read-only` semantics arrive with the cross-family deferred phase, and until then the contract says bash-only honestly.
+- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary).
- **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry.
-- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next turn's `agent/prompt-submit`, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode.
+- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next `agent/prompt-submit` inside its open turn, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode.
- **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline changes behavior the same way a switch does (the approval policy, being stated, is additionally narrated with operator/config attribution).
- **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`.
@@ -203,8 +198,8 @@ Costs and accepted limits:
In-repo precedents this design copies or contrasts with:
-- [The capability-seams RFC](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
-- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../core-data-structures/bash.md)) — the per-call carrier template `sandboxMode` rides, and the explicit-`resolve()` defaulting convention.
-- [The approval seam RFC](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there.
+- [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
+- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../../docs/core-data-structures/bash.md)) — the per-call carrier template `sandboxMode` rides, and the explicit-`resolve()` defaulting convention.
+- [The approval seam Agent Note](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there.
- [Event-sourced sessions](../architecture/2026-06-11-event-sourced-sessions.md) and [the turn-enclosure invariant](../architecture/2026-06-15-turn-enclosure-invariant.md) — the log-as-store foundation the per-session modes fold over, and the commit boundary the anchoring design obeys.
-- [The interception-seams RFC](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own).
+- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own).
diff --git a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.md b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
similarity index 96%
rename from docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.md
rename to .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
index 1be5b225fe..95cad58b46 100644
--- a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.md
+++ b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md
@@ -1,4 +1,4 @@
-# RFC: MCP client plugin — connect to external MCP servers and bridge their tools
+# Agent Note: MCP client plugin — connect to external MCP servers and bridge their tools
Status: implemented
@@ -12,7 +12,7 @@ The `ToolRegistry` already accepts raw JSON Schema tool definitions (documented
### Package
-A single package `@deepseek-ai/dsh-mcp-client` at `packages/mcp/mcp-client/`. No capability-seam three-package split — there is no foreseeable second MCP client implementation, and the convention is "don't split preemptively" ([capability seams RFC](../../implemented/architecture/2026-06-13-capability-seams.md)).
+A single package `@deepseek-ai/dsh-mcp-client` at `packages/mcp/mcp-client/`. No capability-seam three-package split — there is no foreseeable second MCP client implementation, and the convention is "don't split preemptively" ([capability seams Agent Note](../architecture/2026-06-13-capability-seams.md)).
### SDK
@@ -141,7 +141,7 @@ A unified `execute` handler for all tools from one MCP server:
1. Resolve `rawName` (the executor closes over it) and call `client.callTool({ name: rawName, arguments }, { signal: exec.signal })` with the configured timeout — the public name is never sent to the server.
2. Map the result:
- Multiple `text` content blocks → join with `'\n'` into a single `TextBlock` (required: `flattenText` uses `join('')` without separator, so multiple blocks would lose inter-block boundaries).
- - `image` content blocks → discard with a `ctx.logger.warn` (the harness has no image content block type; [drop-image RFC](../../implemented/simplification/2026-07-04-drop-image-content-block.md)).
+ - `image` content blocks → discard with a `ctx.logger.warn` (the harness has no image content block type; [drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md)).
- `isError: true` → map to the harness `isError` result path (`{ content: [...], isError: true }`).
3. Cancellation: `exec.signal` (from the agent loop's cancel) is passed through to the MCP SDK's `callTool`, which sends `$/cancelRequest` to the server.
@@ -199,7 +199,7 @@ Coverage is named per tier; each behavior lives at the cheapest tier that can ex
- **Unit** (`tests/mcp-client.spec.ts`, `tests/apply.spec.ts`, mocked MCP SDK): the `publicToolName` algorithm (clean, normalize, truncate-and-hash, determinism, distinct-identity separation), raw-vs-public wire discipline, cross-server and native-tool coexistence, duplicate-`serverName` load failure and reservation release, invalid-tool-list rejection, generation swap/rollback, failed-re-sync retention, result mapping, cancellation, config schema validation. 100% per-file coverage gates the package.
- **E2E** (`tests/mcp-client.e2e.ts`, keyless): the real MCP protocol against the in-repo fixture server, `@modelcontextprotocol/server-everything`, and `@modelcontextprotocol/server-filesystem` over stdio, and against an in-process `StreamableHTTPServerTransport` server over Streamable HTTP — discovery under the namespace, dotted-name normalization end to end, execution round-trips, duplicate-`serverName` rejection, disposal.
-- **Snapshot**: deliberately none. MCP tools introduce no new transcript surface — they register as raw `ToolDefinition`s and render through the ACP bridge's generic-card fallback, which the bridge's unit suite already pins (`packages/ui/acp/tests/stream-update.spec.ts`). Adding an MCP server to the snapshot example's `cordis.yml` would mutate the pinned `text-turn` system-prompt fixture (forcing a with-key re-record of every recorded golden) and make every replay depend on spawning an external MCP server process — for zero new rendering behavior. If a later change gives MCP tools their own render intent, that change names its snapshot coverage then.
+- **Snapshot**: deliberately none. MCP tools introduce no new transcript surface — they register as raw `ToolDefinition`s and render through the ACP bridge's generic-card fallback, which the bridge's unit suite already pins (`packages/ui/acp/tests/stream-update.spec.ts`). Adding an MCP server to the snapshot example's `cordis.yml` would mutate the pinned `text-turn` system-prompt fixture (forcing a with-key re-record of every recorded expected output) and make every replay depend on spawning an external MCP server process — for zero new rendering behavior. If a later change gives MCP tools their own render intent, that change names its snapshot coverage then.
## Consequences
diff --git a/docs/rfc/implemented/feature/2026-07-07-plan-mode.md b/.agents/notes/implemented/feature/2026-07-07-plan-mode.md
similarity index 85%
rename from docs/rfc/implemented/feature/2026-07-07-plan-mode.md
rename to .agents/notes/implemented/feature/2026-07-07-plan-mode.md
index 9ba86a8c7f..e605e8c9ec 100644
--- a/docs/rfc/implemented/feature/2026-07-07-plan-mode.md
+++ b/.agents/notes/implemented/feature/2026-07-07-plan-mode.md
@@ -1,10 +1,10 @@
-# RFC: Plan mode — a logged per-agent session mode
+# Agent Note: Plan mode — a logged per-agent session mode
Status: implemented
## Problem
-The harness has no way to put an agent into a distinct working stance. The canonical feature that needs one is plan mode — the agent explores and designs under a planning stance, produces a reviewable plan, and crosses back through an explicit approval. [The extension cookbook](../../../cookbook/extension-cookbook.md) already reserves the row ("Plan mode — `tools/pre-execute` (deny writes) + a mode prompt section"), and [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records session modes as a known gap both reference adapters ship. Neither says where the mode STATE lives, how it survives resume and fork, or how its model-visible consequences stay honest with the session log.
+The harness has no way to put an agent into a distinct working stance. The canonical feature that needs one is plan mode — the agent explores and designs under a planning stance, produces a reviewable plan, and crosses back through an explicit approval. [The extension cookbook](../../../../docs/cookbook/extension-cookbook.md) already reserves the row ("Plan mode — `tools/pre-execute` (deny writes) + a mode prompt section"), and [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records session modes as a known gap both reference adapters ship. Neither says where the mode STATE lives, how it survives resume and fork, or how its model-visible consequences stay honest with the session log.
Every shipped plan mode decomposes into the same five parts — a low-authority tool policy, a plan artifact, an approval moment, an execution-state switch, and durable state ([Prior art](#prior-art) carries the survey). Four of the five already exist here as gated infrastructure: what the model is TOLD it can do is shaped per step at [`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) and whatever ships is logged as `request/header*` events ([reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md)); what can RUN is gated at `tools/pre-execute` with typed decisions ([interception seams](../../implemented/feature/2026-06-30-interception-seams.md)); the approval moment is a human answer over the user-interaction seam ([`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md), the [ask-user precedent](../../implemented/feature/2026-06-25-ask-user-question.md)); durable per-agent facts are `SessionEventMap` members ([the `todo/write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)). The missing fifth is the mode itself: a named, durable, per-agent state its listeners can read.
@@ -14,7 +14,7 @@ The deliverable is **plan mode**. It ships as the first **session mode** — a n
The state is one `SessionEventMap` member: **`mode/set`**, a log-only, non-surface event carrying `{ mode: string }` with whole-value-replace semantics, plus a pure `foldMode(events)` that returns the mode in force — the last `mode/set`, or the default mode when none exists. Because [the log is the fact channel](../../implemented/architecture/2026-06-30-event-domain-semantics.md), resume, fork, and compaction restore the mode with no extra machinery, and UIs read flips off `session/event`. The default mode is the absence of policy — no section, no filtering, no gate — so an agent that never sees a `mode/set` behaves byte-identically to a deployment that never loads `dsh-mode`, which keeps every existing snapshot golden stable and makes the plugin safe to compose unconditionally.
-A mode's whole surface is soft: a `system-prompt/assemble` listener renders the mode's guidance section and shows the `exit_plan_mode` tool IFF the folded mode is `plan` — every transition therefore surfaces as an attributable `request/header` event on the next step (a delta when expressible, the full fallback snapshot otherwise), keeping the [reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) invariant green by construction. A mode deliberately enforces NOTHING: no execution gate, no tool filtering, no reach into the sandbox or approval knobs — a user who wants a hard read-only floor while planning switches the sandbox-mode option beside the mode picker, in either order, and neither axis disturbs the other. There is likewise NO per-mode tool allow/deny list — which tools a mode admits is an effects question, parked until tool definitions declare their effects ([Deferred](#deferred)); a mode's restraint is its section's guidance plus the exit review.
+A mode's whole surface is soft: a `system-prompt/assemble` listener renders the mode's guidance section and shows the `exit_plan_mode` tool IFF the folded mode is `plan` — every transition therefore surfaces as an attributable complete `request/header` event on the next step, keeping the [reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) invariant green by construction. A mode deliberately enforces NOTHING: no execution gate, no tool filtering, no reach into the sandbox or approval knobs — a user who wants a hard read-only floor while planning switches the sandbox-mode option beside the mode picker, in either order, and neither axis disturbs the other. There is likewise NO per-mode tool allow/deny list — which tools a mode admits is an effects question, parked until tool definitions declare their effects ([Deferred](#deferred)); a mode's restraint is its section's guidance plus the exit review.
The model leaves plan mode through the **`exit_plan_mode`** tool: its single argument is the plan text, which makes the plan a durable log artifact, and the tool conducts the review itself through the user-interaction seam — a question with options and a free-text channel, not a bare permission — so an approval flips the logged mode back to the default, and a rejection becomes the corrective error carrying the user's feedback verbatim, which keeps the model planning with direction. A user flips the mode from any surface through `ctx.modes.set()`; the flip is applied at the next turn boundary (session events are turn-enclosed) and narrated to the model once, only when the model-visible state actually changed.
@@ -69,7 +69,7 @@ The mode PICKER is this package's surface: `session/new`/`session/load` advertis
DEFAULT_MODE = 'default' // the fold of a log with no mode/set; reserved, not definable
```
-The payload carries no reason/provenance field: a tool-driven flip sits next to its `tool/call` in the log and a user flip sits at its turn boundary, so the cause is log-adjacent — the same "narrative fields are derivable" call the [reconstructability RFC](../../implemented/architecture/2026-07-05-reconstructable-requests.md) made for header deltas (the in-flight `env/state` event carries a `source` precisely because its drift variant has NO log-adjacent cause — a contrast, not a conflict). Mode names are config-declared vocabulary, not opaque cross-boundary ids, so they stay bare strings (no `Branded`).
+The payload carries no reason/provenance field: a tool-driven flip sits next to its `tool/call` in the log and a user flip sits at its turn boundary, so the cause is log-adjacent — the same "narrative fields are derivable" call the [reconstructability Agent Note](../architecture/2026-07-05-reconstructable-requests.md) made for header deltas (the in-flight `env/state` event carries a `source` precisely because its drift variant has NO log-adjacent cause — a contrast, not a conflict). Mode names are config-declared vocabulary, not opaque cross-boundary ids, so they stay bare strings (no `Branded`).
### Config and the resolve step
@@ -86,11 +86,11 @@ The one-field shape is deliberate minimalism, not the final vocabulary: a per-to
`foldMode(events)` is pure (exported for reconstructors and tests); the service tracks it per session with a lazy cursor in a `WeakMap` — O(new events) per read, never invalidated, because the log is append-only and `mode/set` is not a surface node (compaction cannot rewrite it). `set(agent, mode)` validates the name against `list()`'s vocabulary — the configured definitions plus the reserved `default`, which is rejected as a config KEY but always accepted as a `set()` TARGET (a picker's exit-to-default must be a valid write) — drops a no-op (target equals pending ?? current), and otherwise records the intent in a `WeakMap` — it cannot append immediately, because [every session event is turn-enclosed](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md) and an idle agent has no open turn.
-Contained listeners on the loop's interception seams ([defensive patterns](../../../defensive-patterns.md): a policy plugin must not block a prompt or a turn) flush the pending intent as a `mode/set` append — `agent/prompt-submit` fires inside the just-opened turn before its first assembly, `agent/turn-continuation` after each step closed before the next assembly, and both sit outside the step's tool-execution window AND outside any log emit (post-commit `session/event` observers are observe-only; an append from one would re-enter the publishing append), so the executions of a step always run under the mode its assembly folded. When the flushed mode differs from the fold at the last `request/header`, the flush appends one coalesced `context/message` notice in the same frame ("The user switched this session to plan mode."); the user-visible narration cases are enumerated in the [FAQ](#faq). Seeding rides `agent/created`: `AgentOptions.mode` becomes a pending intent, so explicit options beat the logged baseline on create AND resume — the same precedence the call-config seed follows.
+Contained listeners on the loop's interception seams ([defensive patterns](../../../../docs/defensive-patterns.md): a policy plugin must not block a prompt or a turn) flush the pending intent as a `mode/set` append — `agent/prompt-submit` fires inside the just-opened turn before its first assembly, `agent/turn-continuation` after each step closed before the next assembly, and both sit outside the step's tool-execution window AND outside any log emit (post-commit `session/event` observers are observe-only; an append from one would re-enter the publishing append), so the executions of a step always run under the mode its assembly folded. When the flushed mode differs from the fold at the last `request/header`, the flush appends one coalesced `context/message` notice in the same frame ("The user switched this session to plan mode."); the user-visible narration cases are enumerated in the [FAQ](#faq). Seeding rides `agent/created`: `AgentOptions.mode` becomes a pending intent, so explicit options beat the logged baseline on create AND resume — the same precedence the call-config seed follows.
### The soft layer: a computed section and a narrow post-`next()` filter
-A `system-prompt/assemble` waterfall listener reads the calling agent's mode (the `AssembleContext` carries `agent`) and, in a non-default mode, appends the mode's guidance section. The loop already renders per step and logs the result: entering or leaving a mode surfaces on the next step as a `request/header-delta` — or as the full `request/header` fallback snapshot when the change is inexpressible in the delta encoding (adding `exit_plan_mode` at its canonical alphabetical position is a non-tail insertion the append-only tools delta cannot express; removing it on exit is a pure removal and DOES delta) — so every mode transition is an attributable log fact. The section is static per mode and the plan itself stays in the conversation (messages and tool args, already in context), so a mode does not add per-step prompt churn — re-injecting plan state into every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary and would only burn prefix cache.
+A `system-prompt/assemble` waterfall listener reads the calling agent's mode (the `AssembleContext` carries `agent`) and, in a non-default mode, appends the mode's guidance section. The loop already renders per step and logs the result: entering or leaving a mode surfaces on the next step as a complete changed `request/header`, so every mode transition is an attributable log fact. The section is static per mode and the plan itself stays in the conversation (messages and tool args, already in context), so a mode does not add per-step prompt churn — re-injecting plan state into every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary and would only burn prefix cache.
The guidance section is an ordinary registered section, `{ name: 'mode:policy', order: 50, text: context => … }` — order 50 sits after the persona (0) and before tool guidance (100–199); it resolves to the folded mode's configured text and to `''` (dropped at render) for the default mode or an agent-less assembly. The same listener carries the ONE tool rule this plugin has, deliberately narrow (`prepend: true`, so it wraps outside every append-registered listener's post-`next()` mutation): `exit_plan_mode` is visible IFF the agent's folded mode is `plan` — which keeps a default-mode assembly byte-identical to a no-`dsh-mode` deployment even though the tool is always registered. Everything else passes through untouched: which tools a mode admits is not this plugin's decision. Code Mode's soft surface is the `tools:sdk` section rather than the wire schemas, and section text resolves in assemble's base, so the same wrapper re-renders that section under the same visibility rule; the default mode re-renders it too, hiding only the exit binding, keeping a default-mode Code Mode assembly byte-identical to a deployment that never loaded `dsh-mode`. There is NO `tools/pre-execute` listener: a mode gates nothing (the exit tool's own folded-mode recheck rejects an out-of-plan call), and the registry's `ask` vocabulary stays free for genuine permission gating — the exit review is a question with options and feedback, not a permission, so it lives inside the tool's own execution over the user-interaction seam.
@@ -106,7 +106,7 @@ Its [render intent](../../implemented/architecture/2026-07-02-tool-render-intent
### The recorded scenario and the harness op
-`input.json` gains one step op, `{ "op": "setMode", "modeId": "plan" }`, driven through the real `session/set_mode` RPC, and a scripted `elicitationAnswers` queue (FIFO, consumed by the harness client's elicitation callback — the review question's answer). The `plan-mode` scenario: initialize → newSession → setMode(plan) → a prompt the model answers by running a real `cat` through the bash tool INSIDE plan (the composition's sandbox applies exactly as in default mode; on replay the command re-executes under the host runner — Seatbelt on macOS, bwrap on Linux CI) and presenting the plan via `exit_plan_mode` → a scripted approve → the same turn's next step edits for real. Because the mode is set before turn 1, the FIRST `request/header` snapshot is already in plan shape (the full toolset plus the exit tool, the mode section, reason `initial`); the approved exit logs the pure-removal `request/header-delta`, and the scenario pins both, plus the `mode/set` pair. A sibling `plan-mode-reject` scenario scripts the keep-planning answer with feedback text and pins the corrective result. Both need a with-key recording session; the sandbox-denial marker, whose recorded stderr would be the backend's dialect and replay only where it was recorded, stays pinned at `dsh-tool-bash`'s unit tier.
+`input.json` gains one step op, `{ "op": "setMode", "modeId": "plan" }`, driven through the real `session/set_mode` RPC, and a scripted `elicitationAnswers` queue (FIFO, consumed by the harness client's elicitation callback — the review question's answer). The `plan-mode` scenario: initialize → newSession → setMode(plan) → a prompt the model answers by running a real `cat` through the bash tool INSIDE plan (the composition's sandbox applies exactly as in default mode; on replay the command re-executes under the host runner — Seatbelt on macOS, bwrap on Linux CI) and presenting the plan via `exit_plan_mode` → a scripted approve → the same turn's next step edits for real. Because the mode is set before turn 1, the FIRST `request/header` snapshot is already in plan shape (the full toolset plus the exit tool, the mode section, reason `initial`); the approved exit logs a complete changed `request/header`, and the scenario pins both, plus the `mode/set` pair. A sibling `plan-mode-reject` scenario scripts the keep-planning answer with feedback text and pins the corrective result. Both need a with-key recording session; the sandbox-denial marker, whose recorded stderr would be the backend's dialect and replay only where it was recorded, stays pinned at `dsh-tool-bash`'s unit tier.
### The mechanical tail
@@ -114,7 +114,7 @@ No new cordis event is declared (`mode/set` rides `session/event`; the listeners
## Deferred
-Each behind its own decision: subagent mode inheritance via a forwarded `AgentOptions.mode` (the option field itself ships), preset modes beyond `plan` (read-only, accept-edits), the idle-record primitive if pending-intent loss proves real, and — the big one — **effects self-declaration on tool definitions**: a per-tool read-only/mutating classification (the MCP `ToolAnnotations` vocabulary — `readOnlyHint`/`destructiveHint` — is the natural template, with its untrusted-hint caveat implying trust tiers). That item is what a general per-mode tool policy waits on: this RFC first shipped an interim per-mode name allowlist and removed it before release — a hand-maintained list mislabels the effects question, must track every tool a deployment composes, and rots silently as tools arrive — so mode-scoped tool availability (and per-tool `ask` policies) returns as a CONSUMER of declared effects, which is its restart trigger.
+Each behind its own decision: subagent mode inheritance via a forwarded `AgentOptions.mode` (the option field itself ships), preset modes beyond `plan` (read-only, accept-edits), the idle-record primitive if pending-intent loss proves real, and — the big one — **effects self-declaration on tool definitions**: a per-tool read-only/mutating classification (the MCP `ToolAnnotations` vocabulary — `readOnlyHint`/`destructiveHint` — is the natural template, with its untrusted-hint caveat implying trust tiers). That item is what a general per-mode tool policy waits on: this Agent Note first shipped an interim per-mode name allowlist and removed it before release — a hand-maintained list mislabels the effects question, must track every tool a deployment composes, and rots silently as tools arrive — so mode-scoped tool availability (and per-tool `ask` policies) returns as a CONSUMER of declared effects, which is its restart trigger.
The recorded snapshot scenarios are landed: `plan-mode` (the pinned-header arc — plan-shaped initial header, a real `cat` inside plan, scripted elicitation approve, the pure-removal header-delta, a real edit) and `plan-mode-reject` (keep-planning feedback carried verbatim in the corrective `isError`), beside the keyless `modes-advertise` wire golden.
@@ -134,7 +134,7 @@ Behavioral clarifications of the chosen design; rejected designs live in [Altern
**Do subagents inherit the parent's mode?** A fork child inherits for free — the parent's `mode/set` is inside the seeded prefix. A spawn child starts in the default mode unless its creator seeds `AgentOptions.mode`; automatic forwarding by subagent providers is deferred ([Deferred](#deferred)).
-**How does plan mode relate to the sandbox's read-only mode?** They are separate axes that never touch: the mode is the collaboration stance (a `mode/set` fold), the sandbox mode is an enforcement knob (a `bash/sandbox-mode` fold, [the sandbox RFC](2026-07-06-sandbox.md)) — plan mode neither reads nor caps it, exactly as Codex keeps its Plan/Default presets separate from its sandbox and approval settings. A user who wants kernel-enforced read-only while planning sets both: flip the mode picker AND the sandbox-mode option, in either order; each switch changes only its own fold, so there is no interference and no restore step to crash out of. The log attributes each axis to its own event — the stance to `mode/set`, the confinement to `bash/sandbox-mode`.
+**How does plan mode relate to the sandbox's read-only mode?** They are separate axes that never touch: the mode is the collaboration stance (a `mode/set` fold), the sandbox mode is an enforcement knob (a `bash/sandbox-mode` fold, [the sandbox Agent Note](2026-07-06-sandbox.md)) — plan mode neither reads nor caps it, exactly as Codex keeps its Plan/Default presets separate from its sandbox and approval settings. A user who wants kernel-enforced read-only while planning sets both: flip the mode picker AND the sandbox-mode option, in either order; each switch changes only its own fold, so there is no interference and no restore step to crash out of. The log attributes each axis to its own event — the stance to `mode/set`, the confinement to `bash/sandbox-mode`.
**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs and belong to ACP's `session/set_config_option`; the division this proposal pins is picker-to-modes / knobs-to-config-options, recorded in [the feature matrix](../../../../packages/ui/acp/acp-feature-support.md) now that both this stack's picker and the sandbox stack's config options are landed. A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered).
@@ -176,10 +176,10 @@ The ecosystems that leave modes to convention show the failure shapes to avoid.
What holds now, pinned by the unit, protocol, and snapshot tiers:
-- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by the matching `request/header` event (delta or fallback snapshot) on the next step with the dev invariant green throughout.
+- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by the matching complete `request/header` event on the next step with the dev invariant green throughout.
- A user-driven flip narrates exactly once at the next boundary and a net-zero flip sequence narrates nothing; a tool-driven exit narrates only through its tool result.
- In the default mode the plugin is invisible: assemblies are byte-identical with and without `dsh-mode` loaded, and every pre-existing snapshot golden is unchanged.
-- In plan mode the guidance section and the `exit_plan_mode` tool reach both the wire request and the logged header; entering plan logs the fallback header snapshot (a non-tail tool insertion), the approved exit logs the pure-removal header-delta.
+- In plan mode the guidance section and the `exit_plan_mode` tool reach both the wire request and the logged header; entering and leaving plan each log a complete header snapshot.
- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning.
- Mode definitions (section text) are changeable from `cordis.yml` with no code edit; an unknown mode name fails validation loudly at `set()` time, an unknown definition key at load.
- `exit_plan_mode`'s approve path flips the mode and drops the plan surface on the next step; the keep-planning path returns the corrective `isError` carrying the user's feedback and stays in plan mode; the ACP `session/set_mode` round-trip updates `current_mode_update`, and the exit review prompts through each surface's user-interaction provider.
diff --git a/docs/rfc/implemented/feature/2026-07-07-session-prefix.md b/.agents/notes/implemented/feature/2026-07-07-session-prefix.md
similarity index 50%
rename from docs/rfc/implemented/feature/2026-07-07-session-prefix.md
rename to .agents/notes/implemented/feature/2026-07-07-session-prefix.md
index 34cdb8789d..c2f58dac0c 100644
--- a/docs/rfc/implemented/feature/2026-07-07-session-prefix.md
+++ b/.agents/notes/implemented/feature/2026-07-07-session-prefix.md
@@ -1,4 +1,4 @@
-# RFC: The session prefix — request-only messages in front of the derived history
+# Agent Note: The session prefix — request-only messages in front of the derived history
Status: implemented
@@ -6,37 +6,36 @@ Status: implemented
A plugin often owns a session-stable opener the model must always see — a skills catalog, an AGENTS.md digest, a workspace baseline. Before this seam the harness offered two homes, and both are wrong for that content. The system prompt is one rendered string: message-shaped content (a user-role `` envelope, a multi-message primer) does not fit it, and providers weight conversation messages differently from system text. Durable history (`agent.inject()`, a `context/message` at session start) makes the opener permanent: every `deriveMessages()` consumer replays it, the compaction retention walk owns it, forks bake it in stale, and a resume cannot refresh it — a catalog captured at session birth outlives the world it described.
-The obvious third option — let a plugin edit the request's `messages` on the way out — is banned by [the reconstructable-requests RFC](../architecture/2026-07-05-reconstructable-requests.md): every loop-built request is a pure function of the session log, so whatever channel carries the opener must log exactly what it sends. What was missing was a request-only message channel with a durable record.
+The obvious third option — let a plugin edit the request's `messages` on the way out — is banned by [the reconstructable-requests Agent Note](../architecture/2026-07-05-reconstructable-requests.md): every loop-built request is a pure function of the session log, so whatever channel carries the opener must log exactly what it sends. What was missing was a request-only message channel with a durable record.
## 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](../../../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 ([`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)).
Three properties carry the design:
-- **Request-only, header-logged.** `deriveMessages()` never returns the prefix; its one durable record is `EpochHeader.messagePrefix` on the instance's anchoring `request/header` snapshot — the channel the reconstructable-requests RFC already owns for the request's non-history half, so no new session event exists. The dev invariant ([dsh-invariants](../../../../packages/support/invariants/src/index.ts)) recomputes `messagePrefix + boundary derivation` against every loop-built request; an unlogged prefix cannot reach the wire.
-- **Frozen per instance.** Reuse is structural, not disciplined: the cached product cannot change mid-session, so the provider's prompt cache holds by construction and the prefix extends the cacheable region at zero marginal cost per step. A process restart or `ctx.agents.resume()` is a new instance: it recomposes, and any drift lands attributably on the `'resume'` header snapshot. This is the routing rule the seam creates: session-frozen openers ride the prefix; content that changes mid-session rides the append-only history channels (`agent.inject()` or tool/prompt-submit `additionalContexts` — [the interception-seams RFC](2026-06-30-interception-seams.md)), each a durable `context/message` paid once and prefix-cached thereafter.
-- **Composed before the pressure gate.** Composition precedes the instance's first `agent/pre-step`, and the seam hands the composed value through: `agent/pre-step` carries a `sessionPrefix` parameter and `CompactService.compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)` counts it in its token-pressure estimate — a gate reading the previous instance's folded prefix instead would under-gate a resumed or forked instance whose contributor grew, skipping compaction and shipping an over-window first request. A composition interrupted by a cancel/dispose landing inside the waterfall is discarded, never cached: an abort-aware listener's degraded fallback cannot leak into later requests, and the next turn recomposes under a live signal.
+- **Request-only, header-logged.** `deriveMessages()` never returns the prefix; its one durable record is `EpochHeader.messagePrefix` on the instance's anchoring `request/header` snapshot — the channel the reconstructable-requests Agent Note already owns for the request's non-history half, so no new session event exists. The dev invariant ([dsh-invariants](../../../../packages/support/invariants/src/index.ts)) recomputes `messagePrefix + boundary derivation` against every loop-built request; an unlogged prefix cannot reach the wire.
+- **Frozen per instance.** Reuse is structural, not disciplined: the cached product cannot change mid-session, so the provider's prompt cache holds by construction and the prefix extends the cacheable region at zero marginal cost per step. A process restart or `ctx.agents.resume()` is a new instance: it recomposes, and any drift lands attributably on the `'resume'` header snapshot. This is the routing rule the seam creates: session-frozen openers ride the prefix; content that changes mid-session rides the append-only history channels (`agent.inject()` or tool/prompt-submit `additionalContexts` — [the interception-seams Agent Note](2026-06-30-interception-seams.md)), each a durable `context/message` paid once and prefix-cached thereafter.
+- **Exact in the durable request envelope.** Composition precedes the instance's first `agent/pre-step` and request boundary. The first routed request logs the current prefix on its header, so post-step token pressure reads the exact prefix together with the actual prompt, tools, and routed model; no compaction-only parameter is carried through the generic pre-step seam. A composition interrupted by cancel/dispose is discarded, never cached: an abort-aware listener's degraded fallback cannot leak into later requests, and the next turn recomposes under a live signal.
Because composition runs before the boundary snapshot, a composing listener's session append joins the CURRENT request's derived history. Compaction structurally cannot touch the prefix (or the system prompt): it rewrites surface nodes, and header state never enters the surface.
## Testing
-[Interception tests](../../../../packages/core/agent-loop/tests/interception.spec.ts) pin compose-once reuse with no header deltas, prepend order, empty-prefix omission, immutability, and composition before pre-step; [cancellation tests](../../../../packages/core/agent-loop/tests/cancel.spec.ts) pin discard and recomposition. Session codec, invariant, and compaction tests cover header round trips, request reconstruction, and prefix-aware pressure accounting. Snapshot normalization preserves prefix counts, while the [pinned-header scenario](../testing/2026-07-06-pin-request-header-content-in-one-scenario.md) owns content and the default example remains prefix-free. No prefix-specific e2e is needed because the seam is deterministic and provider-independent; the with-key [request-cache e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) covers its cache economics.
+[Interception tests](../../../../packages/core/agent-loop/tests/interception.spec.ts) pin compose-once reuse without changed headers, prepend order, empty-prefix omission, immutability, composition before pre-step, and the prefix on the routed header; [cancellation tests](../../../../packages/core/agent-loop/tests/cancel.spec.ts) pin discard and recomposition. Session, invariant, token-meter, and compaction tests cover header round trips, request reconstruction, and durable prefix-aware pressure accounting. Snapshot normalization preserves prefix counts, while the [pinned-header scenario](../testing/2026-07-06-pin-request-header-content-in-one-scenario.md) owns content and the default example remains prefix-free. The provider-independent seam needs no dedicated e2e; the with-key [request-cache e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) covers its cache economics.
## Alternatives considered
-- **Per-request `before`/`after` slots recomputed every step** (the shape first proposed: a waterfall firing on every request, contributing frozen `before` messages ahead of the history and fresh `after` messages behind it) — rejected. A per-step `before` recompose invites silent drift — nothing anchors it to the log short of logging a header delta per step — and an `after` slot sits behind the growing history, so its tokens re-pay on every request and everything after it is uncacheable. Measured against the alternatives, every current update pattern is served cheaper by a durable append (paid once, cache-read thereafter), and the only content with no home was the session-stable opener — which wants freezing, not recomputation.
-- **A system-prompt section** (`system-prompt/assemble`) — rejected for this content: the assembly renders to the single `system` string, so message-shaped openers do not fit, and the system prompt is deliberately re-assembled per step (with header deltas when it changes) while the opener wants instance-frozen semantics.
+- **Per-request `before`/`after` slots recomputed every step** (the shape first proposed: a waterfall firing on every request, contributing frozen `before` messages ahead of the history and fresh `after` messages behind it) — rejected. A per-step `before` recompose invites drift that must be logged as a full changed header, and an `after` slot sits behind the growing history, so its tokens re-pay on every request and everything after it is uncacheable. Measured against the alternatives, every current update pattern is served cheaper by a durable append (paid once, cache-read thereafter), and the only content with no home was the session-stable opener — which wants freezing, not recomputation.
+- **A system-prompt section** (`system-prompt/assemble`) — rejected for this content: the assembly renders to the single `system` string, so message-shaped openers do not fit, and the system prompt is deliberately re-assembled per step (with a full changed header when it changes) while the opener wants instance-frozen semantics.
- **A durable history opener** (`inject()` at session start) — rejected: permanent history is the failure mode in the problem statement — replayed everywhere, compactable, stale across resumes.
-- **Compose per turn instead of per instance** — rejected: a turn-boundary recompose either desyncs silently from the log or forces a header delta per change, and it busts the provider cache exactly as often as it fires; the legitimate refresh point is the instance boundary, where the `'resume'` snapshot already records drift attributably.
-- **Compose lazily at the first request and let compaction read the folded header** (the shape as first merged) — superseded in review: the fold matches the live prefix only from the instance's second request on, so on a resumed/forked instance's first step the pressure gate read the PREVIOUS instance's prefix and could under-gate. Composing before the first pre-step and handing the live value through the seam makes the estimate exact at every step.
+- **Compose per turn instead of per instance** — rejected: a turn-boundary recompose either desyncs silently from the log or forces a changed header, and it busts the provider cache exactly as often as it fires; the legitimate refresh point is the instance boundary, where the `'resume'` snapshot already records drift attributably.
+- **Carry prompt/prefix through `agent/pre-step` for provisional pressure** — rejected because it couples a generic lifecycle seam to one consumer and still misses later request routing and tools; post-step replay reads every request-envelope field from its durable routed header.
- **A dedicated session event carrying the prefix** — rejected: the header events are the request's non-history record by design; a second event would be a second home for the same fact and another codec to keep total.
## Consequences
-- `agent/pre-step` and `CompactService.compactIfNeeded` carry a `sessionPrefix` parameter: every pre-step listener and compaction backend sees the real per-instance value (all in-repo implementations updated in the same change, per the pre-release stance).
+- `agent/pre-step` stays a generic `(agent, turn, step, signal)` checkpoint. Compaction receives no prefix parameter; `ctx.tokenMeter` folds the prefix from the canonical routed header at post-step.
- A contributor whose content changes mid-session is not re-read until the next instance — by design. A deployment needing mid-session catalog updates routes the change notice through the append-only history channels and pays one durable `context/message`.
- The dropped `after` slot leaves no request-only channel near the request tail; nothing in the repo needs one, and adding it back would re-open the every-step re-pay cost the design exists to avoid.
-- The `request/header-delta` `messagePrefix` arm (whole-array replacement, empty array encoding transition to absence) exists for codec totality; the loop never exercises it, because the cached prefix cannot change within an instance.
- An empty composition is canonical absence: no-contributor deployments log no extra header bytes and their requests are the bare derivation.
diff --git a/docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
similarity index 99%
rename from docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md
rename to .agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
index 830d93577b..83ea99dc75 100644
--- a/docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md
+++ b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md
@@ -1,4 +1,4 @@
-# RFC: Background subagent tasks
+# Agent Note: Background subagent tasks
Status: implemented
diff --git a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md b/.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md
similarity index 91%
rename from docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md
rename to .agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md
index 4996fc2935..08f5bb01cf 100644
--- a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md
+++ b/.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md
@@ -1,4 +1,4 @@
-# RFC: Repeat-tool-call guard plugin
+# Agent Note: Repeat-tool-call guard plugin
Status: implemented
@@ -6,17 +6,16 @@ Status: implemented
A model stuck in a loop re-issues the same tool call with byte-identical arguments — re-running a failing grep, re-reading an unchanged file, polling a command that already gave its answer — and each round trip burns tokens, wall-clock, and (for paid APIs) money without adding information. The harness has nothing that notices: the loop has no step budget, no plugin tracks call repetition, and the model only escapes when it happens to vary its own behavior. The failure mode is real and cheap to detect — [pi-repeat-tool-guard](https://github.com/Kingwl/pi-repeat-tool-guard) ships exactly this as a pi coding-agent extension: count consecutive identical calls and, past a threshold, append a `` telling the model to stop repeating itself and change course.
-The harness already has every seam the pi extension uses, and better ones: [the interception-seams RFC](2026-06-30-interception-seams.md) gives `tools/post-execute` a sanctioned way to attach model-facing context to a finished call, the loop buffers and injects that context with call/result adjacency preserved, and injected context is a logged `context/message` — so a native guard satisfies the model-visible ⟺ logged rule with no new session event. What was missing was only the plugin itself.
+The harness already has every seam the pi extension uses, and better ones: [the interception-seams Agent Note](2026-06-30-interception-seams.md) gives `tools/post-execute` a sanctioned way to attach model-facing context to a finished call, the loop buffers and injects that context with call/result adjacency preserved, and injected context is a logged `context/message` — so a native guard satisfies the model-visible ⟺ logged rule with no new session event. What was missing was only the plugin itself.
## Decision
The guard is a loop-hygiene plugin, not a model-facing tool. It counts consecutive calls to the same tool with identical canonical arguments and injects advisory reminders at configured thresholds. It never delays, blocks, or rewrites a call; the model decides whether to retry differently or finish.
-The plugin is `@deepseek-ai/dsh-repeat-tool-guard` at `packages/guard/repeat-tool-guard/`, opening the `guard/` group for loop-hygiene plugins (single-package groups have precedent: [the todo-write RFC](2026-06-29-todo-write-tool.md) shipped `todo/tool-todo`). It registers three listeners and holds all state in plugin-local maps keyed by `AgentId` — the tool registry is a context-level singleton whose waterfalls interleave every agent's calls (subagents run on the same context), so per-agent keying is correctness, not polish.
+The plugin is `@deepseek-ai/dsh-repeat-tool-guard` at `packages/guard/repeat-tool-guard/`, opening the `guard/` group for loop-hygiene plugins (single-package groups have precedent: [the todo-write Agent Note](2026-06-29-todo-write-tool.md) shipped `todo/tool-todo`). It registers two listeners and holds state in a `WeakMap` keyed by the live `Agent` object — the tool registry is a context-level singleton whose waterfalls interleave every agent's calls (subagents run on the same context), so per-agent keying is correctness, not polish; weak object keys also make a disposal-only cleanup listener unnecessary.
- **`tools/post-execute` (waterfall)** — the one detection point. The listener receives `(exec, result)` together, so counting and reminder delivery need no cross-event pending map (the pi extension needs one only because its `tool_call`/`tool_result` hooks are separate events). It always delegates via `next()` and, when a threshold is hit, prepends a reminder to the downstream decision's `additionalContexts` — the observe-and-enrich posture [the hooks bridges](2026-06-30-hook-bridges.md) already use, honoring the waterfall contract. Counting happens here rather than in `tools/pre-execute` because post-execute also runs for denied calls (`ToolRegistry.execute` routes a deny through the same pipeline), and a model hammering a denied call is exactly the loop worth breaking.
- **`agent/prompt-submit` (waterfall)** — pure reset hook: delegate via `next()`, clear the submitting agent's chain. A user interjection changes the context; repetition across it is not a loop.
-- **`agent/status` (emit)** — on `disposed`, drop the agent's state, bounding the maps over harness lifetime.
### Detection semantics
@@ -25,7 +24,7 @@ The chain key is `(tool name, canonical arguments)`; a call identical to the pre
Two deliberate rules, both documented in [the package README](../../../../packages/guard/repeat-tool-guard/README.md) because they are behavior a reader would otherwise guess at:
- **Untracked calls are transparent to the chain.** A call excluded by `include`/`exclude` neither increments nor resets the counter, so `grep X → todo_write → grep X` still counts as two consecutive `grep X` when `todo_write` is excluded. This is what makes exclusion useful — bookkeeping tools interleaved into a loop must not launder it — and it is the pi extension's (undocumented) semantics, kept on purpose and written down.
-- **Calls without an agent are ignored.** A direct `ctx.tools.execute()` caller (tests, non-loop consumers) has no model to remind and no `AgentId` to key on.
+- **Calls without an agent are ignored.** A direct `ctx.tools.execute()` caller (tests, non-loop consumers) has no model to remind and no live agent object to key on.
### Reminder delivery
diff --git a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
similarity index 78%
rename from docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
rename to .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
index 7ea5f4390e..144bdd018f 100644
--- a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
+++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
@@ -1,4 +1,4 @@
-# RFC: The self-referential cordis toolset
+# Agent Note: The self-referential cordis toolset
Status: implemented
@@ -18,11 +18,11 @@ The vm isolates accidental global pollution, and the context façade hides frame
| Tool | Contract |
|---|---|
-| `cordis_inspect` | Read-only report over the live runtime, one Markdown section per `what` value (omit `what` for all sections). Never mutates. |
+| `cordis_inspect` | Read-only report over the live runtime, one Markdown section per `what` value (omit `what` for all sections). An exact `name` with `what: "api"` or `what: "events"` narrows to one source-documented target. Never mutates. |
| `cordis_mount` | Evaluates `code` (the body of an async JavaScript function) in a `node:vm` sandbox; the code must `return` a cordis plugin, which is mounted as a child of the `cordis-dynamic` group fiber and tracked under a fresh id (`dyn-1`, `dyn-2`, …). |
| `cordis_unmount` | Disposes one dynamic mount by id and returns only after disposal reaches quiescence — every registration the plugin made is unwound, not merely requested to stop. |
-`cordis_inspect` sections: `services` (every provided ctx service and the owning fiber, non-active owners flagged), `plugins` (a flat list of every loaded plugin with its lifecycle state, from `ctx.registry` — what capabilities are loaded, deliberately not the tree shape), `tools` (what the model can call), `dynamic` (the mount table: id, name, state, provided services, awaited services), `api` (live service signatures + the type shapes they reference, from the generated catalog), and `events` (harness events with dispatch mode and signature). The model-facing tool descriptions carry the operational rules the model needs at call time; [the generated tool catalog](../../../tool-catalog.md) is their exhaustive rendering.
+`cordis_inspect` sections: `services` (every provided ctx service and the owning fiber, non-active owners flagged), `plugins` (a flat list of every loaded plugin with its lifecycle state, from `ctx.registry` — what capabilities are loaded, deliberately not the tree shape), `tools` (what the model can call), `dynamic` (the mount table: id, name, state, provided services, awaited services), `api` (live service signatures + the type shapes they reference, from the generated catalog), and `events` (harness events with dispatch mode and signature). Broad `api` and `events` reports omit full JSDoc to stay compact; an exact `name` returns one service or event with its original method/declaration JSDoc. A name is invalid with other sections, unknown targets fail, and an API target must be live. The model-facing tool descriptions carry the operational rules the model needs at call time; [the generated tool catalog](../../../../docs/tool-catalog.md) is their exhaustive rendering.
### Sandbox semantics
@@ -44,15 +44,15 @@ Mounts relate to each other through ordinary cordis service semantics, with thei
### The generated API catalog
-`cordis_inspect` serves API and event data from a generated catalog rather than a duplicated table. The generator reuses the Cordis catalog AST scan and emits service summaries, signatures, event modes, referenced type declarations, and the inherited context surface. Ambiguous type names are omitted and oversized declarations are marked as truncated.
+`cordis_inspect` serves API and event data from a generated catalog rather than a duplicated table. The generator reuses the Cordis catalog AST scan and emits service summaries, signatures, original service-method and event JSDoc, event modes, referenced type declarations, and the inherited context surface. Ambiguous type names are omitted and oversized declarations are marked as truncated.
-Freshness is gated like every generated artifact: `pnpm run verify-cordis-api` (in `doc-sync`) regenerates in memory and fails on any diff, so a JSDoc edit that changes a public signature cannot ship without regenerating the catalog the model reads. At runtime the inspect tool intersects the catalog with the live runtime rather than dumping it: live catalogued services render summary + signatures, live services without a catalog entry (mount-provided ones) render name + owning fiber, catalogued services with no live provider are listed tersely, and the referenced type shapes follow.
+Freshness is gated like every generated artifact: `pnpm run verify-cordis-api` (in `doc-sync`) regenerates in memory and fails on any diff, so a JSDoc or public-signature edit cannot ship without regenerating the catalog the model reads. At runtime the inspect tool intersects the catalog with the live runtime rather than dumping it: broad reports render live catalogued services as summary + signatures, live services without a catalog entry (mount-provided ones) as name + owning fiber, catalogued services with no live provider tersely, and then the referenced type shapes. Exact-name reports render one live service or event with the original JSDoc immediately before each signature; keeping that detail opt-in avoids charging its token cost on exploratory listings.
### Configuration, rendering, and observability
-The plugin exposes one config field, validated by schemastery and documented in [the config catalog](../../../config-catalog.md): `vmTimeoutMs` (default 5000), the millisecond bound on the synchronous portion of mount-code evaluation. Tool names, the `cordis-dynamic` group name, and the `dyn-` id prefix are structural vocabulary and stay fixed. All three tools render as `generic` cards per [the tool cookbook](../../../cookbook/adding-a-tool.md) (`cordis_inspect` a `read`, `cordis_mount` an `execute` carrying the code as `rawInput`, `cordis_unmount` a `delete`), with no `presentResult` overrides.
+The plugin exposes one config field, validated by schemastery and documented in [the config catalog](../../../../docs/config-catalog.md): `vmTimeoutMs` (default 5000), the millisecond bound on the synchronous portion of mount-code evaluation. Tool names, the `cordis-dynamic` group name, and the `dyn-` id prefix are structural vocabulary and stay fixed. All three tools render as `generic` cards per [the tool cookbook](../../../../docs/cookbook/adding-a-tool.md) (`cordis_inspect` a `read`, `cordis_mount` an `execute` carrying the code as `rawInput`, `cordis_unmount` a `delete`), with no `presentResult` overrides.
-Model-visible ⟺ logged holds with no new session event type: a mount or unmount is visible only through its own `tool/call` / `tool/result` pair, which the loop logs, and the changed tool set a mount induces is logged by the request-header delta the loop already emits when schemas change between steps. There is deliberately no `cordis/mount` provenance event — it would duplicate what the tool-call pair records. Dynamic mounts are process-lifetime, not session state: resuming a persisted session rehydrates the conversation but does not re-mount plugins.
+Model-visible ⟺ logged holds with no new session event type: a mount or unmount is visible only through its own `tool/call` / `tool/result` pair, which the loop logs, and the changed tool set a mount induces is logged by the full changed request header the loop emits when schemas change between steps. There is deliberately no `cordis/mount` provenance event — it would duplicate what the tool-call pair records. Dynamic mounts are process-lifetime, not session state: resuming a persisted session rehydrates the conversation but does not re-mount plugins.
## Alternatives considered
@@ -71,10 +71,10 @@ The correctness investment therefore goes where it pays for every capability at
**A hand-maintained service/event reference in the tool.** The first cut of the inspect tool carried a hand-written table of service method signatures. It was replaced by the generated `api-catalog.ts` because a hand table drifts from the JSDoc the moment a signature changes and nothing gates the drift, whereas the generated artifact is freshness-checked against the same AST the docs use.
-**A new `cordis/mount` session event.** A durable provenance event recording each mount (source, name) has clear precedent (`hook/invoked`, `compact/start`). It was declined for v1: mount and unmount are already visible as `tool/call` / `tool/result` pairs and the tool-set change is already logged as a request-header delta, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs mount provenance separable from the tool call.
+**A new `cordis/mount` session event.** A durable provenance event recording each mount (source, name) has clear precedent (`hook/invoked`, `compact/start`). It was declined for v1: mount and unmount are already visible as `tool/call` / `tool/result` pairs and the tool-set change is already logged as a full changed request header, so a dedicated event would only duplicate the record. It remains addable if an audit use case needs mount provenance separable from the tool call.
**A hardened / capability-restricted sandbox.** Trapping Node built-ins and handing mount code a whitelist façade rather than the raw context might suggest an intent to sandbox for safety. It is explicitly not that: the traps and the façade narrow the *surface* mount code sees — steering it onto cordis services and away from leak-prone Node built-ins and framework internals — for correctness and to close the unguarded-context escape, but the capabilities the façade exposes (`ctx.bash`, `ctx.fs`, `ctx.web`) reach the real runtime, so it is not a security boundary. A real one (separate process, permission prompts) was out of scope for a dev/opt-in toolset and would fight the entire point — handing the model the live runtime.
## Consequences
-The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` vetoes the chain, so a mounted listener can lobotomize the agent's own tool dispatch ([waterfall semantics](../../../cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume.
+The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` vetoes the chain, so a mounted listener can lobotomize the agent's own tool dispatch ([waterfall semantics](../../../../docs/cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume.
diff --git a/docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md b/.agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
similarity index 74%
rename from docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
rename to .agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
index bc8a452c91..64fa232831 100644
--- a/docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
+++ b/.agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md
@@ -1,4 +1,4 @@
-# RFC: Bash-backed grep and glob discovery tools
+# Agent Note: Bash-backed grep and glob discovery tools
Status: implemented
@@ -10,13 +10,13 @@ Search output also has two distinct budgets. The tool needs enough raw `rg` outp
## Decision
-`glob` and `grep` are model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, backed by the bash seam, not by new `ctx.fs` provider methods. The package registers model-facing filesystem discovery tools, but execution uses `ctx.bash.resolve(request)` followed by `ctx.bash.run(spec)` with fixed `rg` command templates assembled by the tool. The tool layer owns schemas, argument validation, shell quoting, result parsing, result formatting, retention, formatted-result spill handoff, and timeout declaration. The bash executor owns request defaulting/capping, subprocess execution, process-group termination, environment scrubbing, raw output capture, and backend substitution across local, sandboxed, or remote bash implementations.
+`glob` and `grep` are conditional model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, backed by the bash seam, not by new `ctx.fs` provider methods. At plugin load, the package checks `command -v rg >/dev/null 2>&1` through `ctx.bash.resolve(request)` followed by `ctx.bash.run(spec)`; if the command exits nonzero, the package logs a warning and registers neither tools nor prompt sections. A probe that cannot start, times out, aborts, is killed, or produces no exit code fails plugin load loudly because that is a broken bash executor rather than an absent optional binary. When registered, execution uses the same `ctx.bash.resolve(request)` followed by `ctx.bash.run(spec)` flow with fixed `rg` command templates assembled by the tool. The tool layer owns schemas, argument validation, shell quoting, result parsing, result formatting, retention, formatted-result spill handoff, and timeout declaration. The bash executor owns request defaulting/capping, subprocess execution, process-group termination, environment scrubbing, raw output capture, and backend substitution across local, sandboxed, or remote bash implementations.
The tools do not use `ctx.bash.start()` and do not create model-visible background tasks. They run as ordinary foreground tools from the agent loop's perspective: the tool call returns only after the `rg` command exits, times out, is aborted, or fails. `defineTool({ timeoutMs })` declares the cooperative tool-call budget, `@deepseek-ai/dsh-timeout-policy` enforces it through `exec.signal`, and the tool forwards that signal into the bash request before `resolve()` / `run()`. The bash backend's own timeout remains a second safety cap; whichever aborts first wins.
The tools align `path` with Claude Code's search tools while binding resolution to the bash workdir, not to `ctx.fs`. The tool derives the bash request workdir from `exec.agent?.session.header.cwd`, mirroring `dsh-tool-bash` and `dsh-tool-fs`; when no session cwd exists, it omits `request.workdir` so the bash implementation applies its configured cwd or process cwd through `resolve()`. For `grep`, `path` is an optional ripgrep target and may be a file or directory; omitted means the resolved bash workdir. For `glob`, `path` is an optional directory search root; omitted means the resolved bash workdir. Relative `path` values resolve against that workdir. Returned paths are displayed relative to the resolved bash workdir when possible and are intended to be follow-up-readable only in co-located deployments where the bash workdir and filesystem `read` root are the same workspace. v1 documents that deployment requirement but does not perform runtime cross-service validation. Remote or virtual filesystem search is deferred until there is a shared workspace/root contract or a provider-specific search backend.
-The package does not inject `fs`. It injects `tools`, `systemPrompt`, and `bash`; it deliberately reads `spillStore` with `ctx.get('spillStore')` instead of static inject because formatted-result spill is optional. Existing `@deepseek-ai/dsh-tool-fs` deployments that only want `read` / `write` / `edit` do not need to load bash.
+The package does not inject `fs`. It injects `tools`, `systemPrompt`, and `bash`; it deliberately reads `spillStore` with `ctx.get('spillStore')` instead of static inject because formatted-result spill is optional. Existing `@deepseek-ai/dsh-tool-fs` deployments that only want `read` / `write` / `edit` do not need to load bash. Deployments that load search need `rg` available in the bash executor environment for the tools to enter the model-visible schema.
### Package shape
@@ -63,9 +63,9 @@ Routine budgets stay out of the model-facing schema. `@deepseek-ai/dsh-tool-fs-s
| `rawOutputMaxBytes` | `20000000` | Max complete raw `rg` stdout the tool will parse; matches Claude Code's ripgrep raw buffer. |
| `timeoutMs` | `30000` | Tool-call timeout attached to both tool definitions and enforced by `@deepseek-ai/dsh-timeout-policy`. |
-`globMaxResults` and `grepMaxMatches` use `ItemRetainer({ kind: 'head' })`. `grepMaxLineBytes` uses `TextRetainer({ kind: 'head', maxBytes: grepMaxLineBytes })` for each matched line so preview cuts preserve UTF-8 boundaries. This follows the [tool result retention library](../../implemented/architecture/2026-07-06-tool-result-retention-library.md) mapping for discovery items: collect the complete result, retain head items inline, and keep path mapping, grouping, and per-line preview outside the retainer. `grep` does not expose `case_insensitive`, `head_limit`, `offset`, `count`, multiline, context lines, output modes, or file type filters in v1. A model that needs surrounding context reads the matched file with `read`; a model that needs later results follows the returned spill locator's retrieval hint.
+`globMaxResults` and `grepMaxMatches` use `ItemRetainer({ kind: 'head' })`. `grepMaxLineBytes` uses `TextRetainer({ kind: 'head', maxBytes: grepMaxLineBytes })` for each matched line so preview cuts preserve UTF-8 boundaries. This follows the [tool result retention library](../architecture/2026-07-06-tool-result-retention-library.md) mapping for discovery items: collect the complete result, retain head items inline, and keep path mapping, grouping, and per-line preview outside the retainer. `grep` does not expose `case_insensitive`, `head_limit`, `offset`, `count`, multiline, context lines, output modes, or file type filters in v1. A model that needs surrounding context reads the matched file with `read`; a model that needs later results follows the returned spill locator's retrieval hint.
-The Claude Code values are reference points for the two-layer budget, not model-facing schema precedent. Its dedicated search tools buffer raw ripgrep output up to 20 MB for internal processing, use a 20-second ripgrep timeout on non-WSL platforms (60 seconds on WSL), then apply search-specific caps before the model sees a result: `GrepTool` defaults to `head_limit = 250` and persists formatted results above 20,000 characters, while `GlobTool` defaults to 100 paths and persists formatted results above 100,000 characters. This RFC mirrors the raw-buffer and inline-count defaults, chooses a 30-second default search timeout, and uses this harness's `ctx.spillStore.saveText()` path for formatted-result recovery.
+The Claude Code values are reference points for the two-layer budget, not model-facing schema precedent. Its dedicated search tools buffer raw ripgrep output up to 20 MB for internal processing, use a 20-second ripgrep timeout on non-WSL platforms (60 seconds on WSL), then apply search-specific caps before the model sees a result: `GrepTool` defaults to `head_limit = 250` and persists formatted results above 20,000 characters, while `GlobTool` defaults to 100 paths and persists formatted results above 100,000 characters. This Agent Note mirrors the raw-buffer and inline-count defaults, chooses a 30-second default search timeout, and uses this harness's `ctx.spillStore.saveText()` path for formatted-result recovery.
The `path` field follows the same split as Claude Code: `grep.path` is a file-or-directory ripgrep target, while `glob.path` is a directory search root. v1 does not expose a separate cwd/workdir argument on these tools.
@@ -79,9 +79,9 @@ The `path` field follows the same split as Claude Code: `grep.path` is a file-or
Raw `rg` stdout is an internal transport detail. The tool requests `stdoutMaxBytes: rawOutputMaxBytes` through `ctx.bash.resolve()` and parses `stdout.text` only when the executor returns untruncated stdout within that cap. If stdout is larger than `rawOutputMaxBytes`, or the executor still returns `stdout.truncated`, the tool fails with a clear search error telling the model to narrow `pattern`, `path`, or `include`. The tool never exposes raw `rg` output or bash raw spill paths to the model.
-Only stdout is a parse source. Stderr is diagnostic text for invalid patterns, missing `rg`, and search failures; if bash truncates stderr, the tool uses the retained stderr tail with a truncation note and does not read `stderr.spillPath`.
+Only stdout is a parse source. Stderr is diagnostic text for invalid patterns, runtime `rg` disappearance after registration, and search failures; if bash truncates stderr, the tool uses the retained stderr tail with a truncation note and does not read `stderr.spillPath`.
-If `ctx.bash.run()` reports `aborted` because the tool timeout or caller cancellation fired, the tool returns a structured failure rather than pretending there were no matches. If bash reports its own timeout first, the tool likewise fails with a clear timeout message. Nonzero ripgrep exit semantics are tool-owned: exit 0 is success with matches, exit 1 is success with no matches, invalid pattern / missing `rg` / inaccessible search workdir are failures.
+If `ctx.bash.run()` reports `aborted` because the tool timeout or caller cancellation fired, the tool returns a structured failure rather than pretending there were no matches. If bash reports its own timeout first, the tool likewise fails with a clear timeout message. Nonzero ripgrep exit semantics are tool-owned: exit 0 is success with matches, exit 1 is success with no matches, invalid pattern / runtime `rg` disappearance / inaccessible search workdir are failures.
Search failures use a package-owned `HarnessError` subclass with `SEARCH_*` codes, not `FsErrorCode`, because these tools are not `ctx.fs` provider operations. The v1 vocabulary is `SEARCH_INVALID_PATTERN`, `SEARCH_FAILED`, `SEARCH_RAW_OUTPUT_OVERFLOW`, and `SEARCH_ABORTED`. Model argument validation failures such as missing required fields, blank strings, or unsupported negated/list `include` values remain ordinary tool argument errors.
@@ -116,19 +116,19 @@ Line 12: ...
(Full grep result stored at: /.../session-abc123/9f8e7d-grep-results.txt. Use read with offset/limit, or grep this path to search within it.)
```
-If the complete logical result fits under the inline cap, no formatted spill artifact is created. If the complete logical result is too large but formatted spill is unavailable, the footer says that the result was capped and the complete result could not be saved. The `truncated` / omitted count is a budget fact, not an incomplete-search fact; timeout, invalid regex, missing `rg`, inaccessible workdirs, raw-output overflow, binary skips, and parse failures stay in tool-domain error or incomplete fields.
+If the complete logical result fits under the inline cap, no formatted spill artifact is created. If the complete logical result is too large but formatted spill is unavailable, the footer says that the result was capped and the complete result could not be saved. The `truncated` / omitted count is a budget fact, not an incomplete-search fact; timeout, invalid regex, runtime `rg` disappearance, inaccessible workdirs, raw-output overflow, binary skips, and parse failures stay in tool-domain error or incomplete fields.
## Alternatives considered
**Put `glob` / `grep` on `ctx.fs`.** Rejected for v1: it forces every filesystem backend to grow a search API and makes local ripgrep behavior part of the provider seam. Search is useful product behavior, but it is not a universal text-storage primitive like `readText` or `writeText`.
-**Directly spawn ripgrep from `dsh-fs-local`.** Rejected for this RFC's v1: direct spawn gives the cleanest argv boundary, stdout/stderr control, and early-stop control, but it duplicates process execution concerns that the bash seam already owns: environment scrubbing, process-group kill, timeout propagation, sandbox/remote executor substitution, and bounded output capture. It remains a reasonable optimization if bash-backed search proves too shell-string-sensitive or if foreground streaming becomes necessary.
+**Directly spawn ripgrep from `dsh-fs-local`.** Rejected for this Agent Note's v1: direct spawn gives the cleanest argv boundary, stdout/stderr control, and early-stop control, but it duplicates process execution concerns that the bash seam already owns: environment scrubbing, process-group kill, timeout propagation, sandbox/remote executor substitution, and bounded output capture. It remains a reasonable optimization if bash-backed search proves too shell-string-sensitive or if foreground streaming becomes necessary.
**Use `ctx.bash.start()` for streaming early stop.** Rejected: `start()` creates model-visible background task semantics: task ids, owner tokens, `bash_output`, `bash_kill`, completion notifications, and no built-in timeout. `grep` needs a foreground tool result, not a background bash workflow. If streaming search becomes necessary, the right abstraction is a foreground streaming process handle on the bash/process seam, not borrowing the public background-task API.
**Expose bash raw spill paths to the model.** Rejected: a bash raw spill path contains raw `rg` stdout (`rg --json` records for grep), not the stable formatted search result. Search parses raw stdout only as an internal transport; model recovery uses a formatted result saved through `ctx.spillStore.saveText()`.
-**Add `spillStore.saveFile()` for bash output normalization first.** Rejected for this RFC's v1: `saveFile()` would help a future bash normalization pass move existing executor spill files into session-scoped spill storage, but search only needs bounded in-memory raw `rg` stdout before producing the model-facing artifact. `saveText()` is sufficient for the formatted search result.
+**Add `spillStore.saveFile()` for bash output normalization first.** Rejected for this Agent Note's v1: `saveFile()` would help a future bash normalization pass move existing executor spill files into session-scoped spill storage, but search only needs bounded in-memory raw `rg` stdout before producing the model-facing artifact. `saveText()` is sufficient for the formatted search result.
**Rely on the generic `dsh-spill-policy`.** Rejected: generic post-execute spill sees only the final tool result. If `grep` / `glob` return the first page inline, the generic policy cannot recover omitted results. The search tools must save the complete formatted result themselves before returning the bounded model-facing text.
@@ -138,22 +138,24 @@ If the complete logical result fits under the inline cap, no formatted spill art
**Expand the bash seam with a raw-output reader first.** Rejected: a portable `readRawOutput(ref, maxBytes)` API would add reference lifetime, permission, and backend storage semantics. A per-run `stdoutMaxBytes` request is the narrower seam: search either receives complete stdout within `rawOutputMaxBytes` or fails clearly.
+**Always register and report missing `rg` only at execution time.** Rejected: a model-visible tool schema is a promise that the deployment can attempt that capability. If the bash executor cannot find ripgrep at load, the safer surface is no `glob` / `grep` tools or prompt guidance. Execution-time missing-`rg` classification remains as a defensive fallback for environments that change after registration.
+
## Testing
-- Tests prove an aborted `exec.signal` reaches the bash backend (same-reference spec assertion plus the `SEARCH_ABORTED` result), and cover command construction/quoting (malicious patterns, paths with spaces, leading-dash values, quotes, newlines, glob metacharacters — unit assertions plus a real `bash -c` round-trip for every hostile value), `grep.path` as file and directory targets, `glob.path` as a directory search root, invalid pattern handling, no matches, malformed `rg --json` output, matched-line preview truncation, raw-output overflow, timeout/abort, formatted spill success/failure, the package-owned `SEARCH_*` error codes, and the no-background-task invariant.
+- Tests cover registration-time `rg` probing (probe success registers both tools and prompt sections, nonzero probe skips both tools and prompt sections with a warning, infrastructure probe failures reject plugin load), prove an aborted `exec.signal` reaches the bash backend (same-reference spec assertion plus the `SEARCH_ABORTED` result), and cover command construction/quoting (malicious patterns, paths with spaces, leading-dash values, quotes, newlines, glob metacharacters — unit assertions plus a real `bash -c` round-trip for every hostile value), `grep.path` as file and directory targets, `glob.path` as a directory search root, invalid pattern handling, no matches, malformed `rg --json` output, matched-line preview truncation, raw-output overflow, timeout/abort, formatted spill success/failure, the package-owned `SEARCH_*` error codes, and the no-background-task invariant.
- The first-party tool-owned spill precedent is covered directly: spill backend present, spill backend absent, `saveText()` failure, and missing spill owner.
- The package has real Loader-path coverage for the namespace plugin export shape (`name`, `inject`, `Config`, and `apply`, with no default export).
-- A real-executor integration suite (`dsh-bash-local` + a real `rg`) verifies the world: hostile patterns stay inert, per-session cwd resolution, VCS-metadata exclusion, modification-time ordering, and real ripgrep stderr classification. It self-skips where `rg` is not on PATH (a CI accommodation mirroring the keyless e2e skip); the fake-executor suite alone carries the per-file 100% coverage gate.
-- Snapshot gap note for the transcript-visible spill notice: this landed with the gap note, not a snapshot. The snapshot tier replays the acp-agent tree, and adding the search plugin there changes the assembled system prompt — every golden would need re-recording with a real key, which the implementing environment did not hold. The spill notice's exact transcript text is pinned by unit tests (`formatGlobOutput`/`formatGrepOutput` and the through-the-registry spill tests); wiring the plugin into the acp-agent tree plus a `test:snapshot:record` pass is the follow-up for the next key-holding session.
+- A real-executor integration suite (`dsh-bash-local` + a real `rg`) verifies the world: hostile patterns stay inert, per-session cwd resolution, VCS-metadata exclusion, modification-time ordering, and real ripgrep stderr classification. It self-skips where `rg` is not on the test process PATH (a CI accommodation mirroring the keyless e2e skip); the fake-executor suite carries registration and execution coverage for missing `rg`, plus the per-file 100% coverage gate.
+- Snapshot gap note for the transcript-visible spill notice: this landed with the gap note, not a snapshot. The snapshot tier replays the acp-agent tree, and adding the search plugin there changes the assembled system prompt — every expected output would need re-recording with a real key, which the implementing environment did not hold. The spill notice's exact transcript text is pinned by unit tests (`formatGlobOutput`/`formatGrepOutput` and the through-the-registry spill tests); wiring the plugin into the acp-agent tree plus a `test:snapshot:record` pass is the follow-up for the next key-holding session.
## Consequences
-- `glob` and `grep` are model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, not `ctx.fs` provider methods and not part of the existing `@deepseek-ai/dsh-tool-fs` root plugin. The package injects `tools`, `systemPrompt`, and `bash`; it does not inject `fs`, and `ctx.spillStore` stays optional via `ctx.get('spillStore')`.
+- `glob` and `grep` are conditional model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, not `ctx.fs` provider methods and not part of the existing `@deepseek-ai/dsh-tool-fs` root plugin. They register only when the bash executor can find `rg`; the package injects `tools`, `systemPrompt`, and `bash`, does not inject `fs`, and keeps `ctx.spillStore` optional via `ctx.get('spillStore')`.
- The schemas are exactly `glob(pattern, path?)` and `grep(pattern, path?, include?)`; search caps and timeout are defaulted, validated Config fields (`globMaxResults`, `grepMaxMatches`, `grepMaxLineBytes`, `rawOutputMaxBytes`, `timeoutMs`).
- The tools execute through `ctx.bash.resolve(request)` → `ctx.bash.run(spec)`, forward `exec.signal`, never call `ctx.bash.start()`, and never expose a bash task id. The bash request workdir comes from `exec.agent?.session.header.cwd` when available; the resolved `spec.workdir` drives execution and relative-path display.
- The tools request `stdoutMaxBytes: rawOutputMaxBytes` from the bash seam, parse only untruncated stdout within that cap, and treat over-cap or still-truncated raw output as a clear search failure; raw `rg` output is never exposed to the model.
- Oversized complete formatted results are saved through `ctx.spillStore.saveText()` when available while inline results stay bounded; spill failure, a missing backend, or a missing owner preserves the inline result and reports the unsaved remainder — never an `isError`.
-- The package README, the generated config catalog, and exported JSDoc document the Config fields and `SEARCH_*` codes; the coding-agent example ships the tools (the acp-agent tree waits on the snapshot re-record above); the fs group README records the co-located bash/filesystem deployment requirement.
+- The package README, the generated config catalog, and exported JSDoc document the Config fields and `SEARCH_*` codes; the tui-agent example ships the conditional tool plugin (the acp-agent tree waits on the snapshot re-record above); the fs group README records the `rg` availability and co-located bash/filesystem deployment requirements.
## Risks
diff --git a/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md b/.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md
similarity index 94%
rename from docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md
rename to .agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md
index 118009dedb..988052fb2a 100644
--- a/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md
+++ b/.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md
@@ -1,4 +1,4 @@
-# RFC: Expose agent session identity and JSONL location to tools and hooks
+# Agent Note: Expose agent session identity and JSONL location to tools and hooks
Status: implemented
@@ -10,7 +10,7 @@ The boundary must preserve two properties: the owner of a fact decides how to re
## Decision
-Extend the [`SessionPersistence`](../../implemented/architecture/2026-06-14-session-persistence.md) seam with a synchronous, side-effect-free location query:
+Extend the [`SessionPersistence`](../architecture/2026-06-14-session-persistence.md) seam with a synchronous, side-effect-free location query:
```ts
import type { SessionHeader } from '@deepseek-ai/dsh-session'
@@ -42,7 +42,7 @@ The bash seam exports `DSH_ENV_PREFIX` as the single namespace source and derive
The bash tool description teaches only the durable convention: current harness environment facts are available through managed `$DSH_*` variables and may be inspected when needed. It does not enumerate persistence-specific keys or add a permanent system-prompt section. Tool schemas are already logged in request headers and tool output is logged as `tool/result`, so no new session event is required.
-The [Claude Code and Codex hook bridges](../../implemented/feature/2026-06-30-hook-bridges.md) resolve transcript location from the same persistence seam when constructing payloads. Codex uses `transcript_path: string | null`; Claude Code preserves its string field and falls back to `''`. Hook lookup neither materializes nor flushes a session.
+The [Claude Code and Codex hook bridges](2026-06-30-hook-bridges.md) resolve transcript location from the same persistence seam when constructing payloads. Codex uses `transcript_path: string | null`; Claude Code preserves its string field and falls back to `''`. Hook lookup neither materializes nor flushes a session.
## Peer product findings
diff --git a/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
new file mode 100644
index 0000000000..4904da5bd5
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md
@@ -0,0 +1,101 @@
+# Agent Note: Parallel tool-call execution by per-call safety
+
+Status: implemented
+
+## Problem
+
+An assistant message may contain several sibling `tool-call` blocks. Running them serially adds the latency of independent reads and web requests even though the model has already requested them together.
+
+Concurrency is a host scheduling concern, not model-facing tool metadata. The loop needs to decide which calls may overlap without hardcoding tool names or exposing scheduler policy in the JSON schema.
+
+The session log remains authoritative: every started call has an audit event, every started call receives a result, and model history observes results in the original call order regardless of completion order.
+
+## Decision
+
+Each tool may provide an optional `isConcurrencySafe(args)` classifier. It is synchronous and pure: it examines only the current call's parsed arguments and performs no I/O or mutation. Only an explicit `true` opts in; a missing classifier, invalid arguments, a thrown classifier, or any other return value makes the call exclusive. The canonical type contract lives in the [tool data structures](../../../../docs/core-data-structures/tools.md).
+
+The classifier is deliberately unary. Returning `true` is the tool's promise that this call may overlap with any sibling call that also returns `true`; the scheduler does not compare calls or prove that their resource accesses are compatible.
+
+The unary classifier remains input-sensitive. A tool may classify a read-only operation as parallel and a mutating operation as exclusive. The interface cannot express relational rules such as "these writes are safe only when their paths differ," so a call whose safety depends on a sibling remains exclusive.
+
+`defineTool()` validates arguments before invoking a typed classifier. Invalid arguments classify as exclusive and produce the ordinary argument error only if the call executes. `ctx.tools.executionMode(exec)` resolves the live tool definition and returns the tagged `parallel` or `exclusive` mode; unknown tools fail closed to exclusive.
+
+A tagged mode, rather than a public boolean scheduler API, keeps resource-aware variants representable without changing the classifier contract.
+
+## Scheduling and ordering
+
+The loop waits for the complete assistant message, parses every call once, creates a distinct `ToolExecution` for each call, and scans them in model order. Consecutive parallel calls form one group; every exclusive call forms a singleton group and an ordering barrier. Groups execute sequentially. Classification is lazy: the scheduler resolves the next call after each barrier and reclassifies every later call before replenishing a parallel pool. If a registry mutation makes that call exclusive, the current pool drains before the call starts as the next barrier.
+
+For example:
+
+```text
+[parallel read(A), parallel read(B), exclusive write(A), parallel read(C)]
+
+→ [read(A), read(B)]
+→ [write(A)]
+→ [read(C)]
+```
+
+`read(A)` and `read(B)` may overlap. `write(A)` starts after both finish, and `read(C)` starts after the write finishes.
+
+Every group uses a rolling pool bounded by `maxParallelToolCalls`: the loop starts calls in model order up to the cap and starts another whenever one settles. An exclusive group is a pool of one. A cap of `1` preserves serial execution.
+
+Only dispatch and the tool body overlap. `tools/pre-execute` and `tools/post-execute` run in model order because middleware may maintain ordering-sensitive state. `tools/execute` wrappers run around concurrent dispatches and therefore must be reentrant across distinct executions.
+
+Each started call appends `tool/call` immediately before its pre-execute gate. Completed dispatches occupy model-order slots, and a commit cursor appends `tool/result` and collects `additionalContexts` only when the next slot is ready. Live surfaces may show several pending calls, but results and post-tool context remain model-ordered.
+
+An abort before a group starts records no calls from that group. An abort during a group stops replenishment, waits for already-started calls, commits their results in order, drains accepted batch context after those results, and then ends the step through the existing abort path. Calls that never start have no audit event.
+
+Code Mode remains outside this scheduler because the model emits one native `run_code` call. `run_code` and its internal dispatch queue remain serial; native sibling calls in `mode: 'both'` use the normal scheduler.
+
+## Safety contract
+
+A tool that returns `true` promises that its body is safe to run at the same time as other parallel calls. It must not directly mutate the parent session or other parent-owned state; it returns its outputs to the loop, which commits them in model order.
+
+Any shared state touched during execution must be concurrency-safe. This includes tool wrappers and providers: they may serialize internally or enforce their own capacity, but they must support concurrent dispatch without corrupting state.
+
+## Configuration and declarations
+
+`maxParallelToolCalls` is a positive AgentLoop deployment cap shared by every agent the factory creates. It defaults to `10`; `1` preserves serial execution. Exact fields and defaults live in the generated [configuration catalog](../../../../docs/config-catalog.md).
+
+The shipped declarations are conservative. Web search, web fetch, and filesystem read opt in. Filesystem writes and edits, bash tools, subagent delegation, workflow, user interaction, todo mutation, Code Mode, and Cordis mutation tools remain exclusive. A subagent may share its parent's workspace or external resources, and the unary classifier cannot prove that sibling delegations have disjoint effects. Bash has no proven input-sensitive classifier and remains exclusive.
+
+Filesystem read relies on a narrow recorder exception: its synchronous observation updates may settle out of order, but write and edit re-check the observed version before mutation, so stale state only produces `FS_STALE_VERSION`.
+
+## Verification
+
+Unit coverage pins fail-closed classification, typed argument validation, grouping, barriers, live reclassification after registry replacement, the rolling cap, distinct execution objects, middleware order, ordered results and context, and abort draining. First-party tests pin each parallel declaration.
+
+Snapshot coverage pins the visible multi-call transcript: pending calls may overlap while completed results remain model-ordered. Code Mode coverage pins its serial boundary. No provider-backed e2e is required because scheduling is deterministic loop behavior.
+
+## Alternatives considered
+
+**Keep serial execution.** This avoids new ordering and abort cases but retains unnecessary latency for independent sibling calls.
+
+**Use one tool-level boolean.** A fixed `supportsParallelToolCalls` flag is smaller but cannot distinguish a tool's read-only and mutating operations. The argument-sensitive classifier preserves that distinction.
+
+**Use stateful classification.** Giving the classifier a live agent, registry, or I/O access makes the decision depend on when it runs and creates a gap between classification and dispatch. Mutable authorization and stale-state checks remain execution-time responsibilities.
+
+**Use sibling-aware or resource-aware classification.** The scheduler could compare calls pairwise or let each call declare resource read/write claims. This can parallelize non-conflicting writes, but it requires shared resource identity and conflict semantics across unrelated tools. The unary contract instead gives up that concurrency and fails closed when safety is relational.
+
+**Parallelize the complete tool pipeline.** This keeps the loop on the public one-call API but runs pre- and post-execute middleware concurrently. Existing guards and hook bridges may carry ordered state, so only dispatch overlaps.
+
+**Expose staged methods or a scheduling waterfall.** Public `prepare` / `dispatch` / `finalize` methods or a `tools/execution-mode` event add extension surface before another consumer needs it. The loop uses an internal scheduler view, while `executionMode(exec)` leaves an insertion point for a policy seam.
+
+**Start calls while the model streams.** This may reduce latency further but changes assistant-message authority, replay, and call/result pairing. The scheduler starts only after the assistant message is complete.
+
+**Use fixed-size windows.** Waiting for every call in one window before starting the next leaves capacity idle behind a slow call. The rolling pool preserves the cap without that delay.
+
+**Expose concurrency metadata to the model.** The model can already emit sibling calls. Host scheduling metadata would enlarge requests without improving tool choice.
+
+## Consequences
+
+The design is fail-closed and simple for tool authors, but it cannot exploit concurrency whose safety depends on comparing siblings. A tool that opts in too broadly can expose latent shared-state races.
+
+Parallel calls may begin in cases where serial execution would have aborted before reaching them. The scheduler therefore records only started calls, drains them on abort, and never starts replacements after cancellation.
+
+Ordered commits may hold a fast result behind a slow earlier sibling. This preserves replay and model-history order while live surfaces still show pending progress.
+
+Concurrent external calls can compete for quota or process capacity. Providers own their capacity controls; the loop cap only limits calls from one agent step.
+
+Tool registration is a scheduling boundary. Registry mutations affect not-yet-started calls because the scheduler reclassifies after each barrier and before every pool replenishment. Already-started calls retain the scheduling decision under which they entered the pool.
diff --git a/docs/rfc/implemented/feature/2026-07-10-session-query-service.md b/.agents/notes/implemented/feature/2026-07-10-session-query-service.md
similarity index 93%
rename from docs/rfc/implemented/feature/2026-07-10-session-query-service.md
rename to .agents/notes/implemented/feature/2026-07-10-session-query-service.md
index ebc201f102..a34d699601 100644
--- a/docs/rfc/implemented/feature/2026-07-10-session-query-service.md
+++ b/.agents/notes/implemented/feature/2026-07-10-session-query-service.md
@@ -1,4 +1,4 @@
-# RFC: Exact session query service
+# Agent Note: Exact session query service
Status: implemented
@@ -18,7 +18,7 @@ An exact target read first checks the live store and snapshots the live header a
## Surface semantics
-`dsh-session` exports `foldSurface(events)`, and `SurfaceManager` uses the same transition functions for its incremental cache. The fold returns detached current nodes and each replacement's actual removed seqs. `listEvents()` and `traceEvent()` use that result to classify every raw event, so inspection cannot disagree with model-history derivation about positional replacement semantics.
+`dsh-session` exports `foldSurface(events)`, and `SurfaceManager` uses the same transition functions for its incremental cache. The fold returns detached current event sequences and each replacement's actual removed seqs. `listEvents()` and `traceEvent()` use that result to classify every raw event, so inspection cannot disagree with model-history derivation about positional replacement semantics.
`readEvent()` returns the complete target plus raw neighbors by contiguous seq. `before` and `after` default to zero and are independently bounded by `readWindowMax`, default 50. The result carries a cloned `SessionHeader`, not a source-availability record, because determining a live target's persisted flag would violate the guarantee that live exact reads do not depend on persistence health.
diff --git a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
similarity index 77%
rename from docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
rename to .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
index 3c2e17aebd..0f42a547cf 100644
--- a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
+++ b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
@@ -1,4 +1,4 @@
-# RFC: Configure subagent persona, tool visibility, and depth
+# Agent Note: Configure subagent persona, tool visibility, and depth
Status: implemented
@@ -49,9 +49,11 @@ The global registry remains live. A deny-only filter admits a later global name
The depth limit bounds recursive delegation independently of tool visibility. A top-level agent has depth zero; an in-process child has its parent's validated depth plus one. `maxDepth` is an absolute non-negative safe integer, and a start rejects before child ownership begins when the derived child depth is greater than the cap.
-Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. Omitting the cap leaves depth unbounded by this mechanism.
+The effective parent depth is the greater of durable `SessionHeader.delegationDepth` and runtime `AgentOptions.subagentDepth`. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count.
-A deployment can combine depth and filtering. For example, it may keep the delegation tool visible at depth one but set `maxDepth: 1`, or deny the delegation tool entirely in children. Neither choice changes the provider's conversation-history behavior.
+Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the [SDK helper's generated subagent entries](../../../../packages/sdk/helper/src/features/builtin/index.ts) and [JSON-RPC example](../../../../examples/jsonrpc-agent/cordis.yml) use that general policy, while the shipped interactive ACP, headless, and REPL examples pin one. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`.
+
+A deployment can combine depth and filtering, but the numeric cap does not synthesize a filter. The delegation tool stays visible at the cap because authorization may depend on runtime state; every attempted start checks the calling agent's current durable and runtime depth, and a rejected start returns an errored tool result without publishing a child. A deployment may separately deny delegation tools in children when its visibility policy is static. Neither choice changes the provider's conversation-history behavior.
### Capability gating keeps providers honest
@@ -83,10 +85,10 @@ A security design would need a separate authority representation, propagation ru
**Hide only tool schemas.** Presentation-only filtering lets the model execute a tool that the prompt says does not exist through Code Mode or a forged call. One resolver governs both presentation and execution instead.
-**Use only tool filtering to stop recursion.** Removing the delegation tool is useful but provider-specific and does not protect direct service callers or alternate delegation tools. Absolute depth is an independent structural bound.
+**Encode the depth cap as an automatic tool filter.** A creation-time filter snapshots a decision that may depend on runtime state, affects only one configured tool name, and does not protect direct service callers or alternate delegation tools. The provider instead enforces the absolute cap at every start.
## Consequences
Contributors can configure child role, visible global tools, and recursion without defining new providers. Capability checks fail before ownership starts, unpublished setup makes the first request consistent, and one tool resolver prevents presentation/execution drift.
-The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation.
+The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. A model may call a visible delegation tool after the current depth policy forbids another child and receive an error. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation.
diff --git a/docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md b/.agents/notes/implemented/feature/2026-07-13-session-query-tracing.md
similarity index 98%
rename from docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md
rename to .agents/notes/implemented/feature/2026-07-13-session-query-tracing.md
index f16f543ac5..08f856863e 100644
--- a/docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md
+++ b/.agents/notes/implemented/feature/2026-07-13-session-query-tracing.md
@@ -1,4 +1,4 @@
-# RFC: Session query relationship tracing
+# Agent Note: Session query relationship tracing
Status: implemented
diff --git a/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.i18n.yaml
new file mode 100644
index 0000000000..8e44e3a6bc
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.i18n.yaml
@@ -0,0 +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-14-cross-family-fs-sandbox.md: 9b6312e5994469606bd1645902fc798f70258580
+2026-07-14-cross-family-fs-sandbox.zh.md: d4816e03d94bdf12b2db875d71dccb7db3a2c0d7
diff --git a/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md
new file mode 100644
index 0000000000..9b6312e599
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md
@@ -0,0 +1,94 @@
+# Agent Note: Cross-family file sandbox — one policy home, a sandboxed fs provider, and fs escalation parity
+
+Status: implemented
+
+English | [中文](2026-07-14-cross-family-fs-sandbox.zh.md)
+
+## Problem
+
+`SandboxMode` claims file effects, but originally only `ctx.bash` enforced it. The fs tools (`write`/`edit`) mutate the host filesystem in-process through `ctx.fs`, where an OS argv wrapper is mechanically meaningless — [the sandbox Agent Note](2026-07-06-sandbox.md) § In-process tools records this and left cross-family enforcement as a deferred phase with an open question: whether in-process enforcement stays per-seam or becomes a uniform harness capability. This Agent Note is that phase, and answers it: one shared policy home, per-seam enforcement at each family's correct altitude.
+
+The gap was not read-only-shaped. A confined coding agent's product mode is `workspace-write`: bash may already write under the workspace root while everything outside is denied, so an fs enforcement that could only deny-all would be strictly worse than disabling the fs tools — the model would attempt an in-workspace `write`, be denied, and learn to detour through `bash` heredocs. Cross-family enforcement therefore speaks the full mode ladder, including the path-containment judgment `workspace-write` requires (canonical targets; `..`/symlink/absolute-path escapes) and the same escalation lever bash carries.
+
+A second enforcing family also exposed an ownership problem in the original layout. The deployment default (`mode` + `workspaceRoot`) was configured on `dsh-bash-sandbox`, and the per-session override event was `bash/sandbox-mode`, folded and written by `dsh-bash`'s session-mode kit. With fs enforcing the same policy, either fs reads bash's config and events (a capability family depending on a sibling's plugin config) or each family carries its own copy — and two copies of `workspaceRoot` drift into exactly the split world the sandbox RFC warns about: bash confined to one root while fs fences another.
+
+## Decision
+
+Three coordinated pieces, all composed from the leaf `cordis.yml`, none touching `agent-loop`.
+
+### `ctx.sandboxPolicy` — one home for mode and workspace root
+
+`packages/sandbox/sandbox-policy/` (`@deepseek-ai/dsh-sandbox-policy`) registers `ctx.sandboxPolicy`, the single owner of the deployment's sandbox policy:
+
+- `Config`: `mode` (the closed `SandboxMode` union, default `read-only`) and `workspaceRoot` (default the process cwd, resolved absolute). Misconfiguration fails loud at load.
+- The per-session override event `sandbox/mode`, with its pure fold (`effectiveSandboxMode(events)`), its write path (`setSandboxMode(session, mode)`), and `SANDBOX_MODES`. The event is policy state — consumed by two families — so it lives here, not in either capability's seam. Its shape and log-only semantics match the `approval/*` precedent.
+- `defaultMode` / `workspaceRoot` accessors the enforcing implementations read for their resolve fallback and boundary.
+
+`dsh-bash-sandbox` carries no sandbox config of its own — it injects `sandboxPolicy` and reads the default from it; its `resolve()` precedence is unchanged (escalation grant > per-call stamp > default). `dsh-tool-bash` and `dsh-tool-fs` fold the session's `sandbox/mode` with `effectiveSandboxMode` to stamp each call; `dsh-permission` presets and the ACP bridge write through the relocated setter. The seam that owns bash execution no longer depends on `dsh-session` at all — the session dependency moved to the policy package with the fold.
+
+### `dsh-fs-sandbox` — enforcement inside the provider
+
+`packages/fs/fs-sandbox/` (`@deepseek-ai/dsh-fs-sandbox`) mirrors the `bash-local`/`bash-sandbox` split: `SandboxedFileSystem extends LocalFileSystem`, registered as `ctx.fs`, injecting `sandboxPolicy`. Reads (`resolve`/`stat`/`readText`/`streamText`/`listDir`) pass through untouched — every mode permits reading. The two mutations enforce by mode before delegating to the inherited atomic write:
+
+- `read-only` denies `writeText`/`editText` outright.
+- `workspace-write` fences the canonicalized target against the writable-root set — `writableRoots(policy)` in `dsh-sandbox`: the workspace root plus the platform temp areas (`/tmp`, `os.tmpdir()`), each realpathed — the SAME set the Seatbelt profile grants, so the fs fence is the fourth dialect of one mode meaning alongside the bwrap/Landlock/Seatbelt profiles, and "the write tool cannot write `/tmp` but bash can" asymmetries cannot arise. Containment is prefix-inclusion on real paths; the target is re-canonicalized (`resolve` realpaths the deepest existing ancestor) immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught.
+- `danger-full-access` delegates unfenced.
+
+A denial is the structured `FS_SANDBOX_DENIED` carrying the effective mode — distinct from `FS_PERMISSION_DENIED` (a host EACCES is the world refusing; this is policy refusing). No text inference: an in-process fence knows exactly what it denied. The per-call carrier is a trailing optional `sandboxMode` on `writeText`/`editText` (the filesystem twin of `BashExecRequest.sandboxMode`); the seam stays session-free (the caller stamps, exactly as `resolve` takes a cwd), and the bare local backend carries-and-ignores it. `FileSystem.sandboxMode` is the capability fact (`undefined` on the base and `fs-local`, the default on `SandboxedFileSystem`), so the tool layer advertises escalation from composition truth.
+
+The threat model is stated in the package README: a policy fence in trusted code over model-controlled paths, not a kernel boundary — the operations are the seam's own, only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface (the `code-runtime` "containment, not a security boundary" precedent). Kernel-grade isolation of untrusted CODE stays `ctx.bash`'s job. The residual resolve-to-syscall race is narrowed by the in-place re-canonicalization and eliminated only by platform primitives (`openat2` `RESOLVE_BENEATH`) not worth their portability cost here.
+
+### Tool parity — one denial marker, one escalation flow
+
+`dsh-tool-fs` stamps the effective mode onto each mutation and maps `FS_SANDBOX_DENIED` to the marker the model already knows from bash: `[sandbox: file access denied under mode]`. When `ctx.fs.sandboxMode` reports a confining mode at registration, `write` and `edit` advertise the same `sandbox_permissions` + `justification` fields, teach the same same-turn retry, and resolve the same `ctx.approval` request before executing — the four outcomes and their verbatim fail-closed texts carried over from [the sandbox Agent Note](2026-07-06-sandbox.md) § Escalation (strict widening checked at execution against the call's effective mode; a grant consumed by the one call that asked; no new session events).
+
+The shared pieces live in `dsh-sandbox`, which owns the mode types: `WIDER_MODES`, the escalation-target enum, the argument-pairing validation, the denial/hint marker builders, and `approveEscalation` — the ordered fail-closed choreography. `approveEscalation` takes a minimal STRUCTURAL approver (`EscalationApprover`, generic over the agent and call-id types), not the approval service type, so `dsh-sandbox` gains no dependency on the approval or agent packages: each tool passes its own `ctx.approval`, agent, call id, and tool name as ingredients. `dsh-tool-bash` and `dsh-tool-fs` both use these; the cross-file duplication gate holds the single-sourcing honest.
+
+The [`examples/acp-agent`](../../../../examples/acp-agent/cordis.yml) composition loads `dsh-sandbox-policy` and `dsh-fs-sandbox`, moves the `mode`/`workspaceRoot` config to the policy entry, and drops the old gating that disabled the fs stack under confined modes; `fs-policy` (read-before-edit) composes orthogonally on top. The system prompt still states no sandbox mode — the marker teaches the boundary at the moment it matters, per the sandbox Agent Note's live evidence.
+
+### The enforcement point: provider, not intent gate
+
+The sandbox Agent Note's original cross-family sketch put fs enforcement on the `fs/write-intent`/`fs/edit-intent` events. This Agent Note enforces in the provider instead, on two mechanical facts: the intent slots are single-decision first-wins (occupied by `dsh-fs-policy`, whose contract names a second decider a misconfiguration), and the intent events are dispatched only by `dsh-tool-fs` — a direct `ctx.fs` caller (a cordis-mounted plugin, a custom tool) bypasses them, where provider-level enforcement covers every caller by construction. The sandbox Agent Note's deferred-phase wording is updated to match in the same change.
+
+### Out of scope
+
+- **Network policy for `ctx.web`** — `SandboxMode` claims file effects only; a web-only network knob while bash `curl` runs free would be a false boundary. Revisit when a bash backend enforces network (bwrap `--unshare-net`, Landlock ABI v4+).
+- **The `subagent-acp` consumer** and **per-session workspace root** — unchanged deferred phases of the sandbox RFC; centralizing the root in `ctx.sandboxPolicy` is groundwork for the latter, not its design.
+- **A uniform per-tool sandbox runtime** — remains rejected for the reasons in the sandbox RFC.
+
+## Alternatives considered
+
+- **Enforce on the `fs/*` intent events (the sandbox Agent Note's original sketch)** — rejected on the two mechanical facts in § The enforcement point: single-slot first-wins already occupied, and a bypass for direct `ctx.fs` callers. Provider-level enforcement covers every caller and mirrors bash's swap-the-implementation shape.
+- **Enforce in `tools/pre-execute`** — rejected: the listener sees the model's raw path string before `resolve()`, so it would re-implement cwd defaulting and symlink canonicalization and still race the real resolve. Disqualifying for `workspace-write`, a judgment over canonical paths.
+- **Inline checks in `dsh-tool-fs`** — rejected: covers only the tool path (same bypass as the intent events) and duplicates resolve knowledge one layer above where the canonical target already exists.
+- **A `mode` flag on `dsh-fs-local` instead of a sibling backend** — rejected: the capability fact must be composition truth the way `dsh-bash-local` vs `dsh-bash-sandbox` is; a config flag makes the tool's advertisement conditional on configuration, and the bash family already establishes the sibling-package shape.
+- **Kernel-enforced fs mutations via a confined helper subprocess** — rejected: a process per write; `editText`'s read-match-write critical section would have to move wholesale into the child to stay atomic; and the threat surface (trusted operations, untrusted path argument) does not need a kernel — the fence in trusted code is the complete answer, while untrusted-code isolation stays on `ctx.bash`.
+- **Per-family policy config with a load-time consistency check** — rejected: two homes for one fact, patched by a check that must enumerate every future enforcing family; the policy service makes drift inexpressible instead of detected.
+- **Keep the override event in `dsh-bash` as `bash/sandbox-mode`** — rejected: the event is policy state consumed by two families; leaving it bash-named forces `dsh-fs-sandbox` to depend on bash vocabulary. Pre-release, the rename is a same-change move with snapshot re-records, no shims.
+- **Escalation choreography imported from the approval/agent packages into `dsh-sandbox`** — rejected: it would invert the layering (a base vocabulary package depending on UI/agent packages). The structural approver keeps the logic single-sourced in `dsh-sandbox` while the dependencies stay in the tool layer that already holds them.
+- **A consolidated mutation-options object on the fs seam** (the shape first sketched for the per-call carrier) — rejected on friction: it churns every `writeText`/`editText` caller and splits `signal` across an options bag for mutations while reads keep it positional. A trailing optional `sandboxMode` matches bash's carry-and-ignore pattern and keeps `signal` symmetric across the seam.
+- **Extra writable-root grants on `SandboxPolicy` now** — deferred unchanged: `writableRoots()` derives from the mode meaning today; ad-hoc grants are an escalation-scope question the sandbox RFC left open.
+
+## Consequences
+
+What shipped — the tiers in § Testing hold each:
+
+- Under `read-only`, `write`/`edit` return the `[sandbox: file access denied under read-only mode]` marker and the disk is untouched; `read`/`listDir` behave identically to `dsh-fs-local`.
+- Under `workspace-write`, mutations land under the workspace root and the temp areas and are denied outside; the containment matrix — `..` traversal, absolute paths outside, a pre-existing symlinked directory inside pointing out, and a new file created under such a symlink — denies every escape on real disks.
+- A denied fs mutation retried once with `sandbox_permissions` + `justification` prompts through the composed approval chain; a grant runs exactly that call under the wider mode and the write lands; rejected/cancelled/unavailable each produce their verbatim fail-closed text and mutate nothing.
+- One `permission` preset switch governs both families: after a session switches modes, the next bash call and the next fs mutation both honor the new mode from the same `sandbox/mode` fold.
+- A direct `ctx.fs.writeText` with no per-call stamp is confined at the deployment default.
+- The escalation fields on `write`/`edit` exist exactly when the mounted `ctx.fs` confines, absent under `dsh-fs-local`.
+- `agent-loop` is untouched — everything rides `ctx.sandboxPolicy`, the `ctx.fs` seam, `SessionEventMap` merging, and the tool-execution pipeline.
+
+Costs and accepted limits:
+
+- **The fs fence is a policy boundary, not a kernel one.** Its threat surface is model-chosen paths, not adversarial host processes; the residual resolve-to-syscall TOCTOU is narrowed, not eliminated, and the README says so. Kernel boundaries remain bash's.
+- **`dsh-bash-sandbox` gains a hard dependency on `ctx.sandboxPolicy`.** Every sandboxed composition adds one `cordis.yml` entry or fails loud at load — the intended pre-release foundation move; the examples update in the same change.
+- **Fence-vs-runner parity is derived, not asserted.** The fs fence and the Seatbelt profile both take their writable set from `writableRoots`, and a parity unit test pins the sets; a runner profile changing its writable set without that function would drift.
+- **The marker and escalation teaching now serve two families.** A wording change is a coordinated edit behind one builder in `dsh-sandbox`; the duplication gate and pinned snapshots hold it single-sourced, at the cost that fs and bash cannot deliberately diverge in phrasing without splitting the builder.
+
+## Testing
+
+- Unit: `dsh-sandbox` pins the escalation ladder, the marker builders, the argument-pairing validation, and `approveEscalation`'s ordered fail-closed sequence (non-widening, no-approval, no-agent, each outcome), plus `writableRoots`/`canonicalPath`. `dsh-sandbox-policy` pins the default accessors, the fold/setter, the load-time mode rejection, and HMR safety. `dsh-fs-sandbox` pins the per-mode fence and the containment matrix (inside, temp area, absolute-outside, `..`, symlinked-out directory, new file under one, path-equals-root, root-ending-in-separator) on a real filesystem, plus the per-call override and HMR safety. `dsh-tool-fs` pins advertisement gating, the mode stamp, the fold, denial-marker mapping, and the full escalation matrix (grant, reject, no-service, no-agent, pairing, non-confining guard). `dsh-tool-bash`, `dsh-bash-sandbox`, and `dsh-permission` migrate to the relocated policy/kit.
+- Snapshot: the acp-agent example composes `dsh-sandbox-policy` + `dsh-fs-sandbox`; the pinned header carries the fs escalation fields and the `sandbox/mode` event name, re-recorded once.
diff --git a/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md
new file mode 100644
index 0000000000..d4816e03d9
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md
@@ -0,0 +1,94 @@
+# Agent Note: 跨家族文件沙箱——统一策略归属、沙箱化 fs 提供方、fs 升级对等
+
+Status: implemented
+
+[English](2026-07-14-cross-family-fs-sandbox.md) | 中文
+
+## 问题
+
+`SandboxMode` 声明的是文件效果,但最初只有 `ctx.bash` 执行它。fs 工具(`write`/`edit`)在进程内经由 `ctx.fs` 变更宿主文件系统,那里的 OS argv 包装在机制上毫无意义——[沙箱 RFC](2026-07-06-sandbox.md) § In-process tools 记录了这一点,并把跨家族执行留作一个延后阶段,附带一个未决问题:进程内执行是各 seam 各自表达,还是变成一个统一的 harness 能力。本 Agent Note 就是那个阶段,并给出答案:一个共享的策略归属,在每个家族各自正确的高度上做 per-seam 执行。
+
+这个缺口不是 read-only 形状的。一个受限编码 agent 的产品模式是 `workspace-write`:bash 已经可以在工作区根目录下写入,而其外的一切都被拒绝,所以一个只能全部拒绝的 fs 执行会严格劣于禁用 fs 工具——模型会尝试在工作区内 `write`,被拒,然后学会绕道 `bash` heredoc。因此跨家族执行必须讲完整的模式阶梯,包括 `workspace-write` 要求的路径包含判定(规范化目标;`..`/符号链接/绝对路径逃逸),以及与 bash 相同的升级杠杆。
+
+第二个执行家族还暴露了原布局中的一个归属问题。部署默认值(`mode` + `workspaceRoot`)配置在 `dsh-bash-sandbox` 上,而 per-session 覆盖事件是 `bash/sandbox-mode`,由 `dsh-bash` 的 session-mode 工具集折叠与写入。当 fs 执行同一套策略时,要么 fs 读取 bash 的配置与事件(一个能力家族依赖同级插件的配置),要么各家族各持一份副本——两份 `workspaceRoot` 会漂移进沙箱 RFC 警告过的那个割裂世界:bash 受限于一个根,而 fs 围栏另一个根。
+
+## Decision
+
+三个相互协调的部分,全部在叶子 `cordis.yml` 中组合,均不触及 `agent-loop`。
+
+### `ctx.sandboxPolicy`——mode 与工作区根的统一归属
+
+`packages/sandbox/sandbox-policy/`(`@deepseek-ai/dsh-sandbox-policy`)注册 `ctx.sandboxPolicy`,即部署沙箱策略的唯一所有者:
+
+- `Config`:`mode`(封闭的 `SandboxMode` 联合,默认 `read-only`)与 `workspaceRoot`(默认进程 cwd,解析为绝对路径)。配置错误在加载时高声失败。
+- per-session 覆盖事件 `sandbox/mode`,连同它的纯折叠(`effectiveSandboxMode(events)`)、写入路径(`setSandboxMode(session, mode)`)与 `SANDBOX_MODES`。该事件是策略状态——被两个家族消费——所以它住在这里,而不在任一能力的 seam 里。它的形状与仅日志(log-only)语义遵循 `approval/*` 的先例。
+- `defaultMode` / `workspaceRoot` 访问器,供执行实现读取其 resolve 回退值与边界。
+
+`dsh-bash-sandbox` 自身不再携带任何沙箱配置——它注入 `sandboxPolicy` 并从中读取默认值;其 `resolve()` 优先级不变(升级授权 > per-call 盖章 > 默认)。`dsh-tool-bash` 与 `dsh-tool-fs` 用 `effectiveSandboxMode` 折叠会话的 `sandbox/mode` 以对每次调用盖章;`dsh-permission` 预设与 ACP bridge 经由迁移后的 setter 写入。拥有 bash 执行的那个 seam 不再依赖 `dsh-session`——会话依赖随折叠一起迁到了策略包。
+
+### `dsh-fs-sandbox`——在提供方内部执行
+
+`packages/fs/fs-sandbox/`(`@deepseek-ai/dsh-fs-sandbox`)镜像 `bash-local`/`bash-sandbox` 的拆分:`SandboxedFileSystem extends LocalFileSystem`,注册为 `ctx.fs`,注入 `sandboxPolicy`。读取(`resolve`/`stat`/`readText`/`streamText`/`listDir`)原样透传——每种模式都允许读。两个变更操作在委托给继承来的原子写之前按模式执行:
+
+- `read-only` 直接拒绝 `writeText`/`editText`。
+- `workspace-write` 把规范化后的目标围栏于可写根集合——`dsh-sandbox` 中的 `writableRoots(policy)`:工作区根加上平台临时目录(`/tmp`、`os.tmpdir()`),各自 realpath——与 Seatbelt profile 授予的是同一个集合,所以 fs 围栏是这一个模式含义在 bwrap/Landlock/Seatbelt profile 之外的第四种方言,因此不会出现「write 工具不能写 `/tmp` 而 bash 能」的不对称。包含判定是对真实路径的前缀包含;目标在委托前被立即重新规范化(`resolve` 对最深的既有祖先做 realpath),因此自工具解析该目标以来被换出的祖先符号链接会被捕获。
+- `danger-full-access` 不加围栏地委托。
+
+拒绝是结构化的 `FS_SANDBOX_DENIED`,携带生效模式——区别于 `FS_PERMISSION_DENIED`(宿主 EACCES 是世界在拒绝;这里是策略在拒绝)。无文本推断:进程内围栏确切知道它拒绝了什么。per-call 载体是 `writeText`/`editText` 上一个末尾可选的 `sandboxMode`(文件系统侧对应 `BashExecRequest.sandboxMode`);该 seam 保持无会话依赖(由调用方盖章,正如 `resolve` 接收一个 cwd),而裸的本地后端携带并忽略它。`FileSystem.sandboxMode` 是能力事实(在基类与 `fs-local` 上为 `undefined`,在 `SandboxedFileSystem` 上为默认值),所以工具层按组合真相来宣告升级。
+
+威胁模型写在包 README 里:一道位于可信代码中、针对模型可控路径的策略围栏,而非内核边界——操作是 seam 自身的,只有目标路径不可信,所以「先规范化再判包含」是对这个面的完整答案(`code-runtime` 的「containment, not a security boundary」先例)。对不可信代码的内核级隔离仍是 `ctx.bash` 的职责。resolve 到系统调用之间残留的竞态被就地重新规范化收窄,只有平台原语(`openat2` `RESOLVE_BENEATH`)能彻底消除它,而那在此不值其可移植性代价。
+
+### 工具对等——一个拒绝标记、一条升级流程
+
+`dsh-tool-fs` 把生效模式盖章到每次变更上,并将 `FS_SANDBOX_DENIED` 映射为模型已从 bash 认识的标记:`[sandbox: file access denied under mode]`。当 `ctx.fs.sandboxMode` 在注册时报告一个受限模式,`write` 与 `edit` 宣告相同的 `sandbox_permissions` + `justification` 字段,教授相同的同回合重试,并在执行前解析相同的 `ctx.approval` 请求——四种结果及其逐字的 fail-closed 文案沿用自[沙箱 RFC](2026-07-06-sandbox.md) § Escalation(严格加宽在执行时针对调用的生效模式检查;授权由发起它的那一次调用消费;无任何新会话事件)。
+
+共享部分住在 `dsh-sandbox`,它拥有模式类型:`WIDER_MODES`、升级目标枚举、参数配对校验、拒绝/提示标记构造器,以及 `approveEscalation`——有序的 fail-closed 编排。`approveEscalation` 接收一个最小的结构化 approver(`EscalationApprover`,对 agent 与 call-id 类型泛型化),而非审批服务类型,所以 `dsh-sandbox` 不获得对 approval 或 agent 包的依赖:每个工具把自己的 `ctx.approval`、agent、call id 与工具名作为原料传入。`dsh-tool-bash` 与 `dsh-tool-fs` 都使用它们;跨文件重复检测门禁确保单一来源不走样。
+
+[`examples/acp-agent`](../../../../examples/acp-agent/cordis.yml) 组合加载 `dsh-sandbox-policy` 与 `dsh-fs-sandbox`,把 `mode`/`workspaceRoot` 配置移到策略条目,并去掉在受限模式下禁用整个 fs 栈的旧门控;`fs-policy`(read-before-edit)正交地叠加其上。系统提示仍然不陈述沙箱模式——标记会在真正重要的那一刻教会模型边界,依据沙箱 RFC 的线上证据。
+
+### 执行点:提供方,而非 intent gate
+
+沙箱 RFC 最初的跨家族草图把 fs 执行放在 `fs/write-intent`/`fs/edit-intent` 事件上。本 Agent Note 改为在提供方中执行,基于两个机制性事实:intent 槽是单决策、先到先得(已被 `dsh-fs-policy` 占据,其契约称第二个决策者为配置错误),且 intent 事件只由 `dsh-tool-fs` 派发——一个直连 `ctx.fs` 的调用方(一个 cordis 挂载插件、一个自定义工具)会绕过它们,而提供方级执行按构造覆盖每一个调用方。沙箱 RFC 的延后阶段措辞在同一变更中被更新以匹配。
+
+### 范围之外
+
+- **`ctx.web` 的网络策略**——`SandboxMode` 只声明文件效果;在 bash `curl` 畅通时给一个仅限 web 的网络旋钮会是一道假边界。待某个 bash 后端能执行网络(bwrap `--unshare-net`、Landlock ABI v4+)时再议。
+- **`subagent-acp` 消费者** 与 **per-session 工作区根**——沙箱 RFC 未变的延后阶段;把根集中到 `ctx.sandboxPolicy` 是后者的铺垫,而非其设计。
+- **统一的 per-tool 沙箱运行时**——因沙箱 RFC 中的理由继续否决。
+
+## Alternatives considered
+
+- **在 `fs/*` intent 事件上执行(沙箱 RFC 的原始草图)**——因 § 执行点 中的两个机制性事实被否决:单槽先到先得且已被占据,以及对直连 `ctx.fs` 调用方的绕过。提供方级执行覆盖每一个调用方,并镜像 bash 的换实现形态。
+- **在 `tools/pre-execute` 中执行**——否决:监听器在 `resolve()` 之前看到模型的原始路径字符串,因此它会重新实现 cwd 默认化与符号链接规范化,并且仍与真正的 resolve 竞态。对 `workspace-write`(一个对规范路径的判定)而言是取消资格级的。
+- **在 `dsh-tool-fs` 中做内联检查**——否决:只覆盖工具路径(与 intent 事件同样的绕过),并在规范目标已存在之上重复了一层 resolve 知识。
+- **在 `dsh-fs-local` 上加一个 `mode` 标志而非同级后端**——否决:能力事实必须是组合真相,正如 `dsh-bash-local` 对 `dsh-bash-sandbox`;一个配置标志会让工具的宣告取决于配置,而 bash 家族已经确立了同级包形态。
+- **经受限 helper 子进程做内核级 fs 变更**——否决:每次写一个进程;`editText` 的读-匹配-写临界区不得不整体搬进子进程才能保持原子;而威胁面(可信操作、不可信路径参数)不需要内核——可信代码中的围栏就是完整答案,而不可信代码隔离仍在 `ctx.bash`。
+- **带加载期一致性校验的 per-family 策略配置**——否决:一个事实两个归属,靠一个必须枚举每个未来执行家族的校验来打补丁;策略服务让漂移不可表达,而非被检测到。
+- **把覆盖事件留在 `dsh-bash` 里作 `bash/sandbox-mode`**——否决:该事件是被两个家族消费的策略状态;保留 bash 命名会迫使 `dsh-fs-sandbox` 依赖 bash 词汇。预发布阶段,该改名是同一变更内的迁移,附带快照重录,无任何 shim。
+- **把升级编排从 approval/agent 包导入 `dsh-sandbox`**——否决:那会倒置分层(一个基础词汇包依赖 UI/agent 包)。结构化 approver 让逻辑单一来源于 `dsh-sandbox`,而依赖留在本就持有它们的工具层。
+- **fs seam 上一个合并的 mutation-options 对象**(per-call 载体最初草拟的形状)——因摩擦被否决:它会搅动每一个 `writeText`/`editText` 调用方,并把 `signal` 拆进变更专用的选项包,而读取仍保持位置参数。一个末尾可选的 `sandboxMode` 匹配 bash 的携带并忽略模式,并使 `signal` 在整个 seam 上保持对称。
+- **现在就在 `SandboxPolicy` 上加额外的可写根授权**——照旧延后:`writableRoots()` 如今由模式含义推导;临时授权是沙箱 RFC 留下的升级作用域问题。
+
+## Consequences
+
+已交付的部分——§ Testing 的各层各自钉住:
+
+- 在 `read-only` 下,`write`/`edit` 返回 `[sandbox: file access denied under read-only mode]` 标记,磁盘不受触动;`read`/`listDir` 与 `dsh-fs-local` 行为一致。
+- 在 `workspace-write` 下,变更落在工作区根与临时目录下,其外被拒;包含矩阵——`..` 穿越、指向外部的绝对路径、一个既有的、指向外部的工作区内符号链接目录,以及在这样一个符号链接下新建的文件——在真实磁盘上拒绝每一种逃逸。
+- 一个被拒的 fs 变更,携带 `sandbox_permissions` + `justification` 重试一次,会经组合的审批链提示;一次授权让恰好那一次调用在更宽的模式下运行且写入落盘;rejected/cancelled/unavailable 各自产生其逐字的 fail-closed 文案且不做任何变更。
+- 一次 `permission` 预设切换同时管辖两个家族:会话切换模式后,下一次 bash 调用与下一次 fs 变更都从同一个 `sandbox/mode` 折叠遵循新模式。
+- 一次无 per-call 盖章的直连 `ctx.fs.writeText` 会被围栏于部署默认值。
+- `write`/`edit` 上的升级字段恰好在被挂载的 `ctx.fs` 受限时存在,在 `dsh-fs-local` 下不存在。
+- `agent-loop` 未被触动——一切都骑在 `ctx.sandboxPolicy`、`ctx.fs` seam、`SessionEventMap` 合并,以及工具执行管线之上。
+
+代价与接受的限制:
+
+- **fs 围栏是策略边界,而非内核边界。** 它的威胁面是模型选定的路径,而非对抗性宿主进程;resolve 到系统调用之间残留的 TOCTOU 被收窄而非消除,README 已如实声明。内核边界仍属 bash。
+- **`dsh-bash-sandbox` 获得对 `ctx.sandboxPolicy` 的硬依赖。** 每个沙箱化组合要么加一个 `cordis.yml` 条目,要么在加载时高声失败——这是有意的预发布奠基之举;示例在同一变更内更新。
+- **围栏与 runner 的对等是推导出来的,而非断言的。** fs 围栏与 Seatbelt profile 都从 `writableRoots` 取其可写集合,一个对等单元测试钉住这些集合;一个 runner profile 若在不经该函数的情况下改变其可写集合便会漂移。
+- **标记与升级教学如今服务于两个家族。** 措辞改动是 `dsh-sandbox` 中一个构造器背后的协调编辑;重复检测门禁与钉住的快照维持单一来源,代价是 fs 与 bash 无法在不拆分该构造器的情况下有意地在措辞上分道。
+
+## Testing
+
+- 单元:`dsh-sandbox` 钉住升级阶梯、标记构造器、参数配对校验,以及 `approveEscalation` 的有序 fail-closed 序列(非加宽、无 approval、无 agent、各结果),外加 `writableRoots`/`canonicalPath`。`dsh-sandbox-policy` 钉住默认访问器、折叠/setter、加载期模式拒绝,以及 HMR 安全。`dsh-fs-sandbox` 在真实文件系统上钉住 per-mode 围栏与包含矩阵(内部、临时目录、绝对路径-外部、`..`、指向外部的符号链接目录、其下的新建文件、路径等于根、以分隔符结尾的根),外加 per-call 覆盖与 HMR 安全。`dsh-tool-fs` 钉住宣告门控、模式盖章、折叠、拒绝标记映射,以及完整的升级矩阵(授权、拒绝、无服务、无 agent、配对、非受限守卫)。`dsh-tool-bash`、`dsh-bash-sandbox` 与 `dsh-permission` 迁移到迁移后的策略/工具集。
+- 快照:acp-agent 示例组合 `dsh-sandbox-policy` + `dsh-fs-sandbox`;被钉住的 header 携带 fs 升级字段与 `sandbox/mode` 事件名,一次性重录。
diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
similarity index 65%
rename from docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
rename to .agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
index c7984ee20a..28ecd2a765 100644
--- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-14-time-context-plugin.md: aa24c6246718cfe0bb3ed63d0791cf890514d9fe
-2026-07-14-time-context-plugin.zh.md: e939ff1d0cb250f3fa7a7db34b50d92760e3374d
+2026-07-14-time-context-plugin.md: 189f75fc12fe12e9dec56fc71ea901ec2eaa8b19
+2026-07-14-time-context-plugin.zh.md: 12671cb891531627fffabb7bd91a1532bc3de6b9
diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.md
similarity index 83%
rename from docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md
rename to .agents/notes/implemented/feature/2026-07-14-time-context-plugin.md
index aa24c62467..189f75fc12 100644
--- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md
+++ b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.md
@@ -1,4 +1,4 @@
-# RFC: Optional time-context plugin
+# Agent Note: Optional time-context plugin
Status: implemented
@@ -32,11 +32,11 @@ When `timeZone` is omitted, `Intl.DateTimeFormat` resolves the Node process's sy
### Logging and token shape
-The loop records the temporal block through `request/header` and `request/header-delta` before transmission, satisfying the [reconstructable-requests contract](../architecture/2026-07-05-reconstructable-requests.md). Each request carries one current block; earlier readings do not remain in conversation history. The plugin owns the fact and contributes it through the prompt registry, following the [prompt-variables RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) without a loop special case.
+The loop records the temporal block in full `request/header` snapshots before transmission, satisfying the [reconstructable-requests contract](../architecture/2026-07-05-reconstructable-requests.md). Each request carries one current block; earlier readings do not remain in conversation history. The plugin owns the fact and contributes it through the prompt registry, following the [prompt-variables Agent Note](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) without a loop special case.
## Testing
-Unit tests pin formatting, baselines, refresh policy, validation, per-agent state, disposal, and load-time system-zone capture. A real agent-loop test pins the transmitted prompt and `request/header-delta`. A keyless subprocess e2e boots a test-only `cordis.yml` through the real Loader and stdio app, omits `timeZone` under a controlled `TZ`, drives two turns, and verifies the persisted request headers externally. Default snapshot compositions omit the plugin, so their transcript fixtures contain no temporal block.
+Unit tests pin formatting, baselines, refresh policy, validation, per-agent state, disposal, and load-time system-zone capture. A real agent-loop test pins the transmitted prompt and full `request/header` snapshots. A keyless subprocess e2e boots a test-only `cordis.yml` through the real Loader and stdio app, omits `timeZone` under a controlled `TZ`, drives two turns, and verifies the persisted request headers externally. Default snapshot compositions omit the plugin, so their transcript fixtures contain no temporal block.
## Alternatives considered
@@ -54,6 +54,6 @@ Unit tests pin formatting, baselines, refresh policy, validation, per-agent stat
- Opted-in models receive a zoned clock and inter-turn duration without a tool call. The system-prompt cost is fixed per request instead of growing with the session.
- An omitted `timeZone` follows the process's `TZ`, host, or container zone as observed at plugin load. Operators must configure an explicit zone when the deployment environment does not represent the intended user.
-- A refresh changes the request header and can add a `request/header-delta`. `refreshIntervalMs` trades freshness against durable deltas; `0` records a new value on every step whose whole-second rendering changes.
+- A refresh changes the request header and can add a full `request/header` snapshot with reason `change`. `refreshIntervalMs` trades freshness against the number and size of durable full snapshots; `0` records a new value on every step whose whole-second rendering changes.
- No request exists solely to refresh time. A long-running tool leaves the prior reading until the next step assembles.
- Duration reflects harness processing time at durable append boundaries, not client-network latency before logging. Preserving a client-origin timestamp requires a separate durable input contract.
diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md
similarity index 81%
rename from docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md
rename to .agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md
index e939ff1d0c..12671cb891 100644
--- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md
@@ -1,4 +1,4 @@
-# RFC:可选时间上下文插件
+# Agent Note:可选时间上下文插件
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
## 问题
-本记录中的动态系统提示词存储和刷新决策已由[持久的逐步骤时间上下文](2026-07-16-durable-per-step-time-context.md)取代。需要显式启用的包(package)、分区时间格式和校验仍然保留;后续 RFC 负责当前的模型可见与持久性契约。
+本记录中的动态系统提示词存储和刷新决策已由[持久的逐步骤时间上下文](2026-07-16-durable-per-step-time-context.md)取代。需要显式启用的包(package)、分区时间格式和校验仍然保留;后续 Agent Note 负责当前的模型可见与持久性契约。
如果部署方既未在提示词中提供时钟,也未给模型提供查询工具,agent(智能体)请求就无法获得实时准确的时间。静态文本会变得陈旧,而对于日期、截止时间或闲置时长等常规推理,调用工具会增加开销。缺少已经过去的时长时,模型无法区分紧接着发送的消息与上一条消息几小时后才发送的消息。
@@ -32,11 +32,11 @@ Status: implemented
### 日志与 token 形态
-agent loop(智能体循环)会在发送前通过 `request/header` 和 `request/header-delta` 记录时间区块,从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块;先前的读数不会保留在会话历史中。该插件拥有时间信息,并按照[提示词变量 RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)通过提示词注册表贡献该信息,无需为循环添加特殊分支。
+agent loop(智能体循环)会在发送前通过完整的 `request/header` 快照记录时间区块,从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块;先前的读数不会保留在会话历史中。该插件拥有时间信息,并按照[提示词变量 Agent Note](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)通过提示词注册表贡献该信息,无需为循环添加特殊分支。
## 测试
-单元测试固定格式化、基线、刷新策略、校验、逐 agent 状态、资源释放行为,以及系统时区在加载时的捕获行为。使用真实 agent loop 的测试固定实际发送的提示词和 `request/header-delta`。无密钥子进程端到端测试通过真实 Loader 和 stdio 应用启动测试专用 `cordis.yml`,在受控 `TZ` 下省略 `timeZone`,驱动两个轮次,并从外部校验持久请求头。默认快照组合不包含该插件,因此其中的 transcript(文本记录)fixture(测试前置数据)不包含时间区块。
+单元测试固定格式化、基线、刷新策略、校验、逐 agent 状态、资源释放行为,以及系统时区在加载时的捕获行为。使用真实 agent loop 的测试固定实际发送的提示词和完整的 `request/header` 快照。无密钥子进程端到端测试通过真实 Loader 和 stdio 应用启动测试专用 `cordis.yml`,在受控 `TZ` 下省略 `timeZone`,驱动两个轮次,并从外部校验持久请求头。默认快照组合不包含该插件,因此其中的 transcript(文本记录)fixture(测试前置数据)不包含时间区块。
## 考虑过的替代方案
@@ -54,6 +54,6 @@ agent loop(智能体循环)会在发送前通过 `request/header` 和 `reque
- 选择加入的模型无需调用工具,即可获得分区时钟和轮次间隔时长。每个请求的系统提示词成本固定,不会随会话增长。
- 省略 `timeZone` 时,插件采用加载时观察到的进程 `TZ`、主机或容器时区。当部署环境不能代表目标用户时,运维方必须显式配置时区。
-- 刷新会改变请求头,并可能新增 `request/header-delta`。`refreshIntervalMs` 用新鲜度换取持久增量记录的数量;设为 `0` 时,每个整秒渲染结果发生变化的步骤都会记录新值。
+- 刷新会改变请求头,并可能新增一份 reason 为 `change` 的完整 `request/header` 快照。`refreshIntervalMs` 用新鲜度换取完整持久快照的数量与大小;设为 `0` 时,每个整秒渲染结果发生变化的步骤都会记录新值。
- 系统不会仅为刷新时间而创建请求。长时间运行的工具会保留先前读数,直至下一步骤开始组装。
- 时长反映持久追加边界处的 harness 处理时间,不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳,需要单独的持久输入契约。
diff --git a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml
similarity index 62%
rename from docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml
rename to .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml
index a89248ac6f..f037660761 100644
--- a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-16-durable-per-step-time-context.md: 12d8191eb72b3fabd3164f13a77d9944631b906e
-2026-07-16-durable-per-step-time-context.zh.md: 977ffb7276011ac9ae9b8a72a726bb23baba0a2d
+2026-07-16-durable-per-step-time-context.md: 2d7076d51dbe1a64e5042230bddc6844141ff265
+2026-07-16-durable-per-step-time-context.zh.md: 432e0305cf44dcce1053c6580c9f0039309a7af4
diff --git a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md
similarity index 93%
rename from docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md
rename to .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md
index 12d8191eb7..2d7076d51d 100644
--- a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md
+++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md
@@ -1,4 +1,4 @@
-# RFC: Durable per-step time context
+# Agent Note: Durable per-step time context
Status: implemented
@@ -44,7 +44,7 @@ Their baseline is the durable event timestamp of the preceding time-context mess
Each reading remains a normal surface node until compaction shadows it; positive interval scheduling never removes existing readings. A later request therefore sees the cumulative unshadowed readings that affected earlier preparation and steps, rather than a system-prompt value rewritten in place.
-The plugin contributes nothing to system-prompt assembly. `request/header` and `request/header-delta` contain no time-context text; request reconstruction obtains the complete durable surface prefix at each `step/start`. Readings and requests need not map one-to-one because a failed preparation can leave a reading while interval suppression can prepare a request without appending one. The plugin depends on the agent registry for its lifecycle listener and does not require the system-prompt service at runtime.
+The plugin contributes nothing to system-prompt assembly. `request/header` contains no time-context text; request reconstruction obtains the complete durable surface prefix at each `step/start`. Readings and requests need not map one-to-one because a failed preparation can leave a reading while interval suppression can prepare a request without appending one. The plugin depends on the agent registry for its lifecycle listener and does not require the system-prompt service at runtime.
## Testing
diff --git a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md
similarity index 93%
rename from docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md
rename to .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md
index 977ffb7276..432e0305cf 100644
--- a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md
@@ -1,4 +1,4 @@
-# RFC: 持久的逐步骤时间上下文
+# Agent Note: 持久的逐步骤时间上下文
Status: implemented
@@ -44,7 +44,7 @@ Elapsed since the preceding step context: .
每个读数都作为普通表层节点保留,直至压缩将其隐藏;正数间隔调度绝不会移除已有读数。因此,后续请求会看到影响先前准备过程和步骤且尚未被隐藏的累计读数,而不是一个被原地改写的系统提示词值。
-插件不向系统提示词组装贡献任何内容。`request/header` 和 `request/header-delta` 不包含时间上下文文本;请求重建从每个 `step/start` 取得完整的持久表层前缀。读数与请求无需一一对应,因为失败的准备过程可能留下读数,而间隔抑制也可能使请求准备过程不追加读数。插件通过 agent 注册表使用生命周期监听器,运行时不需要系统提示词服务。
+插件不向系统提示词组装贡献任何内容。`request/header` 不包含时间上下文文本;请求重建从每个 `step/start` 取得完整的持久表层前缀。读数与请求无需一一对应,因为失败的准备过程可能留下读数,而间隔抑制也可能使请求准备过程不追加读数。插件通过 agent 注册表使用生命周期监听器,运行时不需要系统提示词服务。
## 测试
diff --git a/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml
new file mode 100644
index 0000000000..bc77edbd0a
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml
@@ -0,0 +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-17-dedicated-full-screen-tui-front-door.md: 8fbc5dddc029190b346075a65c9e7857187f3d2b
+2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 6ddc3523b7a7173013efe2ef15c5ca0e940929fb
diff --git a/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md
new file mode 100644
index 0000000000..8fbc5dddc0
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md
@@ -0,0 +1,48 @@
+# Agent Note: Dedicated full-screen TUI front door
+
+Status: implemented
+
+English | [中文](2026-07-17-dedicated-full-screen-tui-front-door.zh.md)
+
+## Problem
+
+At the time this front door was introduced, the line-oriented agent handled pipes and ordinary terminals, but a full-screen coding interface had to own raw input, differential screen drawing, cursor state, overlays, and terminal restoration. Combining those contracts in one UI plugin would have coupled a stream-oriented path to a TTY-only lifecycle. The later [redundant-agent removal](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) removes that line agent; this Note continues to own the TUI design.
+
+The interactive channel must remain a Cordis plugin over the same agent, session, tool, and user-interaction services as every other front door. It needs to resume durable history, follow compaction replacements, display tool-owned presentation, and restore the terminal on startup failure and disposal. A standalone chat application or a second agent composition would duplicate behavior outside the plugin graph.
+
+## Decision
+
+DeepSeek Harness ships [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) as a dedicated Cordis plugin. It owns terminal input and presentation only; agent lifecycle, session persistence, tool execution, and the model-facing question tool remain separate composition entries. The plugin requires both stdin and stdout to be TTYs and fails instead of silently changing to line-oriented behavior.
+
+The app layer has one terminal front door. `@deepseek-ai/dsh-tui-demo` mounts the TUI before the configured agent, and `examples/tui-agent` owns the interactive coding composition and Code Mode overlay directly. Non-interactive tasks use `@deepseek-ai/dsh-cli-demo`; ACP remains a separate editor protocol.
+
+The selected front door receives the exact generated or resumed `SessionId` used by the pre-created agent. It mounts before the agent composition, waits for the matching root agent, and enters full-screen mode only after that agent exists. A matching `agent-loop/config-start-failed` event is therefore reported before screen takeover and exits with status 1.
+
+### Session projection and interaction
+
+The TUI rebuilds the transcript from the active `session.surface` and reprojects it whenever an event carries a `surfaceOp`, so resumed and compacted history matches the model-visible conversation. It renders Markdown text and reasoning, token totals, the latest `todo/write` plan, and tool cards produced through each tool definition's `presentCall` and `presentResult` methods. Pending chunks and tool calls update the same components that completed events settle.
+
+Editor input calls `agent.send()` while idle and `agent.steer()` while a turn is running. Cancellation, reasoning visibility, tool-card expansion, redraw, transcript clearing, and exit are terminal-only controls. The plugin registers the shared `userInteraction` provider and presents questions as queued keyboard overlays; agent behavior and answer logging remain owned by their existing services.
+
+### Terminal ownership
+
+Before model output, session data, tool presentation, questions, configuration, or diagnostics reach pi-tui or the terminal title, `displayText()` renders C0 and C1 controls other than line feeds as visible hexadecimal escapes. Only the TUI and pi-tui create ANSI control sequences.
+
+The built-in palette uses standard 16-color ANSI foregrounds and SGR attributes, keeps body text and backgrounds at terminal defaults, and uses reverse video for selection. Host terminals therefore remap the interface for light and dark themes without a TUI-specific theme setting; `color: false` removes styling.
+
+## Verification
+
+The implemented [TUI terminal-state snapshot Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) owns the four-layer verification contract: direct behavior tests, transient semantic terminal snapshots, recorded JSONL journeys through production tools, and Loader/PTY smoke tests. The package README owns configuration, commands, model-visible effects, and current limitations.
+
+## Alternatives considered
+
+- **Keep readline and full-screen modes inside `@deepseek-ai/dsh-stdio`** — rejected because line-oriented output and differential TTY rendering have different dependencies, input rules, logging ownership, and teardown obligations. Separate packages keep the pipe-safe contract small and explicit.
+- **Let the TUI plugin silently downgrade when either stream is not a TTY** — rejected because a fallback hides deployment mistakes and changes interaction semantics. The app bundle may select a front door with `auto`; an explicitly mounted TUI fails loud.
+- **Keep TUI wiring and tests under the readline `repl-agent` leaf** — rejected because one leaf would represent two distinct front doors and break symmetry with `acp-agent`. A dedicated `tui-agent` leaf owns TUI overlays and tests while reusing the repl-agent backend composition.
+
+## Consequences
+
+- Interactive terminal work has a stateful Markdown, card, plan, and question interface with no second terminal protocol to keep aligned.
+- The TUI carries a pi-tui dependency and a strict TTY requirement; non-TTY deployments use the Headless app or a structured protocol.
+- Session projection makes resume and compaction consistent with the durable conversation, but one configured session owns the transcript and editor.
+- Tool packages extend terminal cards through their existing presentation methods without adding tool-specific branches to the TUI.
diff --git a/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md
new file mode 100644
index 0000000000..6ddc3523b7
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md
@@ -0,0 +1,48 @@
+# Agent Note: 独立的全屏 TUI 入口
+
+Status: implemented
+
+[English](2026-07-17-dedicated-full-screen-tui-front-door.md) | 中文
+
+## 问题
+
+在本入口引入时,面向行的 agent 负责 pipe 与普通终端,但全屏 coding 界面必须负责原始输入、差分绘制、光标状态、浮层和终端恢复。把这两类契约合并到一个 UI 插件中,会迫使面向 stream 的路径依赖仅适用于 TTY 的生命周期。后续的[移除重复 agent 决策](../simplification/2026-07-20-remove-stdio-and-echo-agents.md)移除了这个面向行 agent;本 Note 继续负责 TUI 设计。
+
+交互通道必须继续作为 Cordis 插件,使用与其他入口相同的 agent(智能体)、会话、工具和用户交互服务。它需要恢复持久历史、跟随压缩替换、显示工具自有的呈现内容,并在启动失败和资源释放时恢复终端。独立聊天应用或第二套 agent 组合会在插件图之外重复实现这些行为。
+
+## 决策
+
+DeepSeek Harness 将 [`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 作为独立的 Cordis 插件交付。该插件只负责终端输入与呈现;agent 生命周期、会话持久化、工具执行以及模型可见的提问工具仍由不同组合项负责。插件要求 stdin 和 stdout 均为 TTY;条件不满足时会失败,不会静默切换为逐行输出。
+
+应用组合层只有一个终端入口。`@deepseek-ai/dsh-tui-demo` 在已配置 agent 之前挂载 TUI,`examples/tui-agent` 直接拥有交互式 coding 组装及其 Code Mode overlay。非交互任务使用 `@deepseek-ai/dsh-cli-demo`;ACP 仍是独立的编辑器协议。
+
+所选入口接收预创建 agent 使用的同一个新建或恢复 `SessionId`。入口先于 agent 组合挂载,等待相符的根 agent 出现,然后才进入全屏模式。因此,相符的 `agent-loop/config-start-failed` 事件会在接管屏幕前报告,并以状态码 1 退出。
+
+### 会话投影与交互
+
+TUI 从活跃的 `session.surface` 重建 transcript(文本记录),并在事件携带 `surfaceOp` 时重新投影,因此恢复或压缩后的历史与模型可见会话保持一致。TUI 渲染 Markdown 文本与推理、token 用量、最新 `todo/write` 计划,以及各工具定义通过 `presentCall` 和 `presentResult` 方法生成的工具卡片。进行中的分片与工具调用会更新同一组组件,随后由完成事件收束状态。
+
+agent 空闲时,编辑器输入调用 `agent.send()`;轮次运行中则调用 `agent.steer()`。取消、推理显隐、工具卡片展开、重绘、清空 transcript 和退出都只是终端控制。插件注册共享的 `userInteraction` 提供方,以排队的键盘浮层呈现问题;agent 行为和答案日志仍由既有服务负责。
+
+### 终端所有权
+
+在模型输出、会话数据、工具呈现、问题、配置或诊断信息进入 pi-tui 或终端标题前,`displayText()` 会把换行之外的 C0 和 C1 控制字符显示为十六进制转义文本。只有 TUI 和 pi-tui 可以生成 ANSI 控制序列。
+
+内置配色仅使用标准 16 色 ANSI 前景色和 SGR 属性,正文文字和背景沿用终端默认值,选中项使用反显。因此,宿主终端可以直接按浅色或深色主题重映射界面,无需 TUI 专用主题设置;`color: false` 会移除样式。
+
+## 验证
+
+已实现的 [TUI 终端状态快照 Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) 规定四层验证契约:直接行为测试、瞬态语义终端快照、通过生产工具执行的已录制 JSONL 流程,以及 Loader/PTY 冒烟测试。包(package)README 负责记录配置、命令、模型可见效果和当前限制。
+
+## 曾考虑的替代方案
+
+- **把 readline 与全屏模式都保留在 `@deepseek-ai/dsh-stdio` 中**:不予采纳,因为逐行输出和差分 TTY 渲染具有不同的依赖、输入规则、日志所有权和资源清理义务。拆分为独立包可以让管道安全契约保持精简、明确。
+- **当任一进程流不是 TTY 时,让 TUI 插件静默降级**:不予采纳,因为回退会掩盖部署错误并改变交互语义。应用包可以通过 `auto` 选择入口;明确挂载的 TUI 会快速失败。
+- **把 TUI 接线与测试保留在 readline `repl-agent` 叶节点下**:不予采纳,因为一个叶节点会代表两个不同入口,也会破坏它与 `acp-agent` 的对称性。独立的 `tui-agent` 叶节点负责 TUI 浮层和测试,同时复用 repl-agent 的后端组合。
+
+## 后果
+
+- 交互式终端拥有带状态的 Markdown、卡片、计划和提问界面,无需再对齐第二套终端协议。
+- TUI 会引入 pi-tui 依赖并严格要求 TTY;非 TTY 部署使用 Headless app 或结构化协议。
+- 会话投影使恢复和压缩与持久会话保持一致,但只有一个已配置会话拥有 transcript 和编辑器。
+- 工具包通过既有呈现方法扩展终端卡片,无需在 TUI 中增加工具专用分支。
diff --git a/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.i18n.yaml
new file mode 100644
index 0000000000..4edd7b7223
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.i18n.yaml
@@ -0,0 +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-20-windows-tui-support.md: 6b728486dd50faac067933ce06f883447aae821f
+2026-07-20-windows-tui-support.zh.md: 2b53b05ff6231361d79b4304181dc0e6d8e24e68
diff --git a/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.md b/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.md
new file mode 100644
index 0000000000..6b728486dd
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.md
@@ -0,0 +1,33 @@
+# Agent Note: Support the TUI on Windows
+
+Status: implemented
+
+English | [中文](2026-07-20-windows-tui-support.zh.md)
+
+## Problem
+
+The full-screen TUI delegates raw input, ANSI rendering, resize events, and terminal restoration to pi-tui's `ProcessTerminal`. That dependency contains a native Windows console path, but the repository's real-process smoke used Python's POSIX-only `pty` and `termios` modules. Skipping that smoke on Windows would leave the supported product path without coverage for startup, input, interaction, failure reporting, or restoration.
+
+The TUI platform contract must follow the runtime shipped to users rather than the portability of one test driver. A platform exclusion is justified only when the product has an unsupported runtime dependency or a demonstrated semantic gap.
+
+## Decision
+
+[`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) supports interactive terminals on Windows as well as macOS and Linux. The product continues to use pi-tui's `ProcessTerminal`; on Windows it enables virtual-terminal input after raw mode and avoids the Unix-only `SIGWINCH` refresh. DeepSeek Harness adds no platform rejection or reduced Windows mode.
+
+The real Loader smoke selects a native pseudo-terminal boundary by host. macOS and Linux retain the Python POSIX PTY driver. Windows uses `node-pty` and ConPTY. Both drivers receive the same launch command, environment, terminal dimensions, marker-gated input actions, timeout, expected exit code, and output assertions, and all three smoke scenarios run on every supported platform.
+
+`node-pty` is a test-only dependency of the examples workspace. Its reviewed native install script is explicitly enabled in `pnpm-workspace.yaml`; production TUI packages do not acquire a new dependency or subprocess layer.
+
+## Alternatives considered
+
+- **Declare the TUI unsupported on Windows** — rejected because the pinned terminal runtime implements Windows console input explicitly and the harness has no POSIX-only production dependency. A documentation-only exclusion would discard an existing product path to accommodate a test harness gap.
+- **Run the POSIX driver through MSYS, Cygwin, or WSL** — rejected because that would test a compatibility environment rather than the native Windows console path users run.
+- **Use `node-pty` on every host** — rejected because the established POSIX driver already provides the macOS and Linux boundary; replacing it would widen the runtime change without improving those hosts. Platform-specific drivers reserve the `node-pty` runtime path for Windows while sharing one scenario contract.
+- **Rely on renderer unit tests and semantic terminal snapshots** — rejected because fake terminals do not prove Loader boot, real raw input, process exit, or terminal restoration at the operating-system boundary.
+
+## Consequences
+
+- The Windows artifact lane executes the startup, scripted interaction, resume-failure, and restoration scenarios, and the suite has no supported-platform skip.
+- The Windows process proof depends on ConPTY and a pinned `node-pty` release; changing that dependency or its allowed install script requires native-boundary review.
+- The two PTY drivers can differ internally, but shared inputs and assertions keep their observable TUI contract aligned.
+- Windows support remains bounded by the Node and pi-tui versions shipped by the repository; unsupported historical Windows console environments do not receive a compatibility layer.
diff --git a/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.zh.md b/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.zh.md
new file mode 100644
index 0000000000..2b53b05ff6
--- /dev/null
+++ b/.agents/notes/implemented/feature/2026-07-20-windows-tui-support.zh.md
@@ -0,0 +1,33 @@
+# Agent Note: 在 Windows 上支持 TUI
+
+Status: implemented
+
+[English](2026-07-20-windows-tui-support.md) | 中文
+
+## 问题
+
+全屏 TUI 将原始输入、ANSI 渲染、终端尺寸变更事件和终端恢复委托给 pi-tui 的 `ProcessTerminal`。该依赖已实现原生 Windows 控制台路径,但仓库的真实进程冒烟测试此前使用 Python 中仅适用于 POSIX 的 `pty` 和 `termios` 模块。若在 Windows 上跳过该测试,这条受支持的产品路径便会缺少针对启动、输入、交互、失败报告和终端恢复的测试覆盖率。
+
+TUI 平台契约必须以交付给用户的运行时为准,而不是取决于某个测试驱动程序的可移植性。只有产品存在不受支持的运行时依赖,或已证实存在语义缺口时,排除某个平台才有依据。
+
+## 决策
+
+[`@deepseek-ai/dsh-tui`](../../../../packages/ui/tui/README.md) 在 Windows、macOS 和 Linux 上均支持交互式终端。产品继续使用 pi-tui 的 `ProcessTerminal`;在 Windows 上,它会在进入原始模式后启用虚拟终端输入,并避开仅适用于 Unix 的 `SIGWINCH` 刷新。DeepSeek Harness 不增加平台拒绝逻辑,也不采用功能受限的 Windows 模式。
+
+真实 Loader 冒烟测试根据宿主选择原生伪终端边界。macOS 和 Linux 继续使用 Python POSIX PTY 驱动,Windows 则使用 `node-pty` 和 ConPTY。两种驱动接收相同的启动命令、环境、终端尺寸、以标记为触发条件的输入动作、超时、预期退出码和输出断言;3 个冒烟场景都会在每个受支持平台上运行。
+
+`node-pty` 是 examples 工作区仅供测试使用的依赖。该依赖经评审的原生安装脚本在 `pnpm-workspace.yaml` 中显式启用;生产 TUI 包(package)不会新增依赖或子进程层。
+
+## 曾考虑的替代方案
+
+- **声明 TUI 不支持 Windows**:不予采纳,因为固定版本的终端运行时已显式实现 Windows 控制台输入,且 harness 没有仅适用于 POSIX 的生产依赖。仅通过文档排除 Windows,等于为迁就测试 harness 的缺口而舍弃现有产品路径。
+- **通过 MSYS、Cygwin 或 WSL 运行 POSIX 驱动**:不予采纳,因为这会测试兼容环境,而不是用户实际运行的原生 Windows 控制台路径。
+- **在所有宿主上使用 `node-pty`**:不予采纳,因为现有 POSIX 驱动已经为 macOS 和 Linux 提供所需边界;替换该驱动会扩大运行时变更范围,却不会给这两个宿主带来改进。按平台选择驱动,仅在 Windows 上启用 `node-pty` 运行时路径,同时共享同一份场景契约。
+- **依赖渲染器单元测试和语义终端快照**:不予采纳,因为模拟终端无法证明 Loader 启动、真实原始输入、进程退出或操作系统边界上的终端恢复。
+
+## 后果
+
+- Windows 产物 lane 执行启动、脚本化交互、配置恢复失败和终端恢复场景,这套测试不会在任何受支持平台上跳过。
+- Windows 进程级验证依赖 ConPTY 和固定版本的 `node-pty`;变更该依赖或允许执行的安装脚本时,必须进行原生边界评审。
+- 两种 PTY 驱动的内部实现可以不同,但共享的输入和断言会使其可观测 TUI 契约保持一致。
+- Windows 支持范围以仓库交付的 Node 和 pi-tui 版本为界;不受支持的旧版 Windows 控制台环境不会获得兼容层。
diff --git a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.md b/.agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md
similarity index 99%
rename from docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.md
rename to .agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md
index 8fd37a1656..8f32202c8f 100644
--- a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.md
+++ b/.agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md
@@ -1,4 +1,4 @@
-# RFC: Doc-sync enforcement
+# Agent Note: Doc-sync enforcement
Status: implemented
diff --git a/docs/rfc/implemented/process/2026-06-11-quality-gates.md b/.agents/notes/implemented/process/2026-06-11-quality-gates.md
similarity index 87%
rename from docs/rfc/implemented/process/2026-06-11-quality-gates.md
rename to .agents/notes/implemented/process/2026-06-11-quality-gates.md
index 9f92791f4a..84c3da7b95 100644
--- a/docs/rfc/implemented/process/2026-06-11-quality-gates.md
+++ b/.agents/notes/implemented/process/2026-06-11-quality-gates.md
@@ -1,4 +1,4 @@
-# RFC: Mechanical quality gates over prose guidelines
+# Agent Note: Mechanical quality gates over prose guidelines
Status: implemented
@@ -15,7 +15,7 @@ Every AGENTS.md promise gets a command that exits non-zero, wired into git hooks
- jscpd detects cross-file clones in package production TypeScript and repository scripts; narrow source-range exceptions document deliberately parallel implementations.
- Per-file 100% coverage on `packages/*/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion.
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM), and a NodeNext consumer typecheck for built package declarations.
-- lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and pre-push (tests, hygiene); CI runs the full matrix on node 22.19/24/26 plus a demo smoke test driving the echo-agent end to end.
+- lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and pre-push (tests, hygiene); CI runs the full matrix on node 22.19/24/26 plus built application smokes for the Headless, TUI, ACP, JSON-RPC, workflow, and code-runtime entry paths.
## Consequences
@@ -23,4 +23,4 @@ Every AGENTS.md promise gets a command that exits non-zero, wired into git hooks
- The gates themselves are code to maintain; config changes are reviewed like any change.
- 100%-coverage pressure can produce assertion-free tests — mutation testing is the planned counterweight (see [the mutation-testing proposal](../../proposed/testing/2026-06-11-mutation-testing.md)).
-
+
diff --git a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.md b/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md
similarity index 78%
rename from docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.md
rename to .agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md
index 1e63cef940..4075d4738a 100644
--- a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.md
+++ b/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md
@@ -1,4 +1,4 @@
-# RFC: tsdown for JS bundling instead of dumble
+# Agent Note: tsdown for JS bundling instead of dumble
Status: implemented
@@ -13,7 +13,7 @@ Build output currently matters only for `pnpm run build` + publint (nothing publ
Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-backed, actively released):
- Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*/*']` (explicit globs keep bundling to vendored Cordis and the TypeScript package tree; `workspace: true` would also discover example manifests and non-bundled workspace members).
-- Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build RFC](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler.
+- Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build Agent Note](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler.
- Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape).
- `scripts/build.ts` deleted; `pnpm run build` = `tsc -b tsconfig.build.json && tsdown`.
@@ -25,4 +25,4 @@ Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-b
## Consequences
-Runtime bundle outputs still follow the dumble-era public entry shape (`lib/index.js`, plus package-specific variants such as `schemastery`'s `lib/index.mjs`/`lib/index.cjs` and `logger-console`'s `lib/browser.js`); declarations now live under `lib/types` per the [TSC-first build RFC](2026-06-17-ts-build-config.md). Externals still come from each package's dependencies/peerDependencies. We give up dumble's exports-field inference — new packages with non-default shapes need a per-package `tsdown.config.ts` instead of just package.json fields. Future option: tsdown could also absorb declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the bottleneck; that would be a new RFC.
+Runtime bundle outputs still follow the dumble-era public entry shape (`lib/index.js`, plus package-specific variants such as `schemastery`'s `lib/index.mjs`/`lib/index.cjs` and `logger-console`'s `lib/browser.js`); declarations now live under `lib/types` per the [TSC-first build Agent Note](2026-06-17-ts-build-config.md). Externals still come from each package's dependencies/peerDependencies. We give up dumble's exports-field inference — new packages with non-default shapes need a per-package `tsdown.config.ts` instead of just package.json fields. Future option: tsdown could also absorb declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the bottleneck; that would be a new Agent Note.
diff --git a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md b/.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md
similarity index 97%
rename from docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md
rename to .agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md
index 2aa24907d5..a8895ba5e8 100644
--- a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md
+++ b/.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md
@@ -1,4 +1,4 @@
-# RFC: Vendor Cordis as source, not npm dependencies
+# Agent Note: Vendor Cordis as source, not npm dependencies
Status: implemented
diff --git a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md b/.agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md
similarity index 96%
rename from docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md
rename to .agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md
index 7a574a3388..42eb4228b6 100644
--- a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md
+++ b/.agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md
@@ -1,4 +1,4 @@
-# RFC: pnpm as the package manager instead of Yarn 4
+# Agent Note: pnpm as the package manager instead of Yarn 4
Status: implemented
@@ -38,4 +38,4 @@ Performance (measured at migration time on the dev NFS filesystem; single-digit-
On a fast local disk pnpm's content-addressed store typically wins on cold/warm installs and, especially, on **disk footprint** across multiple checkouts (one global store hardlinked into every `node_modules` vs Yarn copying ~279 MB per worktree — some devs regularly keep ~10 or more worktrees for this repo). That dedup advantage did **not** show in the migration-time numbers above because the test store and `node_modules` sat on different filesystems, defeating hardlinks; on a single-filesystem dev box or CI cache it applies. The honest summary: install speed on our NFS dev filesystem is a wash within noise; the move is justified by ecosystem alignment, phantom-dependency safety, and cross-checkout disk dedup — not by a raw install-time win.
-All quality gates (constraints, typecheck, lint, doc-sync, test:coverage at 100%, build, knip, publint, echo-agent demo smoke) pass unchanged on pnpm, which is the correctness proof that the linker swap introduced no phantom-dependency breakage.
+All quality gates (constraints, typecheck, lint, doc-sync, test:coverage at 100%, build, knip, publint, and built application smokes) pass on pnpm, which is the correctness proof that the linker swap introduces no phantom-dependency breakage.
diff --git a/docs/rfc/implemented/process/2026-06-17-ts-build-config.md b/.agents/notes/implemented/process/2026-06-17-ts-build-config.md
similarity index 99%
rename from docs/rfc/implemented/process/2026-06-17-ts-build-config.md
rename to .agents/notes/implemented/process/2026-06-17-ts-build-config.md
index 0687df250c..8f67b6fc2f 100644
--- a/docs/rfc/implemented/process/2026-06-17-ts-build-config.md
+++ b/.agents/notes/implemented/process/2026-06-17-ts-build-config.md
@@ -1,4 +1,4 @@
-# RFC: TSC-first build and one tsconfig
+# Agent Note: TSC-first build and one tsconfig
Status: implemented
diff --git a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md b/.agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md
similarity index 81%
rename from docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md
rename to .agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md
index db98c2fb7d..05dfed2453 100644
--- a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md
+++ b/.agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md
@@ -1,4 +1,4 @@
-# RFC: Markdown cross-link validity linting
+# Agent Note: Markdown cross-link validity linting
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
Docs in this repo link to each other by relative path — `[topic](../implemented/2026-…-….md)`, `[the cookbook](adding-a-tool.md)`, `[architecture.md](../../architecture.md)`. Nothing verified those targets exist. A rename or a move silently breaks every inbound link, and the break is invisible until a reader clicks it. [Doc-sync enforcement](2026-06-11-doc-sync-enforcement.md) already mechanized two classes of doc drift (uncompilable code blocks, a stale event-taxonomy table) and [verify-md-wrap](2026-06-11-doc-sync-enforcement.md) a third (hard-wrapped prose) — but a dead cross-link is a fourth, equally mechanical class that was still verified by eyeball.
-The motivating case is the RFC tree reorganization that introduced this gate: unifying `docs/adr/` + `docs/rfc/` into one `docs/rfc/` with `proposed/`/`implemented/`/`rejected/` subfolders renamed roughly forty inter-doc links by hand. A single fat-fingered path would have shipped a broken link with nothing to catch it.
+The motivating case is the Agent Note tree reorganization that introduced this gate: unifying `docs/adr/` + `.agents/notes/` into one `.agents/notes/` with `proposed/`/`implemented/`/`rejected/` subfolders renamed roughly forty inter-doc links by hand. A single fat-fingered path would have shipped a broken link with nothing to catch it.
## Decision
@@ -26,6 +26,6 @@ This gate checks *existence*, not anchor validity: a link to a real file with a
## Consequences
-- Renames and moves that orphan a cross-link now fail the pre-push hook and CI instead of waiting for a reader to click a dead link. This made the RFC reorganization that introduced the gate self-verifying: the same PR that rewrote forty links also added the check that proves none dangle.
+- Renames and moves that orphan a cross-link now fail the pre-push hook and CI instead of waiting for a reader to click a dead link. This made the Agent Note reorganization that introduced the gate self-verifying: the same PR that rewrote forty links also added the check that proves none dangle.
- One more fast tsx script in the `doc-sync` chain; no new dependency (the mdast/GFM stack is already in devDependencies for `verify-md-wrap`).
-- The convention this enforces — cross-reference docs by machine-checkable relative link, never by bare prose or a number — is documented in [docs/AGENTS.md](../../../AGENTS.md) so authors know the gate exists and why.
+- The convention this enforces — cross-reference docs by machine-checkable relative link, never by bare prose or a number — is documented in [docs/AGENTS.md](../../../../docs/AGENTS.md) so authors know the gate exists and why.
diff --git a/.agents/notes/implemented/process/2026-06-20-agent-note-classification.md b/.agents/notes/implemented/process/2026-06-20-agent-note-classification.md
new file mode 100644
index 0000000000..42c4523e0c
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-06-20-agent-note-classification.md
@@ -0,0 +1,46 @@
+# Agent Note: Classify Agent Notes by kind via path-encoded subdirectories
+
+Status: implemented
+
+## Problem
+
+A lifecycle-only Agent Note tree — `proposed/` / `implemented/` / `rejected/` — does not record what *kind* of decision each file contains. A reader browsing one lifecycle cannot distinguish a new capability from a removal or a tooling-policy change without opening each file.
+
+The repo's standing bias is [mechanical quality gates over prose guidelines](2026-06-11-quality-gates.md): a convention that isn't machine-checked rots. So a classification scheme here had to be enforceable, not an honor-system header.
+
+## Decision
+
+Add a second axis — the Agent Note's **class** — and encode it in the path: `{lifecycle}/{class}/yyyy-mm-dd-topic.md`. The folder *is* the label. A file's location declares its class, the closed set is "these folders and no others," and the existing [verify-md-links](2026-06-18-markdown-cross-link-lint.md) gate already protects the path rewrites the move required.
+
+### The closed set of six classes
+
+| Class | Covers |
+|---|---|
+| `feature` | A new user- or model-facing capability. |
+| `bug-fix` | Corrects a defect or closes a gap a postmortem surfaced. |
+| `simplification` | Removes code, behavior, or surface area without adding a capability. |
+| `architecture` | A structural decision about the **shipped source** — how packages relate, what the runtime vocabulary is. |
+| `process` | Tooling, policy, or workflow **around** the code, not runtime behavior. |
+| `testing` | Test infrastructure and strategy. |
+
+The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. This Agent Note is itself a `process` decision — it changes how the repo is organized and gated, not what the harness does at runtime — so it lives under `implemented/process/`.
+
+### Two gates
+
+Both are `doc-sync` members, in the `verify-md-wrap` style (tsx ESM, verify-don't-generate, exit non-zero on the first violation):
+
+- **`scripts/verify-agent-note-classification.ts`** — the closed lifecycle and class sets. It asserts every file under a lifecycle folder lives in a class folder from the canonical set (a loose `.md` at a lifecycle root, or an unknown class folder, fails) and rejects a centralized `INDEX.md`. The canonical sets live in `scripts/agent-note-tree.ts`, and [the README](../../README.md) documents each class in prose.
+- **`scripts/verify-doc-refs.ts`** — source comments that cite docs. Agent Note paths are referenced not only from Markdown but from TypeScript doc comments (root-relative prose like `.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md`). `verify-md-links` does not see those, so a reorganization could silently orphan them. This gate scans repo-authored `.ts` under `packages/**` and `examples/**` (excluding built `lib/` and `vendor/`) for `docs/….md` and `.agents/notes/….md` tokens, resolves each root-relative path, and asserts it exists. It requires the `.md` extension so extensionless prose is left alone.
+
+## Alternatives considered
+
+- **A `Classification:` prose line** in each file (next to `Status:`), parsed by the gate. Workable, but it duplicates into the file a fact the path can already carry, and a line can disagree with its folder. Path-encoding makes the label and its storage the same thing — there is nothing to keep in sync.
+- **A `refactor` class.** It overlaps `simplification` almost entirely; the only discriminator anyone reached for was "does observable behavior change?", which `simplification` already encodes (it does not). One class, not two.
+- **A generated or hand-maintained corpus index.** Rejected because the lifecycle/class tree is authoritative, while a centralized inventory creates a merge hotspot without providing discovery that tree navigation or repository search cannot provide. The separate [index proposal](../../rejected/process/2026-07-04-generate-agent-note-index-tables.md) records the discarded generated shape.
+
+## Consequences
+
+- Every Agent Note sits under a class folder. A reader can browse one folder to see all simplifications or all testing decisions within a lifecycle.
+- Two more fast tsx scripts in the `doc-sync` chain; no new dependency (the mdast/GFM stack was already present for `verify-md-wrap`/`verify-md-links`).
+- Adding a class is a deliberate act: amend the `const` in `scripts/agent-note-tree.ts` and the [Classification section](../../README.md#classification), not just `mkdir` a folder. The gate rejects an unknown folder, so an ad-hoc class can't slip in.
+- Source-comment doc references are now gated too — a moved or renamed doc that a `.ts` comment cites fails the pre-push hook, closing a drift class `verify-md-links` structurally could not see.
diff --git a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.md b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md
similarity index 62%
rename from docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.md
rename to .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md
index aaac6fbd3c..21b86b1812 100644
--- a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.md
+++ b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md
@@ -1,16 +1,16 @@
-# RFC: Core-data-structures catalog and the `ts type-equiv` drift gate
+# Agent Note: Core-data-structures catalog and the `ts type-equiv` drift gate
Status: implemented
## Problem
-A reader trying to understand the harness could find its *behavior* in [architecture.md](../../../architecture.md) (the service map, the session/turn/step lifecycle, the event taxonomy) but had no single place describing its *vocabulary* — the data structures that behavior moves around. The type shapes lived only in source, scattered across `packages/*/src/types.ts`, so understanding "what is a `Message`, a `SessionEvent`, a `StreamChunk`" meant reading the declarations directly. A prose catalog would help, but a catalog that paraphrases or paste-copies type definitions rots the instant a field changes — and an out-of-sync type doc is worse than none, because a reader trusts it.
+A reader trying to understand the harness could find its *behavior* in [architecture.md](../../../../docs/architecture.md) (the service map, the session/turn/step lifecycle, the event taxonomy) but had no single place describing its *vocabulary* — the data structures that behavior moves around. The type shapes lived only in source, scattered across `packages/*/src/types.ts`, so understanding "what is a `Message`, a `SessionEvent`, a `StreamChunk`" meant reading the declarations directly. A prose catalog would help, but a catalog that paraphrases or paste-copies type definitions rots the instant a field changes — and an out-of-sync type doc is worse than none, because a reader trusts it.
-So the work had two intertwined questions: **what belongs in such a catalog** (the scoping problem — a harness has dozens of cross-package types and dumping all of them helps no one), and **how to keep pasted type definitions from drifting** (the durability problem). This RFC records both decisions. Its sibling, [the generated cordis events + services catalog](2026-06-20-generated-cordis-catalog.md), is the *wiring*-axis complement: this one catalogs the data structures, that one the events and services that move them.
+So the work had two intertwined questions: **what belongs in such a catalog** (the scoping problem — a harness has dozens of cross-package types and dumping all of them helps no one), and **how to keep pasted type definitions from drifting** (the durability problem). This Agent Note records both decisions. Its sibling, [the generated cordis events + services catalog](2026-06-20-generated-cordis-catalog.md), is the *wiring*-axis complement: this one catalogs the data structures, that one the events and services that move them.
## Decision
-A new `docs/core-data-structures/` folder catalogs the vocabulary, with a new `verify-type-equiv` doc-sync gate that keeps every pasted type definition byte-identical to its source.
+A new `docs/core-data-structures/` folder catalogs the vocabulary, with a new `verify-type-equiv` doc-sync gate that keeps every pasted type declaration and its JSDoc synchronized with source.
### What counts as "core" — the spine-vs-seam line
@@ -27,10 +27,10 @@ The rule that settled the remaining cases: ***the type you write, hold, or recei
### The `ts type-equiv` mechanism — literal AND drift-proof
-The durability requirement was specific: the doc should show the **literal** current type definition (so a reader sees the real shape, not a paraphrase) **and** be mechanically guaranteed to match source. The repo already compiles fenced ` ```ts ` blocks (`doc-typecheck`), but a real typechecked block needs import noise and proves only *assignability*, not *byte-equality* — a renamed field with the same type would pass. So:
+The durability requirement was specific: the doc shows the **literal** current type declaration and original JSDoc (so a reader sees the real shape and source contract, not a paraphrase) **and** is mechanically guaranteed to match source. The repo already compiles fenced ` ```ts ` blocks (`doc-typecheck`), but a real typechecked block needs import noise and proves only *assignability* — a renamed field or changed JSDoc can pass. So:
-- Type definitions are pasted verbatim into a dedicated ` ```ts type-equiv ` fence. `doc-typecheck` recognizes the fence and skips it (a bare definition is not standalone-compilable), and **excludes it from the opt-out ratio** — it is a separately-checked category, not an unchecked sketch.
-- A new `scripts/verify-type-equiv.ts` extracts each block via the TypeScript parser and asserts a **verbatim source match** against the declared symbol — chosen over a compiled `_Check` assertion precisely because byte-equality, not assignability, is the property we want.
+- Complete type declarations and their JSDoc are pasted verbatim into a dedicated ` ```ts type-equiv ` fence. A concise ` ```ts public-api ` fence carries the source-equivalent ambient projection for a class whose implementation bodies do not belong in the catalog. `doc-typecheck` recognizes both and skips them (the bare declarations are not standalone-compilable), and **excludes them from the opt-out ratio** — they are a separately-checked category, not unchecked sketches.
+- A new `scripts/verify-type-equiv.ts` extracts each block via the TypeScript parser and asserts that its declaration structure and every JSDoc comment match the declared symbol, ignoring only formatting whitespace and non-JSDoc comments. Ordinary blocks retain the complete declaration. A `public-api` projection retains a class's public fields, constructor, accessors, and methods with their original JSDoc while removing implementation bodies and private or protected members. This is chosen over a compiled `_Check` assertion because source names and documentation identity, not assignability, are the properties the catalog preserves.
- Provenance lives in a central `scripts/type-equiv.manifest.json` (`{ doc, symbol, source }` entries), **not** in directive comments in the prose. The script enforces a **1:1 correspondence**: every type-equiv block has exactly one manifest entry and vice versa, so a block can never be silently unchecked and an entry can never rot.
- Wired into `doc-sync`, so it runs in the same lefthook pre-push and CI paths as the other doc gates.
@@ -41,18 +41,18 @@ The durability requirement was specific: the doc should show the **literal** cur
## Alternatives considered
- **A flat dump of all cross-package vocabulary** — the `BashExecRequest` test case killed it: if seam vocabulary is "core", the catalog helps no one; the tiered spine-vs-seam structure won.
-- **A compiled `_Check` assignability assertion** instead of the verbatim source match — rejected because byte-equality, not assignability, is the property we want: a renamed field with the same type would pass assignability.
+- **A compiled `_Check` assignability assertion** instead of the source match — rejected because assignability does not preserve names or JSDoc: a renamed field with the same type or a changed contract comment would pass.
- **Provenance as directive comments in the prose** — rejected for the central manifest, whose enforced 1:1 correspondence means a block can never be silently unchecked and an entry can never rot.
## Verification lesson
The spine-vs-seam rule was tested against `BashExecRequest`, tool schemas and definitions, the schema DSL, presentation types, and the session/persistence split before adoption.
-`verify-type-equiv` must scan the complete Markdown scope, not only manifest-named documents. Otherwise an unmanifested `type-equiv` block escapes the claimed one-to-one check. The gate therefore reports such blocks as orphans. This RFC records that fail-closed scan rule together with the spine-vs-seam and verbatim-match decisions; the generated Cordis catalog has the symmetric design record in [its RFC](2026-06-20-generated-cordis-catalog.md).
+`verify-type-equiv` must scan the complete Markdown scope, not only manifest-named documents. Otherwise an unmanifested `type-equiv` block escapes the claimed one-to-one check. The gate therefore reports such blocks as orphans. This Agent Note records that fail-closed scan rule together with the spine-vs-seam and verbatim-match decisions; the generated Cordis catalog has the symmetric design record in [its Agent Note](2026-06-20-generated-cordis-catalog.md).
## Consequences
-- The vocabulary now has a single home that **cannot silently drift**: a field rename in source fails `verify-type-equiv` in the pre-push hook and CI until the paste is refreshed.
+- The vocabulary now has a single home that **cannot silently drift**: a field or public class-member change in source fails `verify-type-equiv` in the pre-push hook and CI until the paste is refreshed. Cordis service methods remain owned by the generated services catalog rather than being duplicated here.
- The spine-vs-seam line is a reusable scoping tool, not a one-off: the same "the thing you write/hold/receive is core; the machinery that types/renders/persists it is a detail" rule is what later scoped the events/services catalog's harness-vs-inherited tiering.
- The `ts type-equiv` fence is a third doc-block category alongside ` ```ts ` (compiled) and ` ```ts ignore-check ` (sketch). A later sibling added a fourth, ` ```ts cordis-catalog ` (generated signature), reusing the same skip-and-exclude treatment.
- Adding or reshaping a core type now carries a documentation obligation the author must honor (the gate cannot detect a missing *new* type), backstopped by the `dsh-code-review` checklist.
diff --git a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md b/.agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md
similarity index 65%
rename from docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md
rename to .agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md
index eafce4feae..7de7056b33 100644
--- a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md
+++ b/.agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md
@@ -1,4 +1,4 @@
-# RFC: Generated cordis events + services catalog
+# Agent Note: Generated cordis events + services catalog
Status: implemented
@@ -6,13 +6,13 @@ Status: implemented
A plugin author needs two reference surfaces that no single document gave them: every cordis **event** they can listen to (with its exact signature and dispatch mode) and every `ctx.` **service** they can call (with its exact interface). The pieces existed but were scattered — a hand-maintained event-taxonomy *table* in `docs/architecture.md` (names + prose Mode/Purpose, name-set-checked by `verify-event-taxonomy`), a Service-map table (8 rows of role prose), and the `interface Events` / `interface Context` declarations themselves. The taxonomy table also could not catch a brand-new *undocumented* event: a name-set verifier only checks the names that are already in the table on both sides.
-This is the wiring-axis complement to the [core-data-structures catalog](../../../core-data-structures/core.md) ([its RFC](2026-06-20-core-data-structures-catalog.md)): that one catalogs the *data structures* the loop moves around (verified hand-pastes); this one catalogs the *events and services* that move them.
+This is the wiring-axis complement to the [core-data-structures catalog](../../../../docs/core-data-structures/core.md) ([its Agent Note](2026-06-20-core-data-structures-catalog.md)): that one catalogs the *data structures* the loop moves around (verified hand-pastes); this one catalogs the *events and services* that move them.
## Decision
Generate the catalog from source instead of hand-maintaining a table and verifying a subset.
-`scripts/gen-cordis-catalog.ts` uses the TypeScript compiler API to emit separate event and service references from declarations and source JSDoc. Events include dispatch modes; services include public signatures. Deterministic `--write` and `--check` modes make both pages generated artifacts, with freshness enforced by `doc-sync`.
+`scripts/gen-cordis-catalog.ts` uses the TypeScript compiler API to emit separate event and service references from declarations and source JSDoc. Events include dispatch modes and their original member JSDoc; services include public signatures with each method's original JSDoc. Deterministic `--write` and `--check` modes make both pages generated artifacts, with freshness enforced by `doc-sync`.
Pure generation is correct here because the codebase is disciplined enough that the AST is the whole truth: every event/service name is a string literal that round-trips to a static declaration — there are no dynamically-named events and no runtime-only services. So a generated doc cannot be wrong, and it closes the undocumented-event gap structurally (generation enumerates source rather than checking a hand-written subset).
@@ -20,8 +20,8 @@ Specific choices:
- **`@mode` tag, cross-checked.** Each harness event's JSDoc carries an explicit `@mode emit|waterfall|parallel|serial` tag; the generator hard-errors on a missing tag. Where the signature shape is conclusive — a trailing `next: () => …` parameter is structurally a waterfall — it asserts the tag agrees and hard-errors on a contradiction. The emit/parallel/serial distinction is not structurally visible (`session/flush` returns `Promise | void` with no `next`, as does the ordered `agent/pre-step` checkpoint), so it is trusted from the tag. The authoring rule lives in [AGENTS.md](../../../../AGENTS.md).
- **Tiered scope.** The harness tier (the 8 `@deepseek-ai/dsh-*` services + their events) is rendered in full from source. The inherited tier (cordis-core `ctx.on/emit/effect/provide/…` + the `internal/*` events + loader/hmr/timer) is pinned vendor source a plugin also sees; it is rendered tersely (name + one-line + source pointer) from a curated table in the generator, NOT walked from the vendor AST — the cordis-core `Context` mixes true ctx members with non-service fields (`root`, `baseUrl`, `logger`), and the vendor surface changes only on a deliberate vendor sync.
-- **Cross-links to the data-structure catalog.** A type name in a signature (`GenerateOptions`, `StreamChunk`, `ToolDefinition`, …) links to the core-data-structures page that documents it. The map is a small hand-curated const in the generator — NOT `type-equiv.manifest.json`, which documents the `…Map` symbols while signatures reference the derived union names, and lists a few symbols on two pages.
-- **A dedicated fence.** Signature blocks use a ` ```ts cordis-catalog ` info string that `doc-typecheck` recognizes and skips (a bare signature fragment is not standalone-compilable), excluded from the opt-out ratio — the same treatment `type-equiv` blocks get.
+- **Cross-links to the data-structure catalog.** Every repository-owned type name in a signature (`GenerateOptions`, `StreamChunk`, `ToolDefinition`, …) links to its primary core-data-structures page through a curated map. The AST walk is fail-closed: each parameter, generic constraint/default, and return-type reference must be mapped, be the signature's own type parameter, be a named TypeScript/Cordis foundation type, or carry a named exception with its non-catalog documentation owner. Violations aggregate with source pointers and name the appropriate owning lists. The map does NOT reuse `type-equiv.manifest.json`, which documents `…Map` symbols while signatures reference derived union names and lists some symbols on multiple pages.
+- **A dedicated fence.** Signature blocks use a ` ```ts cordis-catalog ` info string and place the original event or public-method JSDoc immediately before its declaration. `doc-typecheck` recognizes and skips the bare fragments, excluding them from the opt-out ratio — the same treatment `type-equiv` blocks get.
This **supersedes the event-taxonomy half** of [doc-sync enforcement](2026-06-11-doc-sync-enforcement.md): `verify-event-taxonomy` and its `docs/architecture.md` table are retired (the architecture.md heading stays, its body now points at the catalog; the Service-map role table stays as curated prose). doc-typecheck, verify-md-wrap, verify-md-links, and verify-type-equiv are unchanged.
@@ -29,11 +29,11 @@ This **supersedes the event-taxonomy half** of [doc-sync enforcement](2026-06-11
- **Verify-don't-generate, as the retired taxonomy check did** — reversed *for this surface only*: the data here is mechanically complete, so generation is strictly stronger (full signatures, cannot drift, catches undocumented events) than a name-set check of a hand-maintained table.
- **Walking the vendor AST for the inherited tier** — rejected for the curated table: the cordis-core `Context` mixes true ctx members with non-service fields, and the pinned vendor surface changes only on a deliberate sync.
-- **Reusing `type-equiv.manifest.json` as the signature cross-link map** — rejected for a small hand-curated const: the manifest documents the `…Map` symbols while signatures reference the derived union names, and it lists a few symbols on two pages.
+- **Reusing `type-equiv.manifest.json` as the signature cross-link map** — rejected for a complete curated const plus fail-closed coverage: the manifest documents `…Map` symbols while signatures reference derived union names, and it lists some symbols on multiple pages. The explicit map makes each rendered destination and each non-catalog exception a reviewable decision.
## Consequences
-- The catalog cannot drift: a source change that the committed file doesn't reflect fails `verify-cordis-catalog` in the pre-push hook and CI. A new event with no `@mode` tag, or a tag that contradicts its signature, fails the generator outright.
-- Event prose now has a single home — the JSDoc at the declaration. Thin JSDoc yields a thin catalog entry, which pressures authors to document at the source (the generator is a forcing function for the AGENTS.md "every export has a semantic JSDoc" rule).
+- The catalog cannot drift: a source change that the committed file doesn't reflect fails `verify-cordis-catalog` in the pre-push hook and CI. A new event with no `@mode` tag, a tag that contradicts its signature, or an unclassified signature type fails the generator outright.
+- Event and service-method contracts have a single home — the JSDoc at the declaration. The catalog repeats that original JSDoc inside its generated signature block and uses its description portion as entry prose, so thin source documentation yields a thin catalog entry.
- The inherited tier is hand-summarized, so a vendor sync that adds/renames a cordis-core event or `ctx` member needs a matching edit to the curated table in `gen-cordis-catalog.ts`. This is the deliberate cost of not walking pinned vendor source; it changes rarely and is called out in the generator.
- `verify-event-taxonomy.ts` is deleted and the `docs/architecture.md` event table is gone; anyone who linked to a specific table row now lands on the generated catalog instead.
diff --git a/docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml
similarity index 61%
rename from docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml
rename to .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml
index 5f83acc3aa..a1d901ad15 100644
--- a/docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-02-bilingual-docs-and-pairing-gate.md: 68c0f3bbc0472b0c96f9d64fc6b1b24ac7008795
-2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 2cf8f9b9c17d8a521d8674833e909b34f315cfe0
+2026-07-02-bilingual-docs-and-pairing-gate.md: 3be1d5d8fd9dba20cfca34c79cb01d89fad8097a
+2026-07-02-bilingual-docs-and-pairing-gate.zh.md: a8aa8812934e755fe0175c8f3f20d194e4d24b4a
diff --git a/docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md
similarity index 82%
rename from docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md
rename to .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md
index 68c0f3bbc0..3be1d5d8fd 100644
--- a/docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md
+++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md
@@ -1,4 +1,4 @@
-# RFC: Bilingual documentation via paired sibling files and a pairing gate
+# Agent Note: Bilingual documentation via paired sibling files and a pairing gate
Status: implemented
@@ -10,14 +10,14 @@ This repo's README and docs tree are read by people and agents inside and outsid
## Decision
-- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../i18n/terminology.md).
+- **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md).
- **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR.
- **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical), excluded (generated or bilingual-by-construction) files stay unpaired, and date-named documents on or after the manifest's `requiredSince` cutoff have complete pairs. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows.
-- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth.
+- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. The skill directs the orchestrating agent to delegate translation writing to a subagent.
## Alternatives considered
-- **English as the canonical source with a fingerprint inside the translation** — the design first proposed for this RFC: `.zh.md` files carried an HTML comment recording the English source's blob hash, and translation flowed EN → ZH only. Revised in review: the team wants Chinese-first authoring (write and review a Chinese RFC, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged.
+- **English as the canonical source with a fingerprint inside the translation** — the design first proposed for this Agent Note: `.zh.md` files carried an HTML comment recording the English source's blob hash, and translation flowed EN → ZH only. Revised in review: the team wants Chinese-first authoring (write and review a Chinese Agent Note, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged.
- **Locale directories (`docs/en/` + `docs/zh/`, the Kubernetes/ECharts model)** — rejected: this repo has no docs-site framework to map locales to routes, moving every English file would churn every existing cross-reference, and `verify-md-links`/`verify-doc-refs` would need path-mapping logic instead of working unchanged.
- **A separate translation repo (the PingCAP `docs`/`docs-cn` model)** — rejected: right for a docs product with independent release trains, overkill for a monorepo's own documentation; it also puts the translation outside the reach of this repo's gates.
- **Interleaved bilingual files (single file, both languages)** — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial inconsistency invisible.
@@ -34,5 +34,5 @@ Paired sibling files with locale suffixes are the dominant Chinese big-tech conv
- Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, "who confirmed these consistent, and when" is answerable from git blame on the yaml.
- When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring.
- Generated docs (`cordis-catalog/`, `tool-catalog/`, `module-graph.md`) are excluded for now; the planned follow-up is to teach their generators to emit Chinese alongside English, at which point they leave the exclusion list.
-- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. A date-named document dated on or after the manifest's `requiredSince` cutoff merges bilingual or not at all, so new date-named RFCs do not enlarge that backlog.
+- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. A date-named document dated on or after the manifest's `requiredSince` cutoff merges bilingual or not at all, so new date-named Agent Notes do not enlarge that backlog.
- The recorded hashes double as the update tool (`git cat-file -p ` recovers either side's last-confirmed text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism.
diff --git a/docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md
similarity index 83%
rename from docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md
rename to .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md
index 2cf8f9b9c1..a8aa881293 100644
--- a/docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md
+++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md
@@ -1,4 +1,4 @@
-# RFC:通过配对兄弟文件与配对门禁实现双语文档
+# Agent Note:通过配对兄弟文件与配对门禁实现双语文档
Status: implemented
@@ -10,14 +10,14 @@ Status: implemented
## 决策
-- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../i18n/terminology.md)。
+- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。
- **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。
- **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:required 的配对必须存在;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)不得配对;凡文件名以日期开头且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,也必须有完整配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单只进不退:每个合并的翻译批次将自己的文件加入其中,覆盖面只增不减。
-- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。
+- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。该 skill 要求编排 agent 把翻译写作委派给 subagent。
## 曾考虑的替代方案
-- **英文为正典源、指纹放在译文内**:本 RFC 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 RFC,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。
+- **英文为正典源、指纹放在译文内**:本 Agent Note 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 Agent Note,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。
- **语言目录(`docs/en/` + `docs/zh/`,Kubernetes/ECharts 模式)**:否决。本仓库没有将 locale 映射到路由的文档站框架;如果移动所有英文文件,所有既有交叉引用都要随之修改;且 `verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑,而非原样工作。
- **独立翻译仓库(PingCAP `docs`/`docs-cn` 模式)**:否决。适合有独立发布节奏的文档产品,对 monorepo 自身的文档而言过重;还会把译文置于本仓库门禁触及不到的地方。
- **中英混排单文件(一个文件、两种语言)**:否决。每个 diff 都翻倍,破坏一段一行约定的 diff 易读性,且局部不一致不可见。
@@ -34,5 +34,5 @@ Status: implemented
- 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。
- 两侧说法冲突时,没有机械规则裁决谁赢,由 PR 评审裁决。这是同权的代价,且是有意接受的:另一个选项(正典语言)会禁止中文先行撰写。
- 生成文档(`cordis-catalog/`、`tool-catalog/`、`module-graph.md`)暂被排除;计划中的后续工作是让生成器在输出英文的同时输出中文,届时将这些文件移出排除清单。
-- 推进天然是渐进的:`required` 之外的文档是可见的 backlog(待翻清单,`--list`),而非红色的 CI;因此配对按可评审的批次落地,无需一个巨型 PR。凡文件名以日期开头且日期不早于 manifest 中 `requiredSince` 分界日期的文档,都必须配齐双语文件,因此新建的日期命名 RFC 不会增加这份 backlog。
+- 推进天然是渐进的:`required` 之外的文档是可见的 backlog(待翻清单,`--list`),而非红色的 CI;因此配对按可评审的批次落地,无需一个巨型 PR。凡文件名以日期开头且日期不早于 manifest 中 `requiredSince` 分界日期的文档,都必须配齐双语文件,因此新建的日期命名 Agent Note 不会增加这份 backlog。
- 记录的 hash 兼作更新工具(`git cat-file -p ` 能还原任一侧上次确认的文本,用于基于 diff 的最小更新),因此这套机制从不强迫整篇重译。
diff --git a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md
similarity index 96%
rename from docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md
rename to .agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md
index 7da1a05a3a..06ab61732c 100644
--- a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md
+++ b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md
@@ -1,4 +1,4 @@
-# RFC: Generated tool-schema catalog (boot-and-harvest)
+# Agent Note: Generated tool-schema catalog (boot-and-harvest)
Status: implemented
@@ -19,7 +19,7 @@ The cordis catalog is a pure TypeScript-AST pass because every event/service nam
- `tool-subagent`'s tool name is `config.toolName ?? 'subagent'` — chosen at load, not a literal.
- An MCP plugin can register **raw JSON Schema** directly via `ctx.tools.register()` without `defineTool` at all, so enumerating `defineTool(` call sites structurally under-counts.
-The only faithful source of truth is the schema the registry actually holds after the plugin loads. Booting is the [testing-policy discipline](../../../testing.md) "verify the world, not the self-report" applied to a doc generator: read the shipped artifact, not a re-derivation of it.
+The only faithful source of truth is the schema the registry actually holds after the plugin loads. Booting is the [testing-policy discipline](../../../../docs/testing.md) "verify the world, not the self-report" applied to a doc generator: read the shipped artifact, not a re-derivation of it.
### Restoring "nothing silently omitted"
diff --git a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md b/.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md
similarity index 64%
rename from docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md
rename to .agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md
index a17973ea2e..7969f0e80c 100644
--- a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md
+++ b/.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md
@@ -1,10 +1,10 @@
-# RFC: Documentation graph index for maintainers and SDK users
+# Agent Note: Documentation graph index for maintainers and SDK users
Status: implemented
## Problem
-The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../module-graph.md) is generated from package `peerDependencies`, the generated [Cordis events](../../../cordis-catalog/events.md) and [services](../../../cordis-catalog/services.md) catalogs are generated from Cordis `Events` and `Context` declarations, [tool-catalog.md](../../../tool-catalog.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source.
+The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../../docs/module-graph.md) is generated from package `peerDependencies`, the generated [Cordis events](../../../../docs/cordis-catalog/events.md) and [services](../../../../docs/cordis-catalog/services.md) catalogs are generated from Cordis `Events` and `Context` declarations, [tool-catalog.md](../../../../docs/tool-catalog.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../../docs/core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source.
Those references are accurate, but they are mostly catalogs. A maintainer still has to synthesize the relationships: which packages form a capability seam, which app bundles a concrete spine, which event is durable vs live, where a hook or policy plugin can intercept work, and which model-facing tool depends on which service. An SDK user has the same problem from another angle: "Which package do I install or load for the behavior I want, and which event/service/tool do I extend?"
@@ -12,7 +12,7 @@ The hooks subsystem makes event producer/consumer topology and interception poin
## Decision
-Add generated relationship graph docs, indexed at [docs/graph-atlas.md](../../../graph-atlas.md), produced by focused generators and verified by `pnpm run verify-doc-graphs` / existing catalog freshness checks as part of `doc-sync`.
+Add generated relationship graph docs, indexed at [docs/graph-atlas.md](../../../../docs/graph-atlas.md), produced by focused generators and verified by `pnpm run verify-doc-graphs` / existing catalog freshness checks as part of `doc-sync`.
The index is a relationship layer above the existing catalogs. It does not replace exact references; instead, it links to them and explains how their pieces fit together.
@@ -26,19 +26,20 @@ Every graph page declares one maintenance mode:
### First shipped index
-The first index links ten relationship surfaces. Package topology and tool-package affordances live in the existing generated catalogs that already own those facts; the remaining focused diagrams are generated by `scripts/gen-doc-graphs.ts`.
+The index links eleven relationship surfaces. Package topology and tool-package affordances live in the existing generated catalogs that already own those facts; the remaining focused diagrams are generated by `scripts/gen-doc-graphs.ts`.
| Graph | Maintenance mode | Source of truth |
|---|---|---|
-| [module dependency graph](../../../module-graph.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths |
-| [tool schema catalog and package map](../../../tool-catalog.md) | generated | boot-harvested tool schemas plus tool-package service/effect metadata |
-| [capability seams and core services](../../../capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` |
-| [echo-agent app composition](../../../../examples/echo-agent/composition.md) | hybrid generated | `examples/echo-agent/cordis.yml` plugin list plus curated app/bundle expansion |
-| [coding-agent app composition](../../../../examples/coding-agent/composition.md) | hybrid generated | `examples/coding-agent/cordis.yml` plugin list plus curated app/bundle expansion |
+| [module dependency graph](../../../../docs/module-graph.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths |
+| [tool schema catalog and package map](../../../../docs/tool-catalog.md) | generated | boot-harvested tool schemas plus tool-package service/effect metadata |
+| [capability seams and core services](../../../../docs/capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` |
+| [tui-agent app composition](../../../../examples/tui-agent/composition.md) | hybrid generated | `examples/tui-agent/cordis.yml` plugin list plus curated app/bundle expansion |
+| [headless-agent app composition](../../../../examples/headless-agent/composition.md) | hybrid generated | `examples/headless-agent/cordis.yml` plugin list plus curated app/bundle expansion |
+| [cordis-agent app composition](../../../../examples/cordis-agent/composition.md) | hybrid generated | `examples/cordis-agent/cordis.yml` plugin list plus curated app/bundle expansion |
| [acp-agent app composition](../../../../examples/acp-agent/composition.md) | hybrid generated | `examples/acp-agent/cordis.yml` plugin list plus curated app/bundle expansion |
-| [event producer/consumer matrix](../../../event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides |
-| [agent turn and step lifecycle](../../../agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics |
-| [tool execution pipeline](../../../tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall |
+| [event producer/consumer matrix](../../../../docs/event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides |
+| [agent turn and step lifecycle](../../../../docs/agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics |
+| [tool execution pipeline](../../../../docs/tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall |
| [ACP snapshot replay](../../../../packages/ui/acp/snapshot-replay.md) | curated | snapshot harness behavior |
### Why generators own the docs
diff --git a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md b/.agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
similarity index 83%
rename from docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
rename to .agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
index 42ab306bb0..882ab8cf82 100644
--- a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
+++ b/.agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md
@@ -1,4 +1,4 @@
-# RFC: JSDoc completeness gate for the cordis surface
+# Agent Note: JSDoc completeness gate for the cordis surface
Status: implemented
@@ -20,14 +20,14 @@ The contract:
- **Explicitness the walk can check**: the gate is a pure-AST pass (no type checker), so a service method must annotate its return type (an inferred return cannot be classified) and surface parameters must be simple identifiers (a binding pattern has no name for `@param` to match).
- **Violations aggregate** into one error listing every offender — a remediation pass sees the whole list at once. The previously fail-fast `@mode` checks moved into the same aggregated report, with their message texts unchanged.
-The tags are **enforcement-only**: `parseJsDoc` now ends description prose at the first block tag (standard JSDoc semantics, which also stops multi-line tag descriptions from leaking into the catalog as prose), so `@param`/`@returns` never change the rendered catalog.
+The generator keeps two views of the same source comment: `parseJsDoc` ends entry prose at the first block tag, while the `ts cordis-catalog` signature block includes the original JSDoc with `@param`, `@returns`, and `@mode` intact. Readers therefore see the complete source contract without block-tag text leaking into the surrounding prose.
Negative-path tests in `packages/core/agent/tests/gen-cordis-catalog.spec.ts` drive `collectEvents`/`collectServices` against synthetic fixtures to prove each guard fires and that the exemptions hold. The authoring rule lives in the root [AGENTS.md](../../../../AGENTS.md) conventions bullet alongside the `@mode` rule.
## Alternatives considered
- **An ESLint rule** — cannot see the scope's machine definition (which `interface Events` members and which `ctx.` classes are the cordis surface); the catalog generator computes exactly that mapping on every run, so the gate lives there.
-- **Rendering the tags into the catalog** — restructuring the services section into per-method entries was considered and deliberately deferred: source JSDoc plus IDE hover is where method docs are consumed, and the catalog stays an index.
+- **Expanding every method into a separate prose section** — rejected: the catalog stays skimmable by keeping one service section and one signature block, while the JSDoc attached to each declaration preserves the full method contract in place.
- **An escape-hatch tag** — none exists; the surface is small and curated (12 services, 57 methods, 27 events at adoption), and the point is that the check cannot be waved off.
## Consequences
@@ -36,4 +36,4 @@ Negative-path tests in `packages/core/agent/tests/gen-cordis-catalog.spec.ts` dr
- The service surface must annotate return types explicitly and use identifier parameters. Neither constraint bound at adoption (every method already annotated; no destructured seam parameters existed); both are now load-bearing requirements a violating change will discover mechanically.
- The general AGENTS.md JSDoc rule ("one-liners when one line suffices") acquires a stricter carve-out on this surface: a one-line summary still suffices only when the method has no parameters and a void result.
- `@param` on `next` or `this` stays legal but unchecked — a deliberate asymmetry: the gate enforces the payload contract and refuses to demand boilerplate.
-- The rendered catalog is unchanged by the tags (prose stops at the first block tag). If method-level rendering is wanted later, that is a catalog-design decision to take separately, not a gap in this gate.
+- Each generated event or method fragment carries its original JSDoc, while the prose summary remains tag-free. Source edits therefore refresh both the readable index and the exact contract shown beside the signature.
diff --git a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md b/.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md
similarity index 74%
rename from docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md
rename to .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md
index 3f88e43c1d..68e055f0a2 100644
--- a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md
+++ b/.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md
@@ -1,17 +1,17 @@
-# RFC: Documentation tiers, budgets, and the ceiling gate
+# Agent Note: Documentation tiers, budgets, and the ceiling gate
Status: implemented
## Problem
-Standing docs accumulated repeated rules, retold incidents, duplicated package maps, and stale RFC summaries despite existing writing guidance. Because review alone did not prevent that growth, the repository needed a mechanical budget alongside its documentation taxonomy.
+Standing docs accumulated repeated rules, retold incidents, duplicated package maps, and stale Agent Note summaries despite existing writing guidance. Because review alone did not prevent that growth, the repository needed a mechanical budget alongside its documentation taxonomy.
## Decision
-- **A tier taxonomy with one home per fact.** [docs/AGENTS.md](../../../AGENTS.md) is the documentation standard: it assigns every Markdown tier a single job (standing orders, system map, type catalog, decision records, incident stories, how-tos, per-package contracts, generated catalogs, workflows), forbids restating a fact outside its home tier (link instead), and carries the slop checklist used when writing or reviewing any doc.
-- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, RFCs, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them.
+- **A tier taxonomy with one home per fact.** [docs/AGENTS.md](../../../../docs/AGENTS.md) is the documentation standard: it assigns every Markdown tier a single job (standing orders, system map, type catalog, decision records, incident stories, how-tos, per-package contracts, generated catalogs, workflows), forbids restating a fact outside its home tier (link instead), and carries the slop checklist used when writing or reviewing any doc.
+- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them.
- **Ceilings are an enforcement frontier that ratchets.** A ceiling sits at least 5% above the doc's current size — working headroom, so routine wording edits pass while real growth still trips the gate — and ratchets down, keeping that margin, as the doc is brought to its target budget (root `AGENTS.md` ≤ 1,500 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600; `packages/README.md` ≤ 600) — the same rollout mechanism as the [translation-pairing `required` list](2026-07-02-bilingual-docs-and-pairing-gate.md). When the gate goes red the fix is to relocate or condense per the taxonomy; raising a ceiling is permitted only with explicit justification in the PR description, the manifest diff being the reviewable act.
-- **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc-standards](../../../../.agents/skills/dsh-doc-standards/SKILL.md) carries the placement/audit/red-gate workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md) over the i18n contract.
+- **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc-standards](../../../skills/dsh-doc-standards/SKILL.md) carries the placement/audit/red-gate workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) over the i18n contract.
## Alternatives considered
diff --git a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md b/.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md
similarity index 58%
rename from docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md
rename to .agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md
index 0de6255322..bd16baf8f6 100644
--- a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md
+++ b/.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md
@@ -1,22 +1,22 @@
-# RFC: Generated persistence log event catalog
+# Agent Note: Generated persistence log event catalog
Status: implemented
## Problem
-`SessionEventMap` is the on-disk vocabulary, but its declarations are split across the owning session package and declaration merges. The generated persistence catalog is the single reference for every event and payload; hand-maintained tables drift and are removed. These records are not Cordis events—observers receive them through the single `session/event` bus event—so the Cordis catalog cannot cover them. The generator discovers all declarations and the doc-sync freshness gate rejects omissions or stale output.
+`SessionEventMap` is the on-disk vocabulary, but its declarations are split across the owning session package and declaration merges. The generated persistence catalog is the single reference for every event, its complete payload declaration and source JSDoc, and the shared `SessionEvent` envelope; hand-maintained tables drift and are removed. These records are not Cordis events—observers receive them through the single `session/event` bus event—so the Cordis catalog cannot cover them. The generator discovers all declarations and the doc-sync freshness gate rejects omissions or stale output.
## Decision
Generate `docs/persistence-catalog.md` from source, with a freshness gate, as the fourth reference surface: the *records* a persisted session log can contain, complementing the cordis catalog (wiring), core-data-structures (vocabulary), and the tool catalog (tools).
-`gen-persistence-catalog.ts` scans every owning and declaration-merged `SessionEventMap` with the TypeScript AST. It renders source JSDoc, payload type, derived surface badge, reference links, and source location. The doc-sync freshness check rejects a vocabulary change whose catalog was not regenerated.
+`gen-persistence-catalog.ts` scans every owning and declaration-merged `SessionEventMap` with the TypeScript AST. It renders each member from its leading JSDoc through the complete payload type, retaining nested property comments and removing only its containing indentation, and also pastes the owning `SessionEventType`, `SurfaceEventType`, `SurfaceOp`, and `SessionEvent` declarations that compose the persisted envelope. Derived surface badges, reference links, and source locations remain outside the declaration blocks. The doc-sync freshness check rejects a vocabulary or envelope change whose catalog was not regenerated.
Specific choices:
-- **JSDoc completeness, enforced.** Every member must carry description prose — the JSDoc becomes the catalog entry, the same forcing function the cordis catalog applies to bus events. An `@mode` tag on a member is a hard error: dispatch modes belong to cordis bus events, and a log event has none — the tag would misread as "this fires on the bus with mode X". Violations aggregate into one error listing every offender.
+- **JSDoc completeness, enforced.** Every member and rendered envelope type must carry description prose, and the full source JSDoc stays attached to its declaration in the catalog. An `@mode` tag is a hard error: dispatch modes belong to cordis bus events, and persisted records have none. Violations aggregate into one error listing every offender.
- **The surface badge is derived, not hand-listed.** `SurfaceEventType` — the subset that produces LLM messages and may carry `surfaceOp` — is parsed from its union declaration in the owning package; a union member naming no declared event is a hard error (a stale union member would otherwise silently badge nothing). Everything else renders **log-only**.
-- **A dedicated fence.** Payload blocks use a ` ```ts persistence-catalog ` info string that `doc-typecheck` recognizes and skips, excluded from the opt-out ratio — the same treatment as `ts cordis-catalog` (a bare payload fragment is not standalone-compilable).
+- **A dedicated fence.** Declaration blocks use a ` ```ts persistence-catalog ` info string that `doc-typecheck` recognizes and skips, excluded from the opt-out ratio — the same treatment as `ts cordis-catalog` (the declarations reference types from their owning modules and are not standalone-compilable).
- **Repo scope.** The catalog enumerates the packages in this repo, matching the siblings' packages-only scope; a downstream plugin can merge further event types, which are outside the catalog by construction. The walk defends its own assumptions with hard errors: the owning top-level `interface SessionEventMap` must be the single exported declaration in `@deepseek-ai/dsh-session` (an unrelated, local, or duplicate same-named interface cannot be catalogued as the on-disk vocabulary), no declaration may carry `extends` (inherited keys would join `keyof SessionEventMap` without a catalog row), every member must be a property signature with an explicit payload type (a method-form member would join `keyof` yet slip past a silent walk), and a duplicate member across declarations fails.
This supersedes the hand-copies: the session.md `hook/*` table, the compact README's event table, the hook-protocol README's payload bullets, and the session README's name-list now link the catalog instead of restating payloads (the surrounding semantics prose stays where it was). The two stray `@mode emit` tags on the hook-protocol merge members are removed — the new gate rejects them as the category error they were.
@@ -28,7 +28,7 @@ This supersedes the hand-copies: the session.md `hook/*` table, the compact READ
## Consequences
-- The catalog cannot drift: a vocabulary change the committed file doesn't reflect fails `verify-persistence-catalog` in the pre-push hook and CI, and a new merged event with no JSDoc fails the generator outright — a plugin can no longer add an undocumented on-disk record type.
-- Event prose has a single home, the JSDoc at the declaration; thin JSDoc yields a thin catalog entry, pressuring authors to document at the source.
+- The catalog cannot drift: a vocabulary or envelope change the committed file doesn't reflect fails `verify-persistence-catalog` in the pre-push hook and CI, and a new merged event with no JSDoc fails the generator outright — a plugin can no longer add an undocumented on-disk record type.
+- Event prose has a single home, the JSDoc at the declaration; the catalog preserves that JSDoc and any nested field comments without flattening or paraphrasing them.
- The `SurfaceEventType` union is now structurally load-bearing for docs: renaming an event without updating the union (or vice versa) fails the generator, not just the compiler.
- The badge derivation assumes the union stays a closed set of string literals with exactly one owner; a refactor away from that shape must update the generator in the same change.
diff --git a/.agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md b/.agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md
new file mode 100644
index 0000000000..1a6aa40477
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md
@@ -0,0 +1,28 @@
+# Agent Note: One gated in-file format for Agent Notes
+
+Status: implemented
+
+## Problem
+
+Agent Note paths encoded lifecycle and class, but file contents still mixed headings, status formats, ADR and proposal templates, and proposal-era sections in implemented records. Authors copied whichever neighbor they found, and lifecycle moves could skip the required rewrite because no gate enforced an in-file contract.
+
+## Decision
+
+[README.md § The file format](../../README.md#the-file-format) is the in-file contract — the header block (`# Agent Note: ` plus a dateless, folder-agreeing `Status:` enum whose only content is the rejection reason), the per-lifecycle body skeleton (`Problem` opener everywhere; `Proposal`/`Acceptance criteria`/`Risks` in `proposed/`; present-tense `Decision`/`Consequences` with proposal-era headings banned in `implemented/`; frozen proposal shape in `rejected/`), a mandatory `Alternatives considered` section, and the canonical section vocabulary between which bespoke technical sections stay free-form. `pnpm run verify-agent-note-format` ([scripts/verify-agent-note-format.ts](../../../../scripts/verify-agent-note-format.ts)) enforces every mechanical clause as part of `doc-sync`, so a lifecycle move that skips its rewrite now fails CI instead of review memory.
+
+The whole corpus was normalized in the same change that defined the format — the pre-release stance: no transition period, no dual-format tolerance. The one grandfather is content, not format: alternatives are recorded, never invented, so a pre-format Agent Note whose alternatives are not reconstructible from the record carries the exact `agent-note-format: alternatives-not-recorded` comment, which the gate accepts only for files dated before this Agent Note.
+
+## Alternatives considered
+
+- **A full rigid template** (one fixed section sequence per lifecycle, every Agent Note restructured to fit) — rejected: the big design Agent Notes carry eight to fifteen bespoke technical sections (package topology, wire contracts, schemas) that are load-bearing content, not drift; a rigid sequence would force destructive rewrites now and template-fighting forever.
+- **Header-only normalization** (H1 and Status, bodies untouched) — rejected: the debt markers flagged the *body* genre split, and leaving `Context`/`Decision` beside `Problem`/`Proposal` indefinitely resolves nothing.
+- **No Status line** (the folder already is the status; the three newest pre-format Agent Notes (and the zh counterpart of one) omitted the line) — rejected in favor of keeping a self-describing file: the drift risk that motivated dropping it is neutralized by gating the line against the folder instead.
+- **Dated status** (`Status: implemented (accepted YYYY-MM-DD)`) — rejected: the acceptance date is narrated history the writing rules keep out of docs; the filename carries first-proposed, git carries the rest, and the gate could check a date's format but never its truth.
+- **A bare `# ` H1** — rejected: the `Agent Note: ` prefix self-describes the genre when a file is read outside its tree, and the format gate prevents it from drifting.
+- **`## What we give up` as the implemented closer** (the README's own phrase for what an Agent Note records) — rejected: it names only costs, and an honest consequences section records what the trade-off bought as well.
+- **Convention without a gate** (write the contract down, enforce by review) — rejected: the slop checklist already outlawed spec-speak in `implemented/` by convention, and nineteen files show what convention alone achieves here.
+- **A standalone `FORMAT.md` contract file** — rejected because one front door carrying layout, classification, and format is easier to discover and maintain than two contract files.
+
+## Consequences
+
+Every Agent Note now costs slightly more structure, and the mandatory `Alternatives considered` section is deliberate friction: a decision recorded without what it beat invites the re-litigation Agent Notes exist to prevent. Pre-format Agent Notes whose alternatives were not reconstructible carry the grandfather comment permanently — an honest gap on the record rather than fabricated rationale. `doc-sync` gains one gate, and moving an Agent Note between lifecycle folders is now real work at move time (the body rewrite the move always owed) instead of deferred cleanup nothing tracked. The thirty-nine debt markers are gone, resolved by the template they were waiting for.
diff --git a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md b/.agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
similarity index 99%
rename from docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
rename to .agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
index 6ebb477dea..452c877182 100644
--- a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
+++ b/.agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
@@ -1,4 +1,4 @@
-# RFC: Export-surface JSDoc gate
+# Agent Note: Export-surface JSDoc gate
Status: implemented
diff --git a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md b/.agents/notes/implemented/process/2026-07-06-generated-config-catalog.md
similarity index 93%
rename from docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md
rename to .agents/notes/implemented/process/2026-07-06-generated-config-catalog.md
index 999ebd8503..82355ab511 100644
--- a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md
+++ b/.agents/notes/implemented/process/2026-07-06-generated-config-catalog.md
@@ -1,4 +1,4 @@
-# RFC: Generated plugin config catalog
+# Agent Note: Generated plugin config catalog
Status: implemented
@@ -8,7 +8,7 @@ The repository had no source-backed reference for plugin configuration. Package
## Decision
-`scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../config-catalog.md) from each plugin's declared config type and JSDoc, with injection requirements, referenced-type links, and a source pointer. Package-local types are included transitively; workspace and external types are linked or named. Deterministic `--write` and `--check` modes make the committed page a generated artifact.
+`scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../../docs/config-catalog.md) from each plugin's declared config type and JSDoc, with injection requirements, referenced-type links, and a source pointer. Package-local types are included transitively; workspace and external types are linked or named. Deterministic `--write` and `--check` modes make the committed page a generated artifact.
Pure AST generation is correct here for the same reason it is for the events/services catalog and NOT for the tool catalog: a config type is a static declaration and every schemastery schema in the repo is a static `z.object`/`z.intersect` literal, so the source is the whole truth — nothing about the config surface is runtime-composed.
diff --git a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.md b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md
similarity index 89%
rename from docs/rfc/implemented/process/2026-07-06-node-engine-floor.md
rename to .agents/notes/implemented/process/2026-07-06-node-engine-floor.md
index af08c96be3..507641a99a 100644
--- a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.md
+++ b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md
@@ -1,4 +1,4 @@
-# RFC: Raise the Node LTS engine floor to 22.19
+# Agent Note: Raise the Node LTS engine floor to 22.19
Status: implemented
@@ -13,7 +13,7 @@ Set `engines.node` to `^22.19.0 || >=24.0.0` and test the keyless CI compatibili
Two Node features gate the source runtime:
- **`node:sqlite`** — `packages/session-persistence/session-persistence-sqlite` does a top-level `import { DatabaseSync } from 'node:sqlite'`. The module dropped its `--experimental-sqlite` flag requirement at **22.13** (LTS) and **23.4** (Current); before those, importing it throws at load.
-- **Native TypeScript type-stripping** — the `packages/examples/stdio-demo/tests/built-bin.e2e.ts` smoke boots the published `lib/bin.js` under plain `node` (no tsx) and loads the example's `.ts` plugins (`mock-llm.ts`, `echo-tool.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
+- **Native TypeScript type-stripping** — the built-mode `examples/headless-agent/tests/keyless-smoke.e2e.ts` smoke boots `dsh-cli-demo`'s published `lib/bin.js` under plain `node` (no tsx) and loads the example's `.ts` test adapter (`cli-mock-llm.ts`). Type-stripping is the default from **22.18** (LTS) and **23.6** (Current); before those it needs `--experimental-strip-types`.
Those source features clear on the 22.x line at **22.18**, but the installed Pi adapter dependency raises the advertised LTS floor. `@deepseek-ai/dsh-llm-pi-ai` depends on `@earendil-works/pi-ai@0.79.3`, whose package declares `engines.node >=22.19.0`, so the LTS floor is **22.19**. The 24.x branch remains `>=24.0.0`. The disjoint range excludes Node 23 entirely: Node 23.0–23.5 still has at least one flagged source feature, and the 23 line is non-LTS/EOL, so advertising `>=23.6` would add a dead release line and a CI leg no deployment should use.
@@ -24,7 +24,7 @@ Those source features clear on the 22.x line at **22.18**, but the installed Pi
- The advertised LTS branch no longer undercuts the Pi adapter dependency floor.
- CI proves the Node 22 LTS floor directly with Node 22.19, keeps the Node 24 branch on `node: 24`, and keeps Node 26 for the next even line; each leg typechecks the source graph and launches the unbuilt workflow worker for real.
- The built-bin smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the test stays the plain `node lib/bin.js` path it documents.
-- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this RFC in the same change.
+- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this Agent Note in the same change.
## Alternatives considered
diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md
similarity index 99%
rename from docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.md
rename to .agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md
index be31a439c1..65ad437ca9 100644
--- a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.md
+++ b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md
@@ -1,4 +1,4 @@
-# RFC: Parallel GitHub CI gates
+# Agent Note: Parallel GitHub CI gates
Status: implemented
diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md
similarity index 94%
rename from docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.md
rename to .agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md
index 4e27b111d6..60472e51ec 100644
--- a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.md
+++ b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md
@@ -1,4 +1,4 @@
-# RFC: Parallel pre-push gates
+# Agent Note: Parallel pre-push gates
Status: implemented
@@ -14,7 +14,7 @@ Flattening those members directly into `lefthook.yml` solves the local hook only
[lefthook.yml](../../../../lefthook.yml) keeps one pre-push job named `full check` and runs `pnpm run check:pre-push`. That package script delegates to [scripts/run-gates.ts](../../../../scripts/run-gates.ts), the same bounded scheduler CI uses.
-The `pre-push` mode expands into leaf gates for the unit suite, snapshot suite, build, `hygiene` members, `doc-sync` members, and module-graph freshness. The leaf list keeps the same gate vocabulary as the package scripts, including RFC classification and RFC format, while the runner schedules independent checks with four active top-level workers by default; `DSH_GATE_CONCURRENCY` overrides that bound.
+The `pre-push` mode expands into leaf gates for the unit suite, snapshot suite, build, `hygiene` members, `doc-sync` members, and module-graph freshness. The leaf list keeps the same gate vocabulary as the package scripts, including Agent Note classification and Agent Note format, while the runner schedules independent checks with four active top-level workers by default; `DSH_GATE_CONCURRENCY` overrides that bound.
The build gate makes the hook self-contained from a clean worktree. `publint`, `verify-node-next-types`, and the pre-push form of `doc-typecheck` wait for that build output, while source-only gates continue in parallel.
diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md b/.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md
similarity index 57%
rename from docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
rename to .agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md
index 6cd8851149..0d294feb0f 100644
--- a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
+++ b/.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md
@@ -1,18 +1,18 @@
-# RFC: A gated Known-Limitations section in every package README
+# Agent Note: A gated Known-Limitations section in every package README
Status: implemented
## Problem
-The [documentation standard](../../../AGENTS.md) assigns limitations to package READMEs. Without a shared shape, an omitted section cannot distinguish an audited absence from forgotten documentation, and variant headings prevent a repository-wide search.
+The [documentation standard](../../../../docs/AGENTS.md) assigns limitations to package READMEs. Without a shared shape, an omitted section cannot distinguish an audited absence from forgotten documentation, and variant headings prevent a repository-wide search.
## Decision
-Every package manifest under `packages///package.json` has a sibling README with the canonical `## Known Limitations and Deferred Work` section. Its bullets record durable consumer gaps and non-obvious maintainer constraints owned by that package; ordinary cleanup remains in its source TODO or owning RFC. The [`verify-package-readme-limitations` gate](../../../../scripts/verify-package-readme-limitations.ts) derives the package set from manifests, rejects missing READMEs, and requires exactly one canonical h2 with at least one top-level bullet. Near-miss headings such as “Limitations,” “Deferred,” “What is NOT here,” or “Non-goals” fail.
+Every package manifest under `packages///package.json` has a sibling README with the canonical `## Known Limitations and Deferred Work` section. Its bullets record durable consumer gaps and non-obvious maintainer constraints owned by that package; ordinary cleanup remains in its source TODO or owning Agent Note. The [`verify-package-readme-limitations` gate](../../../../scripts/verify-package-readme-limitations.ts) derives the package set from manifests, rejects missing READMEs, and requires exactly one canonical h2 with at least one top-level bullet. Near-miss headings such as “Limitations,” “Deferred,” “What is NOT here,” or “Non-goals” fail.
A package with nothing to declare is listed in `NO_LIMITATIONS` and omits the section. Adding a limitation requires removing the entry; renames and removals fail because every entry must name a scanned package.
-The gate checks presence, shape, and the allowlist. Review under the documentation and [prose](../../../../.agents/skills/dsh-prose-standard/SKILL.md) standards owns coverage and accuracy. The standing rule lives in [packages/AGENTS.md](../../../../packages/AGENTS.md).
+The gate checks presence, shape, and the allowlist. Review under the documentation and [prose](../../../skills/dsh-prose-standard/SKILL.md) standards owns coverage and accuracy. The standing rule lives in [packages/AGENTS.md](../../../../packages/AGENTS.md).
## Alternatives considered
diff --git a/.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md b/.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md
new file mode 100644
index 0000000000..dd986f3661
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md
@@ -0,0 +1,31 @@
+# Agent Note: Package Model Experience contract
+
+Status: implemented
+
+## Problem
+
+A package README can explain APIs and runtime mechanics without answering the questions that dominate an agent harness's behavior and cost: what from this package reaches a model request, under which conditions, how long those tokens remain, and whether later requests preserve a reusable KV-cache prefix. The omission is especially hard to audit in a plugin architecture. A consumer may turn a backend result into a tool message, a policy plugin may replace success with an error, compaction may remove old history, and an agent-scoped registration may change one agent's prompt or schemas while leaving every other agent unchanged. Reading only the nominally model-facing packages therefore misses real context effects, while reading source across every dependency is too expensive for routine review.
+
+## Decision
+
+Every workspace package README with a model-facing or model-adjacent contract ends with the canonical [Model Experience section](../../../../docs/cookbook/adding-a-package.md#4-write-the-package-readme), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. An audited model-agnostic generic package omits the section through `NO_MODEL_EXPERIENCE_SECTION`.
+
+Packages with direct, conditional, capped, lifetime, multi-surface, or auxiliary-model effects use one H3 per context surface. Each surface contains three ordered H4 fields—`What the model sees`, `Token effect`, and `KV Cache effect`—and each field starts with one prose paragraph. The cache field distinguishes append-only growth, a stable repeated prefix, replacement of earlier tokens, and an independent model request; it names every package-owned configuration, scope, lifecycle, compaction, or routing change that can alter the request before newly appended content. “Does not invalidate” means the package preserves an already-reusable prefix, not that a provider promises a cache hit or retention period. Stable package-owned text is quoted exactly: system-prompt prose and other long literals use a titled H5 plus `markdown` fence under the field that introduces them, normally `What the model sees`, while short literals stay inline with named interpolation placeholders. Tool-schema surfaces link their anchored section in the generated [tool catalog](../../../../docs/tool-catalog.md) and state only composition or configuration deltas; runtime-only definitions explain why the catalog omits them. Data-dependent and provider-owned text is summarized. Agent-scoped visibility is explicit, and prompt and schema surfaces remain separate when scoping can hide one without the other.
+
+A package with no model-context effect, or one path rendered entirely by another package, uses the verifier's audited short form: one sentence beginning `None, as ` or `Indirectly, through ` followed by a `KV Cache effect` H4 and one prose paragraph. Pure transport and keyless test-support packages use the none form when they create no model-bound content. Provider backends use the indirect form even when they cap or filter data, and wiring bundles use it when named children own every effect. These sections locate the contribution and disclaim direct cache invalidation without restating the consumer. Structured sections likewise document only package-owned inputs, transformations, and deltas.
+
+`verify-package-readme-model-experience` discovers package manifests and validates the three classifications, canonical final-section order, exact field heading depth and order, non-empty field paragraphs, H5 ownership of verbatim blocks, concrete literal evidence, and anchored tool-catalog links. It runs in `doc-sync` and the parallel gate runner. Review still owns coverage, link relevance, and factual accuracy.
+
+## Alternatives considered
+
+- **Document only packages that register prompts or tools** — rejected because backends, policy plugins, adapters, persistence, scoping, and compaction change the content or lifetime of tokens without owning a model-facing schema.
+- **Generate one central context-cost catalog from source** — rejected because an AST can find registrations but cannot infer semantic conditions such as history retention, output truncation, parent-versus-child visibility, or an auxiliary model boundary. The package README is the implementation-local contract; a central copy would add another drift surface.
+- **Require numeric token counts** — rejected because exact counts depend on the selected model tokenizer, adapter serialization, configuration, and runtime data. The stable contract is the growth shape: fixed per request, conditional per call, retained, replaced, capped, or zero-direct.
+- **Use a table** — rejected because exact source text and conditional result shapes make cells dense and difficult to scan. Repeated subsections give each context surface readable vertical space while preserving the same fields.
+- **Allow every zero-impact package to omit the section** — rejected because unconstrained absence is ambiguous between an audited zero and forgotten documentation. Omission is reserved for model-agnostic generic packages named with a reason in the verifier; model-adjacent zero-impact packages keep one explicit sentence.
+- **Require the full structured form for audited zero or simple indirect packages** — rejected because it repeats labels around one fact. A gated sentence plus cache field preserves explicit coverage without the ceremony.
+- **Convention without a gate** — rejected because a repo-wide contract must also cover every future package; review memory cannot reliably detect an omitted README section.
+
+## Consequences
+
+A reviewer can start at any model-facing or model-adjacent package and see its contribution to the conversation model, child models, and auxiliary calls without reconstructing the full plugin graph. Token-budget work can distinguish repeated request overhead from data-dependent history, while cache-sensitive work can identify append-only paths and the earliest package-owned prefix mutation. Agent-scoped changes have an explicit documentation checkpoint. Package authors maintain one or more compact context-surface blocks or one classified short form whenever model-visible behavior changes; audited generic packages carry no irrelevant model boilerplate. The structured fields do not promise provider-exact token counts or cache hits; measurements remain model-, provider-, and workload-specific, while the documented growth, visibility, and prefix-stability contract stays stable.
diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
new file mode 100644
index 0000000000..b3bdbb0c1b
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md
@@ -0,0 +1,41 @@
+# Agent Note: Project canonical documentation into the website
+
+Status: implemented
+
+## Problem
+
+The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub.
+
+## Decision
+
+Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths.
+
+`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl.
+
+`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout.
+
+Locale home projections retain only the canonical YAML frontmatter. The repository-facing body can keep its H1 and bilingual source links, while the VitePress home theme owns the rendered hero and features and the site navigation owns locale switching.
+
+The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates.
+
+Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception.
+
+Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen.
+
+## Alternatives considered
+
+**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative.
+
+**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer.
+
+**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order.
+
+**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments.
+
+**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists.
+
+## Consequences
+
+Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection.
+
+The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation.
diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml
similarity index 59%
rename from docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml
rename to .agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml
index eb411f1158..366faaf6a3 100644
--- a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-14-typescript-program-backed-semantic-gates.md: 3e7a76e86d83080ae1a4f91ca97cc9749c90ef29
-2026-07-14-typescript-program-backed-semantic-gates.zh.md: 0a13452012e7f6cbd3ad7994845ba1985355c089
+2026-07-14-typescript-program-backed-semantic-gates.md: f9c00a4b6a5e9f08c11902e9267e4c1a954cebf8
+2026-07-14-typescript-program-backed-semantic-gates.zh.md: ce1f1edc765f621ca9f650720aa2db43f636e330
diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md
similarity index 99%
rename from docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md
rename to .agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md
index 3e7a76e86d..f9c00a4b6a 100644
--- a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md
+++ b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md
@@ -1,4 +1,4 @@
-# RFC: TypeScript Program-backed semantic gates
+# Agent Note: TypeScript Program-backed semantic gates
Status: implemented
diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md
similarity index 99%
rename from docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md
rename to .agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md
index 0a13452012..ce1f1edc76 100644
--- a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md
+++ b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md
@@ -1,4 +1,4 @@
-# RFC: 基于 TypeScript Program 的语义门禁
+# Agent Note: 基于 TypeScript Program 的语义门禁
Status: implemented
diff --git a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml
new file mode 100644
index 0000000000..31ec0bdb07
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml
@@ -0,0 +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-17-run-ci-examples-from-built-lib.md: 22f69ed56bfc479281648bfb40df5acbd129ebc0
+2026-07-17-run-ci-examples-from-built-lib.zh.md: 74b985f578dd25f785e556c0cd493a7a9292fc43
diff --git a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md
new file mode 100644
index 0000000000..22f69ed56b
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md
@@ -0,0 +1,42 @@
+# Agent Note: Run CI examples from built lib
+
+Status: implemented
+
+English | [中文](2026-07-17-run-ci-examples-from-built-lib.zh.md)
+
+## Problem
+
+CI boots examples and Cordis-backed test projects through `node --import tsx` and the root tsconfig `paths` map. This adds TypeScript transformation cost and changes package resolution: imports resolve to workspace source instead of following package `exports` into built `lib/`.
+
+These runs therefore do not test the same code or resolution behavior as an installed consumer. A package can pass CI while its built export graph is incomplete or resolves differently.
+
+## Decision
+
+Execution has two modes. `src` is the default local-development mode and uses tsx; `lib` is the strict CI mode and starts built bins with plain Node, without tsx or tsconfig path mapping.
+
+- CI subprocesses that boot an example or a checked-in `cordis.yml` use `lib` mode.
+- TypeScript fixtures that only implement an ACP or MCP peer and do not load Cordis run directly with Node. An explicit source-path regression may remain in `src` mode.
+
+### Resolution topology
+
+Every test Cordis config must resolve its bare modules by walking upward from the config directory.
+
+- `examples/` is one pnpm workspace member and provides the shared `examples/node_modules` resolution root.
+- Every checked-in test Cordis config, including snapshot configs and package-owned fixtures, lives under its corresponding `examples//` tree. A config owned by `packages///` maps to `examples//tests/fixtures///cordis.yml`; the test driver and assertions remain package-local.
+- Every package named by an example Cordis config is declared in both `examples/package.json` for `lib` resolution and the root `tsconfig.json` references for `src` mode.
+
+### Launch policy
+
+The shared Loader test harness selects `src` or `lib` from `DSH_EXAMPLE_MODE`. CI builds first and selects `lib`; an unset mode keeps the fast local source loop.
+
+## Alternatives considered
+
+- **Keep CI on tsx** — rejected because it preserves transformation overhead and source-only resolution behavior.
+- **Use lib everywhere** — rejected because local development would require a build before every run. Dual mode keeps that cost out of the development loop.
+- **Build a private `node_modules` tree per test** — rejected because it duplicates consumer scaffolding. The `examples/` workspace root gives every Cordis config one real and declared resolution path.
+
+## Consequences
+
+- CI validates built package exports without tsx changing module resolution; local development retains the no-build source loop.
+- CI must build before these tests, and manual `lib` runs can observe stale local artifacts.
+- Cordis config dependencies are not visible to normal TypeScript import analysis, so `examples/package.json` and the root tsconfig references must stay synchronized with the configs.
diff --git a/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
new file mode 100644
index 0000000000..74b985f578
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md
@@ -0,0 +1,42 @@
+# Agent Note: 在 CI 中从构建后的 lib 运行示例
+
+Status: implemented
+
+[English](2026-07-17-run-ci-examples-from-built-lib.md) | 中文
+
+## 问题
+
+CI 通过 `node --import tsx` 和根 tsconfig 的 `paths` 映射启动示例与加载 Cordis 配置的测试项目。这种方式既增加了 TypeScript 转换开销,也改变了包解析行为:import 会解析到 workspace 源码,而不是经包的 `exports` 进入构建后的 `lib/`。
+
+因此,这些测试没有覆盖已安装消费方实际运行的代码和解析路径。即使包的构建导出图不完整或解析结果不同,CI 仍可能通过。
+
+## 决策
+
+执行机制包含两种模式。`src` 是本地开发的默认模式并使用 tsx;`lib` 是严格的 CI 模式,通过 plain Node 启动构建后的 bin,不加载 tsx,也不使用 tsconfig 路径映射。
+
+- CI 中启动示例或签入仓库的 `cordis.yml` 的子进程使用 `lib` 模式。
+- 仅实现 ACP 或 MCP 对端、且不加载 Cordis 的 TypeScript fixture(测试前置数据)直接由 Node 运行。只有显式验证源码路径的回归测试可以保留 `src` 模式。
+
+### 解析拓扑
+
+每个测试 Cordis 配置都必须能从配置文件所在目录向上解析裸模块。
+
+- `examples/` 作为一个 pnpm workspace 成员,提供统一的 `examples/node_modules` 解析根目录。
+- 所有签入仓库的测试 Cordis 配置,包括快照配置和包内测试 fixture,都放在对应的 `examples//` 目录树下。归属 `packages///` 的配置映射到 `examples//tests/fixtures///cordis.yml`;测试驱动和断言仍留在包内。
+- 示例 Cordis 配置中引用的每个包都同时登记在 `examples/package.json` 和根 `tsconfig.json` 的 references 中,分别支持 `lib` 与 `src` 解析。
+
+### 启动策略
+
+共享 Loader 测试 harness 通过 `DSH_EXAMPLE_MODE` 选择 `src` 或 `lib`。CI 先构建再选择 `lib`;未设置模式时保留快速的本地源码开发回路。
+
+## 曾考虑的替代方案
+
+- **CI 继续使用 tsx**:不予采纳,因为它会保留转换开销和仅适用于源码的解析行为。
+- **所有环境只使用 lib**:不予采纳,因为本地开发每次运行前都必须构建。双模式避免把这项成本带入开发回路。
+- **每个测试单独构造 `node_modules`**:不予采纳,因为它会重复消费方脚手架。以 `examples/` 作为 workspace 根,可让每个 Cordis 配置通过同一条真实且显式声明的路径解析模块。
+
+## 后果
+
+- CI 可以验证构建后的包导出,不再受 tsx 模块解析影响;本地开发仍保留免构建的源码回路。
+- CI 必须先构建再运行这些测试;手动执行 `lib` 模式时可能读取陈旧的本地产物。
+- 常规 TypeScript import 分析无法识别 Cordis 配置依赖,因此 `examples/package.json`、根 tsconfig references 与配置文件必须保持同步。
diff --git a/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.i18n.yaml b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.i18n.yaml
new file mode 100644
index 0000000000..2b1dc53f6b
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.i18n.yaml
@@ -0,0 +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-19-remove-generated-agent-note-index.md: 27c1591b29a1ca64370de6ffadfb9c524a804ced
+2026-07-19-remove-generated-agent-note-index.zh.md: 868955bc10900f784bd88066042abe24454e27b5
diff --git a/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md
new file mode 100644
index 0000000000..27c1591b29
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md
@@ -0,0 +1,33 @@
+# Agent Note: Keep Agent Notes discoverable without a generated index
+
+Status: implemented
+
+English | [中文](2026-07-19-remove-generated-agent-note-index.zh.md)
+
+## Problem
+
+A committed Agent Note index duplicates facts already encoded by each file's lifecycle/class path, filename date, and H1. Every branch that adds, moves, or renames an otherwise unrelated Agent Note rewrites the same generated file, making that artifact a predictable merge hotspot.
+
+The centralized chronological list adds little discovery value beyond browsing the lifecycle/class tree or searching the repository, while its generator, renderer, command, and freshness check remain maintenance surface.
+
+## Decision
+
+The lifecycle/class filesystem tree is the Agent Note inventory. [README.md](../../README.md) remains the curated front door and contract, while ordinary tree navigation and repository search provide discovery.
+
+`scripts/agent-note-tree.ts` owns the closed lifecycle/class sets and structural walker. `verify-agent-note-classification` validates that tree and rejects the legacy homes and a root `INDEX.md`; it does not render or freshness-check a centralized list.
+
+This decision supersedes the rejected [generated-index proposal](../../rejected/process/2026-07-04-generate-agent-note-index-tables.md).
+
+## Alternatives considered
+
+**Keep the committed generated index and resolve conflicts by regenerating it.** Regeneration makes conflict resolution mechanical but does not prevent unrelated branches from modifying the same artifact or reduce the review noise it creates.
+
+**Offer an uncommitted on-demand index command.** It avoids committed conflicts but preserves a renderer and command for a discovery path already served by tree navigation and repository search.
+
+**Restore a hand-maintained index.** It has the same shared-file contention and adds completeness/order mistakes that generation avoided.
+
+## Consequences
+
+- Adding, moving, or renaming an Agent Note no longer changes a corpus-wide generated file.
+- The classification gate performs less work and the documentation gate topology gains no process or stage.
+- Readers give up a single chronological page and use the lifecycle/class tree or repository search instead.
diff --git a/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md
new file mode 100644
index 0000000000..868955bc10
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md
@@ -0,0 +1,33 @@
+# Agent Note: 无需生成索引即可发现 Agent Note
+
+Status: implemented
+
+[English](2026-07-19-remove-generated-agent-note-index.md) | 中文
+
+## 问题
+
+提交到仓库的 Agent Note 索引,会重复记录每个文件的生命周期/类别路径、文件名日期和 H1 已经编码的事实。任何分支只要添加、移动或重命名彼此无关的 Agent Note,都会重写同一个生成文件,因此该产物会成为可预见的合并冲突热点。
+
+与浏览生命周期/类别目录树或搜索仓库相比,这份集中式时间顺序清单提供的发现价值有限;但其生成器、渲染器、命令和新鲜度检查仍然构成维护负担。
+
+## 决策
+
+生命周期/类别文件系统目录树就是 Agent Note 清单。[README.md](../../README.md) 继续作为人工维护的入口和契约,普通的目录树浏览与仓库搜索负责内容发现。
+
+`scripts/agent-note-tree.ts` 持有封闭的生命周期/类别集合与结构遍历器。`verify-agent-note-classification` 校验该目录树,并拒绝旧目录和根目录中的 `INDEX.md`,但不会渲染集中式清单或检查其新鲜度。
+
+本决策取代已拒绝的[生成索引提案](../../rejected/process/2026-07-04-generate-agent-note-index-tables.md)。
+
+## 备选方案
+
+**保留提交到仓库的生成索引,并通过重新生成解决冲突。** 重新生成能让冲突解决过程机械化,但无法阻止无关分支修改同一产物,也不会减少由此产生的评审噪音。
+
+**提供不提交到仓库的按需索引命令。** 这可以避免已提交文件的冲突,但仍需维护渲染器和命令,而目录树浏览与仓库搜索已经覆盖该发现路径。
+
+**恢复人工维护的索引。** 它具有相同的共享文件争用问题,还会重新引入生成机制已经避免的完整性和排序错误。
+
+## 影响
+
+- 添加、移动或重命名 Agent Note 时,不再改动覆盖整个语料库的生成文件。
+- 分类门禁执行的工作更少,文档门禁拓扑也不会增加进程或阶段。
+- 读者不再获得单一的时间顺序页面,改用生命周期/类别目录树或仓库搜索。
diff --git a/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.i18n.yaml b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.i18n.yaml
new file mode 100644
index 0000000000..ae5ed9b11e
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.i18n.yaml
@@ -0,0 +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-19-require-agent-notes-for-non-trivial-changes.md: f2645832ebcdd0b81cbff5415c7eb6f60b6fa8cf
+2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md: 659aa7cad0823fa0082be1827f8c083037376a4c
diff --git a/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.md b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.md
new file mode 100644
index 0000000000..f2645832eb
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.md
@@ -0,0 +1,31 @@
+# Agent Note: Require an Agent Note for every non-trivial change
+
+Status: implemented
+
+English | [中文](2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md)
+
+## Problem
+
+A selective threshold based on whether a decision seems durable, contested, and surprising lets substantial changes land without preserving their rationale. Code and tests show what changed, but they cannot consistently preserve why an approach won, which alternatives lost, or what costs maintainers accepted.
+
+## Decision
+
+Every non-trivial change adds or updates at least one Agent Note in the same PR. Non-trivial changes include behavior, architecture, cross-file or cross-package contracts, process or tooling, testing strategy, on-disk, wire, or configuration formats, and other decisions a maintainer may reasonably revisit.
+
+Updating the note that already owns a decision satisfies the rule; a new note is required only when no note owns it. Purely mechanical or local edits with no behavioral, contractual, structural, process, or rationale change are exempt. The [Agent Notes README](../../README.md#when-to-write-one) owns this boundary, while root `AGENTS.md` carries the standing order.
+
+Review enforces the semantic boundary. No automated gate attempts to classify a diff as trivial or non-trivial, so this policy adds no gate stage or runtime.
+
+## Alternatives considered
+
+**Require notes only for decisions judged durable, contested, and surprising.** The threshold is subjective enough that a substantial change can be treated as obvious or local, losing the rationale Agent Notes exist to preserve.
+
+**Require a new note for every change.** This duplicates an existing note when it already owns the decision and adds empty ceremony to purely mechanical edits.
+
+**Add a CI diff-classification gate.** A mechanical check cannot reliably determine whether a semantic change is trivial, while another gate adds runtime and invites false positives or superficial compliance.
+
+## Consequences
+
+- Every substantial change preserves its rationale and rejected alternatives beside the implementation.
+- Contributors maintain an existing owning note instead of creating duplicate records.
+- Mechanical edits remain lightweight, and the gate topology and runtime remain unchanged.
diff --git a/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md
new file mode 100644
index 0000000000..659aa7cad0
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 每项实质性变更都必须附带 Agent Note
+
+Status: implemented
+
+[English](2026-07-19-require-agent-notes-for-non-trivial-changes.md) | 中文
+
+## 问题
+
+如果只在决策被认为持久、有争议且出人意料时才记录 Agent Note,实质性变更就可能在没有保存决策依据的情况下落地。代码和测试能展示改动内容,却无法稳定保留某种方案胜出的原因、被放弃的备选方案,以及维护者接受的成本。
+
+## 决策
+
+每项实质性变更都在同一个 PR 中新增或更新至少一份 Agent Note。实质性变更包括行为、架构、跨文件或跨包契约、流程或工具、测试策略、磁盘格式、线协议或配置格式,以及维护者可能合理重审的其他决策。
+
+更新已经持有该决策的 Agent Note 即满足规则;仅当没有 Agent Note 持有该决策时才新增记录。完全机械或局部、且不改变行为、契约、结构、流程或决策依据的编辑可豁免。[Agent Notes README](../../README.md#when-to-write-one) 持有这条边界,根目录 `AGENTS.md` 则携带常驻指令。
+
+评审负责执行这条语义边界。自动化门禁不尝试把差异分类为平凡或实质性变更,因此这项政策不会增加门禁阶段或运行时间。
+
+## 备选方案
+
+**只为被判断为持久、有争议且出人意料的决策要求 Agent Note。** 这条门槛过于主观,实质性变更可能被视为显而易见或局部改动,从而丢失 Agent Note 本应保存的决策依据。
+
+**每项变更都必须新增 Agent Note。** 当现有 Agent Note 已经持有该决策时,这会产生重复记录,也会让纯机械编辑承担空洞的流程负担。
+
+**添加 CI 差异分类门禁。** 机械检查无法可靠判断语义变更是否平凡,额外门禁还会增加运行时间,并引入误报或表面合规。
+
+## 影响
+
+- 每项实质性变更都会在实现旁保留其决策依据和被放弃的备选方案。
+- 贡献者维护现有的决策持有记录,而不是创建重复记录。
+- 机械编辑仍保持轻量,门禁拓扑和运行时间也保持不变。
diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml
new file mode 100644
index 0000000000..6bae3b4d87
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml
@@ -0,0 +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-20-generated-cordis-core-api.md: 848dec2dba6f432c706798c40abe98e8937da651
+2026-07-20-generated-cordis-core-api.zh.md: c40a480224f4e1387b71ade9264458cd84403584
diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md
new file mode 100644
index 0000000000..848dec2dba
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md
@@ -0,0 +1,31 @@
+# Agent Note: Generate the Cordis core API reference
+
+Status: implemented
+
+English | [中文](2026-07-20-generated-cordis-core-api.zh.md)
+
+## Problem
+
+Plugin authors need the detailed Cordis APIs behind `ctx`, event dispatch, fibers, plugin registration, and services. The generated [Harness event and service catalogs](2026-06-20-generated-cordis-catalog.md) intentionally summarize inherited Cordis members, so they do not replace a method-level Cordis reference. Keeping a second hand-written copy under the website would drift from the vendored source and make the renderer an additional documentation owner.
+
+## Decision
+
+`scripts/cordis-core-api.ts` reads the public declarations and original JSDoc from `vendor/cordis/src` with the TypeScript compiler API. An explicit page manifest generates five files under [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md): Context, Events, Fiber, Registry, and Service. `scripts/gen-cordis-catalog.ts` writes these pages together with the Harness event and service catalogs, and `verify-cordis-catalog` rejects stale output.
+
+The generator validates that documented classes and methods retain descriptive JSDoc, including parameter and non-void return contracts. It emits declaration-only `ts cordis-catalog` fences with the original JSDoc, then renders the same description, parameters, and return contract as readable Markdown. Source links point to the vendored files, and the five pages cross-link to one another. The Harness catalogs remain the exhaustive inventory of repository-declared events and `ctx.*` services; the core pages document how the inherited Cordis APIs operate.
+
+`website/docs.ts` publishes the five canonical files under matching `/reference/cordis-api/` and `/en/reference/cordis-api/` routes. Both locales use the English generated source until the generator emits translated pages, so changing language preserves navigation structure and route identity.
+
+## Alternatives considered
+
+**Restore the old website files as canonical Markdown.** This would recover the pages quickly, but their signatures and prose could drift from the vendored implementation and the website would regain a second documentation source.
+
+**Expand the inherited tier of the Harness catalogs in place.** Those catalogs answer which Harness events and services exist. Mixing full framework class references into the same pages would obscure that inventory and reverse their deliberate terse inherited tier.
+
+**Publish vendored source declarations directly.** Source files are authoritative but do not provide stable topic pages, curated public ordering, or website navigation, and they expose implementation bodies that are not part of the reference contract.
+
+## Consequences
+
+The five Cordis API pages follow vendor updates through one deterministic generator and share the repository's documentation freshness gate. The website gains a dedicated Cordis API section without copied site content, while root and English navigation remain structurally identical.
+
+The page manifest is curated, so a newly public Cordis core type needs an explicit generator entry. Generated prose is English-only, and source JSDoc quality directly limits reference quality; Chinese output requires generator-level translation rather than hand-editing the generated files.
diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md
new file mode 100644
index 0000000000..c40a480224
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md
@@ -0,0 +1,31 @@
+# Agent Note: 生成 Cordis 核心 API 参考文档
+
+Status: implemented
+
+[English](2026-07-20-generated-cordis-core-api.md) | 中文
+
+## 问题
+
+插件作者需要了解 `ctx`、事件派发、Fiber、插件注册和 Service 背后的详细 Cordis API。已有的 [Harness 事件与服务目录](2026-06-20-generated-cordis-catalog.md)有意只简要概括继承自 Cordis 的成员,因此无法替代方法级 Cordis 参考文档。如果在网站下维护另一份手写副本,它会与 vendored 源码产生漂移,也会让渲染器成为额外的文档所有者。
+
+## 决策
+
+`scripts/cordis-core-api.ts` 使用 TypeScript Compiler API,从 `vendor/cordis/src` 读取公开声明和原始 JSDoc。一个显式页面清单在 [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md) 下生成五个文件:Context、Events、Fiber、Registry 和 Service。`scripts/gen-cordis-catalog.ts` 将这些页面与 Harness 事件和服务目录一同写入,`verify-cordis-catalog` 会拒绝过期产物。
+
+生成器会验证所记录的类和方法保留描述性 JSDoc,包括参数和非 void 返回值契约。它生成包含原始 JSDoc 且仅含声明的 `ts cordis-catalog` 代码围栏,再将同一份说明、参数和返回值契约渲染为便于阅读的 Markdown。源码链接指向 vendored 文件,五个页面之间相互交叉链接。Harness 目录仍是仓库声明的事件与 `ctx.*` 服务的完整清单;核心页面负责说明继承自 Cordis 的 API 如何工作。
+
+`website/docs.ts` 将五个规范源文件发布到结构对应的 `/reference/cordis-api/` 和 `/en/reference/cordis-api/` 路由。在生成器产出翻译页面之前,两个 locale 都使用英文生成源,因此切换语言时导航结构和路由标识保持不变。
+
+## 考虑过的替代方案
+
+**将旧网站文件恢复为规范 Markdown。** 这能快速恢复页面,但其签名和说明可能与 vendored 实现漂移,网站也会重新成为第二个文档来源。
+
+**直接扩充 Harness 目录中的继承层。** 这些目录回答有哪些 Harness 事件与服务。将完整的框架类参考混入同一页面会模糊这份清单的定位,并推翻继承层保持精简的既有决定。
+
+**直接发布 vendored 源码声明。** 源文件具有权威性,但不能提供稳定的主题页面、经过筛选的公开顺序或网站导航,还会暴露不属于参考契约的实现体。
+
+## 影响
+
+五个 Cordis API 页面通过同一个确定性生成器跟随 vendor 更新,并复用仓库的文档新鲜度检查。网站无需复制内容即可获得独立的 Cordis API 章节,中文入口和英文入口的导航结构保持一致。
+
+页面清单需要人工维护,因此新增公开 Cordis 核心类型时必须显式添加生成器条目。当前生成说明只有英文,且源码 JSDoc 的质量直接决定参考文档质量;中文产物需要在生成器层实现翻译,不能手工编辑生成文件。
diff --git a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md b/.agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md
similarity index 88%
rename from docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md
rename to .agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md
index 54f8397fc9..f1a9c9aa3c 100644
--- a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md
+++ b/.agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md
@@ -1,4 +1,4 @@
-# RFC: Drop the mutable session summary
+# Agent Note: Drop the mutable session summary
Status: implemented
@@ -20,7 +20,7 @@ Delete the mutable session summary entirely. `SessionSummary` and the `SessionMe
Anything the summary was meant to provide is **derivable from the append-only log** when a consumer actually needs it (`firstPrompt` = first `user/message`; recency = the last event's `time` or the file mtime) or already lives in the immutable header (`createdAt`, `cwd`). The one thing *not* derivable — a user-*edited* title — had no implementation and is pure YAGNI; it can return as its own log event or header field if a real feature ever needs it.
-This is recorded as a decision because it is **durable** (it narrows a public service contract and an on-disk format across two backends), **contested** (the summary was a deliberate forward-looking design, not an accident), and **surprising** (a future reader finding `SessionHeader` where the original RFC describes `SessionMeta` would otherwise ask why the summary vanished). It also unblocks the [shared persistence write coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md): with no mutable summary, the coordinator's hook interface needs no `updateSummary` hook and the JSONL-sidecar-vs-SQLite-column durability divergence disappears, so the two backends' write paths converge.
+This is recorded as a decision because it is **durable** (it narrows a public service contract and an on-disk format across two backends), **contested** (the summary was a deliberate forward-looking design, not an accident), and **surprising** (a future reader finding `SessionHeader` where the original Agent Note describes `SessionMeta` would otherwise ask why the summary vanished). It also unblocks the [shared persistence write coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md): with no mutable summary, the coordinator's hook interface needs no `updateSummary` hook and the JSONL-sidecar-vs-SQLite-column durability divergence disappears, so the two backends' write paths converge.
## No migration
@@ -30,4 +30,4 @@ This is unreleased software (see [root AGENTS.md](../../../../AGENTS.md) § "Pre
A future session picker now has to derive its preview/ordering from the log (or reintroduce a typed field) rather than reading a ready-made summary row. That is the correct cost: a cache for a feature that does not exist is dead weight that every backend pays to maintain and every contract test pays to assert. The principle — **a passing test pins current behavior, not necessarily correct behavior; behavior can be an artifact of a past compromise** — is now recorded as a standalone convention in [root AGENTS.md](../../../../AGENTS.md), with this change as its worked example.
-
+
diff --git a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md b/.agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md
similarity index 96%
rename from docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md
rename to .agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md
index a93e3d3196..4e8a092989 100644
--- a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md
+++ b/.agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md
@@ -1,4 +1,4 @@
-# RFC: Fold trace-only session facts into load-bearing events
+# Agent Note: Fold trace-only session facts into load-bearing events
Status: implemented
@@ -33,7 +33,7 @@ A consumer can no longer filter the canonical log for standalone `usage` or step
## Implementation note
-Shipped as proposed, with one scope refinement (per AGENTS.md "RFCs are proposals, not golden truth"):
+Shipped as proposed, with one scope refinement (per AGENTS.md "Agent Notes are proposals, not golden truth"):
- **Empty-content `assistant/message` hosts usage with no data loss.** The proof the proposal demanded (no persisted usage chunk becomes unrepresented) lands on the max-tokens path: a step cut off with usage but empty content (e.g. only a dropped tool call) previously emitted a standalone `usage`. It now records an empty-content `assistant/message { content: [], usage }`. To keep that from injecting a spurious content-less assistant turn into the provider transcript, `deriveMessages()` skips empty-content `assistant/message` events. A regression test asserts usage stays represented AND derived history is uncorrupted.
diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
similarity index 81%
rename from docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
rename to .agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
index ab921dd62c..d8d4015b0d 100644
--- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
+++ b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md
@@ -1,4 +1,4 @@
-# RFC: Drop the unconsumed `llm/adapter-change` event
+# Agent Note: Drop the unconsumed `llm/adapter-change` event
Status: implemented
@@ -6,25 +6,25 @@ Status: implemented
`LlmService.registerAdapter()` emits `llm/adapter-change` on registration and disposal ([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts)). Grepping `llm/adapter-change` across `packages/*/src` and `examples/*/src` finds only the declaration, emit sites, docs, and tests; no production listener subscribes to it.
-This differs from `tools/change` and `system-prompt/change`. Those two events are also unconsumed today, but they are plausible registry-change signals for future live tool/prompt UIs. LLM adapter registration is more of a boot-time implementation detail: adapters are not a user-visible palette and the real model-call interception seam is `llm/stream`. Keeping an adapter-change event with no listener repeats the [drop-the-dead-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) pattern at a smaller scale.
+This differs from `tools/change` and `system-prompt/change`. Those two events are also unconsumed today, but they are plausible registry-change signals for future live tool/prompt UIs. LLM adapter registration is more of a boot-time implementation detail: adapters are not a user-visible palette and the real model-call interception seam is `llm/stream`. Keeping an adapter-change event with no listener repeats the [drop-the-dead-summary](2026-06-19-drop-mutable-session-summary.md) pattern at a smaller scale.
The event is not free. `registerAdapter()` yields its rollback disposer before emitting `llm/adapter-change` so a throwing listener unwinds the mutation instead of leaking an adapter entry, and the package carries tests for that listener-throw path. That defensive ordering protects a failure mode only tests can trigger.
## Decision
-Only `llm/adapter-change` is removed: the declaration in `dsh-llm`'s `interface Events`, the `ctx.emit('llm/adapter-change')` calls, and the "Emits `llm/adapter-change` on registration and disposal" sentence in `LlmService.registerAdapter`'s JSDoc. `registerAdapter()`'s effect generator keeps the mutation and rollback disposer for HMR/disposal but sheds the listener-throw rollback ordering that existed only for the removed event. The adapter-disposer test asserts the returned disposer removes the adapter without subscribing to the event; the listener-throw rollback test is gone with its subject. The event taxonomy in [docs/architecture.md](../../../architecture.md) and [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) is updated in the same change.
+Only `llm/adapter-change` is removed: the declaration in `dsh-llm`'s `interface Events`, the `ctx.emit('llm/adapter-change')` calls, and the "Emits `llm/adapter-change` on registration and disposal" sentence in `LlmService.registerAdapter`'s JSDoc. `registerAdapter()`'s effect generator keeps the mutation and rollback disposer for HMR/disposal but sheds the listener-throw rollback ordering that existed only for the removed event. The adapter-disposer test asserts the returned disposer removes the adapter without subscribing to the event; the listener-throw rollback test is gone with its subject. The event taxonomy in [docs/architecture.md](../../../../docs/architecture.md) and [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) is updated in the same change.
## Alternatives considered
### Why not remove every registry change event?
-A microkernel where registries announce mutations is a coherent convention. `tools/change` and `system-prompt/change` may become useful when a UI can live-refresh available tools or prompt sections. This RFC leaves that convention intact where it has a plausible user-facing consumer and cuts only the adapter-change event whose current and likely future consumer is unclear.
+A microkernel where registries announce mutations is a coherent convention. `tools/change` and `system-prompt/change` may become useful when a UI can live-refresh available tools or prompt sections. This Agent Note leaves that convention intact where it has a plausible user-facing consumer and cuts only the adapter-change event whose current and likely future consumer is unclear.
If an LLM adapter browser or dynamic model-picker needs this signal later, reintroduce it with that consumer and a clearer payload than "something changed."
## Verification
-`llm/adapter-change` and its emits are gone and the regenerated cordis catalog is fresh; HMR-safety holds (disposing a contributing fiber removes the adapter); `tools/change` and `system-prompt/change` remain documented and tested; and no production path changed observable behavior — the ACP snapshot goldens and the echo-agent smoke are byte-unchanged.
+`llm/adapter-change` and its emits are gone and the regenerated cordis catalog is fresh; HMR-safety holds (disposing a contributing fiber removes the adapter); `tools/change` and `system-prompt/change` remain documented and tested; and the ACP snapshots plus the keyless Headless Loader smoke pin the unchanged production paths.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
similarity index 86%
rename from docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
rename to .agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
index 2b10bf36db..b482a444b5 100644
--- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
+++ b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md
@@ -1,4 +1,4 @@
-# RFC: Drop unconsumed assembled LLM convenience surfaces
+# Agent Note: Drop unconsumed assembled LLM convenience surfaces
Status: implemented
@@ -12,7 +12,7 @@ Status: implemented
The only production consumer of the LLM service is the agent loop, and it uses `stream()` exclusively — feeding raw chunks through its own `BlockAssembler` so it can log chunks for replay fidelity while assembling in parallel ([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts), the `ctx.llm.stream(req)` step). Grepping `streamBlocks` and `ctx.llm.generate` across `packages/*/src` and `examples/*/src` finds no production callers. The references are the service methods, docs, and tests; adapter tests use `generate()` as a convenient driver, but they can hand-drain `stream()` through the same assembler helper without preserving a public production API.
-This is the [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) pattern: assembled-view APIs with tested contracts, consumed by tests rather than production. They were built speculatively for consumers that do not care about token-level deltas, but the one real consumer cares about deltas precisely so it can persist high-fidelity replay data.
+This is the [drop-mutable-session-summary](2026-06-19-drop-mutable-session-summary.md) pattern: assembled-view APIs with tested contracts, consumed by tests rather than production. They were built speculatively for consumers that do not care about token-level deltas, but the one real consumer cares about deltas precisely so it can persist high-fidelity replay data.
`streamBlocks()` drags a dedicated slice of `BlockAssembler` behind it: `flushReady()` and `flushRemaining()` ([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts)) plus the `flushed` cursor field exist only to support incremental in-order yield. `generate()` drags `GenerateResult`, `BlockAssembler.result()`, and the `llm/generate` waterfall as a second interception surface over the same underlying stream. The loop's assembler usage is `push()` / `message()` / `usage` / `finish` — not streaming flush or one-shot service assembly.
@@ -26,7 +26,7 @@ This is the [drop-mutable-session-summary](../../implemented/simplification/2026
## Verification
-`streamBlocks`, `generate`, `llm/generate`, and the assembler helpers they alone required are gone with no new dead exports; both real adapters are exercised through `stream()` and the shared assembler; the loop behaves identically (ACP snapshot goldens unchanged); and the README, architecture doc, and module docs carry no mention of the removed surfaces.
+`streamBlocks`, `generate`, `llm/generate`, and the assembler helpers they alone required are gone with no new dead exports; both real adapters are exercised through `stream()` and the shared assembler; the loop behaves identically (ACP snapshot expected outputs unchanged); and the README, architecture doc, and module docs carry no mention of the removed surfaces.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md b/.agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
similarity index 77%
rename from docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
rename to .agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
index ce16ff2ee2..782ffe891e 100644
--- a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
+++ b/.agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
@@ -1,25 +1,25 @@
-# RFC: Prune dead methods from the persistence seam
+# Agent Note: Prune dead methods from the persistence seam
Status: implemented
-> **Implementation note:** Only `SessionPersistence.has()` and `.delete()` were removed. `BashExecutor.get()` and `.list()` remain because removing their one-line lookup surface required substantially more completion-tracking machinery in consumers. Their id branding is covered by the [branded-ids RFC](../architecture/2026-06-20-branded-ids.md).
+> **Implementation note:** Only `SessionPersistence.has()` and `.delete()` were removed. `BashExecutor.get()` and `.list()` remain because removing their one-line lookup surface required substantially more completion-tracking machinery in consumers. Their id branding is covered by the [branded-ids Agent Note](../architecture/2026-06-20-branded-ids.md).
## Problem
-A capability seam ([interface / implementation / consumer](../../implemented/architecture/2026-06-13-capability-seams.md)) carries abstract methods that no consumer calls. The seam exists to let implementations and consumers evolve independently — but a method no consumer programs against is not a seam, it is speculative surface every implementation must still implement and test.
+A capability seam ([interface / implementation / consumer](../architecture/2026-06-13-capability-seams.md)) carries abstract methods that no consumer calls. The seam exists to let implementations and consumers evolve independently — but a method no consumer programs against is not a seam, it is speculative surface every implementation must still implement and test.
### `SessionPersistence.has()` and `.delete()`
The abstract service declared its operations beyond create/append: `load`, `list`, `has`, `delete`. Production consumers of `ctx.sessionPersistence` use only two: the agent-loop resume path calls `load()` ([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)), and the ACP bridge calls `list()` for `session/list` ([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts)). Grepping every `sessionPersistence.*` / `persistence.*` use across `packages/*/src` and `examples/` finds no `has(` and no `delete(` on the service. The `.has(`/`.delete(` calls in `packages/ui/acp/src/index.ts` are on the in-memory `SessionStore` and a local `Set` of loading ids, not persistence. The only callers of `has`/`delete` were the contract suites and per-backend specs.
-`has()` was not just unused — it was the most intricate branch in the shared coordinator: a tracked-vs-untracked dual-probe (`loadLive(id, cwd)` for a live-tracked session vs `loadStored(id)` for an untracked one) with a multi-line rationale. `delete()` dragged the `deleteStored` backend hook that every backend had to implement. This is the [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one.
+`has()` was not just unused — it was the most intricate branch in the shared coordinator: a tracked-vs-untracked dual-probe (`loadLive(id, cwd)` for a live-tracked session vs `loadStored(id)` for an untracked one) with a multi-line rationale. `delete()` dragged the `deleteStored` backend hook that every backend had to implement. This is the [drop-mutable-session-summary](2026-06-19-drop-mutable-session-summary.md) pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one.
## Decision
The methods nothing consumes are removed — from the abstract seam, the implementation, and the contract/spec suites that existed only to exercise them:
-- `SessionPersistence.has()` / `.delete()` are gone: the abstract declarations, the coordinator's `has`/`delete`/`deleteCore`, and the `PersistenceBackend.deleteStored` hook (jsonl + sqlite each implemented `deleteStored` only to satisfy the hook — those implementations went too). The backends are the [dual-backend](../../implemented/architecture/2026-06-14-session-persistence.md) design and otherwise out of scope; removing a hook they implemented for no consumer is part of removing the hook, not a backend redesign.
-- Every doc and source-comment reference is updated to the surviving four-method, `list()`-only contract — not only literal `has(`/`delete(`/`deleteStored` spellings but `{@link has}`/`{@link delete}` JSDoc links and "six public methods" counts — across the seam and backend READMEs, [docs/architecture.md](../../../architecture.md), the [session-persistence](../../implemented/architecture/2026-06-14-session-persistence.md) and [write-coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) RFCs, and the coordinator/backends JSDoc.
+- `SessionPersistence.has()` / `.delete()` are gone: the abstract declarations, the coordinator's `has`/`delete`/`deleteCore`, and the `PersistenceBackend.deleteStored` hook (jsonl + sqlite each implemented `deleteStored` only to satisfy the hook — those implementations went too). The backends are the [dual-backend](../architecture/2026-06-14-session-persistence.md) design and otherwise out of scope; removing a hook they implemented for no consumer is part of removing the hook, not a backend redesign.
+- Every doc and source-comment reference is updated to the surviving four-method, `list()`-only contract — not only literal `has(`/`delete(`/`deleteStored` spellings but `{@link has}`/`{@link delete}` JSDoc links and "six public methods" counts — across the seam and backend READMEs, [docs/architecture.md](../../../../docs/architecture.md), the [session-persistence](../architecture/2026-06-14-session-persistence.md) and [write-coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md) Agent Notes, and the coordinator/backends JSDoc.
## Alternatives considered
diff --git a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md b/.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md
similarity index 92%
rename from docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md
rename to .agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md
index 7ed7d10211..c85f644853 100644
--- a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md
+++ b/.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md
@@ -1,4 +1,4 @@
-# RFC: Keep one public stop primitive
+# Agent Note: Keep one public stop primitive
Status: implemented
@@ -34,4 +34,4 @@ A future plugin cannot abort only the current model/tool step while preserving q
## Related
-This RFC only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via `whenIdle()`. The resulting public surface is `send()`, `steer()`, `inject()`, `cancel()`, `whenIdle()`, status, options, session, and identity.
+This Agent Note only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via `whenIdle()`. The resulting public surface is `send()`, `steer()`, `inject()`, `cancel()`, `whenIdle()`, status, options, session, and identity.
diff --git a/.agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md b/.agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md
new file mode 100644
index 0000000000..910eb46e92
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md
@@ -0,0 +1,45 @@
+# Agent Note: Stop mirroring durable boundaries as agent events
+
+Status: implemented
+
+
+
+## Problem
+
+The loop records the canonical transcript in `SessionEvent` and also emitted a parallel set of live `agent/*` boundary mirror events: `agent/turn-start`, `agent/turn-end`, `agent/step-start`, and `agent/step-end`. The mirrors made consumers choose between two sources of truth for the SAME durable fact. ACP already chose the session log for the editor-facing transcript because it is the one durable, replayable record; consuming a live mirror would require reconciling its timing with the boundary already stored in that log. The stdio UI was the only production consumer that still rendered turn boundaries from the mirror events; it already rendered tool calls and results from `session/event`.
+
+This duplication is not free. Every lifecycle change had to update the session event, the mirror event, docs, invariants, tests, and snapshot expectations. The duplicate boundary events also made failure ordering subtle: a turn can be durably closed before a live `agent/turn-end` listener runs, so a post-boundary listener failure has no valid in-log position left and must be reported out of band.
+
+## Decision
+
+Make `session/event` the single live boundary/transcript stream. Consumers that render turns, tool calls, tool results, assistant messages, and durable boundaries subscribe to `session/event` and derive their UI from the same event vocabulary persistence uses.
+
+The four durable-boundary mirrors — `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end` — are removed from the agent event taxonomy. A UI that wants the agent handle at a boundary retains the live target object from `agent/created`/`agent/disposed` and compares its session directly; `dsh-ui-stdio` uses this to label the app-owned agent's `[main turn N]` header while other sessions render their durable id. The canonical record remains the event-sourced session log.
+
+The step mirrors (which had no consumer at all) were removed first, in [the event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md); that Agent Note KEPT the turn mirrors on the stated justification that the stdio UI needed the `Agent` handle at the turn boundary. This Agent Note finishes the job: `dsh-ui-stdio` is a disposable test REPL whose rendering can change freely, so "ui-stdio needs it" is not a reason to keep a mirror — it reads `session/event` and retains only its live target object.
+
+## Scope: what is and isn't removed
+
+Removed (durable-boundary mirrors — the session log is authoritative for each): `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end`.
+
+RETAINED — NOT durable-boundary mirrors, so out of scope for this decision:
+
+- `agent/steering` — not a boundary, so out of scope for THIS decision (the original proposal bundled it into the removal; that would have been scope creep here). It mirrors the durable `steering/message` control record rather than a boundary, and was removed by its own follow-up: [Remove the `agent/steering` mirror emit](2026-07-04-remove-agent-steering-mirror.md).
+- `agent/stream-chunk` — the live token stream. Out of scope for THIS decision (a mirror of the durable `assistant/chunk`, not a boundary), it was removed by its own follow-up: [Stop mirroring the token stream as an agent event](2026-07-02-remove-stream-chunk-mirror.md).
+- `agent/created`, `agent/disposed`, `agent/status`, `agent/error`, `agent/queued` — lifecycle/control events that are not transcript data. `agent/queued` in particular is an inbox acknowledgement that fires before any durable event exists (cancelled queued work may never enter the log), so it is deliberately live-only.
+
+## Alternatives considered
+
+- **Bundling `agent/steering` into the removal** — the original proposal's shape; narrowed out as scope creep: it mirrors the durable `steering/message` control record, not a boundary, and was removed by [its own later decision](2026-07-04-remove-agent-steering-mirror.md) (as was `agent/stream-chunk`, by [the stream-chunk-mirror Agent Note](2026-07-02-remove-stream-chunk-mirror.md)).
+- **Keeping the turn mirrors for the stdio UI** — [the event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md)'s original stance; rejected here because `dsh-ui-stdio` is a disposable test REPL, not a load-bearing consumer, and it renders boundaries from `session/event` plus its live target object instead.
+
+## Consequences
+
+A plugin can no longer observe turn/step boundaries from a convenient `Agent`-first event. It subscribes to `session/event` and, if it needs the live object, resolves the shared id through `ctx.agents` or retains the object it already owns. That is an acceptable trade: boundary consumers should not depend on a second event feed that can drift from the durable log.
diff --git a/.agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md b/.agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md
new file mode 100644
index 0000000000..a8a2c375b5
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md
@@ -0,0 +1,38 @@
+# Agent Note: Unify the agent id and the session id
+
+Status: implemented
+
+## Problem
+
+A live agent/session pair needs one identity for registry routing, event sourcing, and persistence. Giving the factory independent `agentId` and `sessionId` inputs would permit pairings no production path can use, while forcing every consumer to choose or translate between two names for the same lifecycle.
+
+ACP uses the same value for both identities. Stdio and hooks also operate on the session event stream and need the corresponding live agent directly; no production path reattaches one live agent object to several sessions or drives one session through several agent ids.
+
+The [agent-scope runtime](../architecture/2026-07-12-agent-scope-runtime-design.md) uses one `AgentCreationTransaction` for create and resume, and agent/session entries share the same final-entry collision rule. A second identity would not represent separate liveness, rollback, or quiescence; it would only add API and translation state around the same transaction.
+
+Session identity likewise has one home in `Session.header.id`; `Session.id` is a derived accessor rather than independent state that needs duplicate validation.
+
+## Decision
+
+An agent's registry id equals its session id. `CreateAgentOptions` accepts one `sessionId` used for both final registry entries; resume registers the agent under `resumeSessionId`; in-process subagent creation uses the child session id; and `Session.id` derives from `header.id`. A remote ACP run has no local agent/session pair: it keeps one parent-minted lifecycle id while the child server's wire-local session id remains private to ACP calls. The existing creation transaction, final-entry collision checks, and exact-entry detach semantics remain; maps and fields whose sole job was translating between local ids are gone.
+
+The config-driven path keeps `agents[].id` as a stable configuration label, not a live routing identity. An ordinary fresh start mints the combined id `${label}-session-${randomUUID()}` so durable restarts do not collide. A coupled app may pre-mint and pass an exact `sessionId`: first use creates it, while an AgentLoop remount with an already-present persistence service resumes materialized history under that same identity. `resumeSessionId` instead requires an existing persisted identity. The two exact-id inputs are mutually exclusive. Stdio uses the resume-or-create form so its config-created agent and UI share one opaque identity across loop reloads instead of guessing from a prefix. Logs may use the stable label while all live and durable lookups use the one `SessionId`.
+
+`agent/created` and `agent/disposed` remain. They are paired publication lifecycle events, not identity aliases; any later consumer-free removal needs its own proposal after a fresh search.
+
+## Alternatives considered
+
+**Keep separate routing and log identities.** A stable configured label plus a fresh durable conversation is useful, but it does not require two live identities: the label can remain configuration/display metadata while the combined per-run `SessionId` owns routing and persistence. Keeping two ids would preserve translation maps and permit impossible pairings without adding lifecycle capability.
+
+## Verification
+
+- Agent create/resume and subagent creation carry one identity, and `Session` stores it in one place.
+- The creation transaction retains final-entry collision, exact-entry detach, rollback, and quiescence coverage without identity-specific lifecycle state.
+- ACP, stdio, hooks, bash ownership, persistence, and lineage use the shared `SessionId` directly. The ACP subagent backend mints its lifecycle id in the parent namespace because a child server's returned session id is only server-local; the ACP bridge verifies exact `Agent` ownership from the forward session map; and JSON-RPC forwards only lifecycle events whose service-snapshotted `local` flag is true, obtains the delegating parent from the scoped event carrier, and keeps no child identity or lineage cache.
+- The config-driven resume-or-create policy is explicit and covered across a durable restart.
+- A production listener search kept `agent/created`/`agent/disposed` and their publication semantics.
+- Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass.
+
+## Consequences
+
+This forecloses latent multi-session-actor and session-handoff designs and makes persisted client-chosen session identity the registry identity. If separate routing identity becomes a real requirement, it needs an explicit lifecycle design rather than an unconstrained caller-supplied pair.
diff --git a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md b/.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md
similarity index 86%
rename from docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md
rename to .agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md
index d2b4c6dc4f..3753fb803a 100644
--- a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md
+++ b/.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md
@@ -1,10 +1,10 @@
-# RFC: Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin
+# Agent Note: Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin
Status: implemented
## Problem
-The filesystem capability from [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) currently makes one abstract `FileSystem` service own two different jobs:
+The filesystem capability from [filesystem-capability-seam](../architecture/2026-06-17-filesystem-capability-seam.md) currently makes one abstract `FileSystem` service own two different jobs:
1. **Provider operations** — resolving targets, stat/version metadata, text reads/streams, atomic writes, and guarded literal edits.
2. **Agent-facing policy** — line windows, literal edit semantics, and read-before-write/edit observed-state.
@@ -13,7 +13,7 @@ That makes every future backend reimplement model-facing read semantics and obse
This also creates a real UX dead-end: a windowed read records `view: partial`, and partial views cannot authorize `edit`. A model that reads lines 100-150 of a large file therefore cannot edit line 120 unless it first gets a `full` read, which may be impossible for a file past the read cap. Literal edit only needs freshness: the bytes being matched must still be from the version the model read.
-The old RFC already deferred a separate `@deepseek-ai/dsh-fs-policy` package. This RFC builds that layer and keeps `ctx.fs` close to fsspec-style storage primitives (`info`/`cat`/`open`), without turning it into full fsspec.
+The old Agent Note already deferred a separate `@deepseek-ai/dsh-fs-policy` package. This Agent Note builds that layer and keeps `ctx.fs` close to fsspec-style storage primitives (`info`/`cat`/`open`), without turning it into full fsspec.
## Decision
@@ -28,7 +28,7 @@ provider dsh-fs-local local implementation of ctx.fs
`dsh-tool-fs` keeps the same model-facing `read`/`write`/`edit` schemas. It is the executor: it injects `fs` (not a policy service) and reaches `ctx.fs` directly, owns read windowing, and dispatches the `fs/*` events so `dsh-fs-policy` can gate and record.
-This RFC decided the four-layer split, the provider contract, and the freshness policy. The tool↔policy COUPLING was then refined by [the event-gate RFC](../architecture/2026-06-26-file-context-as-event-gate.md): `dsh-fs-policy` is a gate PLUGIN that participates through the `fs/*` events rather than a `ctx.fileContext` method service, so the tool is not method-coupled to it and read windowing + the fs I/O live in `dsh-tool-fs`. This document describes that landed event-gate shape; the provider's version guard is optional (omit = unconditional bare provider).
+This Agent Note decided the four-layer split, the provider contract, and the freshness policy. The tool↔policy COUPLING was then refined by [the event-gate Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md): `dsh-fs-policy` is a gate PLUGIN that participates through the `fs/*` events rather than a `ctx.fileContext` method service, so the tool is not method-coupled to it and read windowing + the fs I/O live in `dsh-tool-fs`. This document describes that landed event-gate shape; the provider's version guard is optional (omit = unconditional bare provider).
## Provider Contract
@@ -63,11 +63,11 @@ type FsWriteIntent =
This is a *text-storage* seam, deliberately half a level above byte-level fsspec (`cat`/`open` hand back raw bytes). UTF-8 decoding, binary/NUL rejection, guarded full-file writes, and guarded literal text edits live in the provider so the policy layer never touches raw bytes, reimplements cross-chunk decoding, or separates stale checks from the mutation critical section. Model-facing concepts still stay out of the provider: no line windows, numbered lines, rendered footers, or observed-state store leak down.
-Deleted from `dsh-fs`: `readPage`, `FsExpectation`, `FsView`, `FsStateSource`, `FsReadRequest`, `FsTextLine`, line/window constants, `formatReadBody`, and the observed-state `WeakMap`. `applyEdit` is replaced by the narrower provider primitive `editText`, whose contract is version-guarded literal text mutation rather than policy-layer read authorization. The `FS_PARTIAL_OBSERVATION` code also leaves the `FsErrorCode` taxonomy: freshness authorization has no partial/full distinction, so nothing can raise it. `FsTargetKey` and `FsVersion` become branded opaque ids under the existing [branded-ids RFC](../../implemented/architecture/2026-06-20-branded-ids.md).
+Deleted from `dsh-fs`: `readPage`, `FsExpectation`, `FsView`, `FsStateSource`, `FsReadRequest`, `FsTextLine`, line/window constants, `formatReadBody`, and the observed-state `WeakMap`. `applyEdit` is replaced by the narrower provider primitive `editText`, whose contract is version-guarded literal text mutation rather than policy-layer read authorization. The `FS_PARTIAL_OBSERVATION` code also leaves the `FsErrorCode` taxonomy: freshness authorization has no partial/full distinction, so nothing can raise it. `FsTargetKey` and `FsVersion` become branded opaque ids under the existing [branded-ids Agent Note](../architecture/2026-06-20-branded-ids.md).
## Policy Contract
-`@deepseek-ai/dsh-fs-policy` is a plugin, not a service: it registers no `ctx.*` key and injects nothing. It owns the write/edit freshness policy and observed-state that do not belong on the `FileSystem` provider base class (where a sandboxed/remote backend would otherwise inherit model-facing observation policy it has no business carrying). It contributes that policy through the `fs/*` event gate the executor dispatches. (This RFC originally proposed a concrete `ctx.fileContext` service with `read`/`write`/`edit` methods; [the event-gate RFC](../architecture/2026-06-26-file-context-as-event-gate.md) refined it into the plugin described here so the tool is never method-coupled to the policy.)
+`@deepseek-ai/dsh-fs-policy` is a plugin, not a service: it registers no `ctx.*` key and injects nothing. It owns the write/edit freshness policy and observed-state that do not belong on the `FileSystem` provider base class (where a sandboxed/remote backend would otherwise inherit model-facing observation policy it has no business carrying). It contributes that policy through the `fs/*` event gate the executor dispatches. (This Agent Note originally proposed a concrete `ctx.fileContext` service with `read`/`write`/`edit` methods; [the event-gate Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md) refined it into the plugin described here so the tool is never method-coupled to the policy.)
Observed state lives here as `WeakMap>`. An entry exists iff the owner has read, written, OR edited that target (every success emits `fs/observed`), so its presence *is* the prior-observation record — there is no separate `hasRead` flag. The owner is derived structurally from the opaque event actor (`{ agent?: { session? } }`), a shape that lives in `dsh-fs-policy`, not `dsh-fs`.
@@ -97,7 +97,7 @@ Cross-process writes are best-effort freshness plus atomic replacement: `mtime:s
## Supersedes
-This RFC reverses two decisions from [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) and narrows a third:
+This Agent Note reverses two decisions from [filesystem-capability-seam](../architecture/2026-06-17-filesystem-capability-seam.md) and narrows a third:
- Read-before-write/edit policy moves out of `ctx.fs` and into the `dsh-fs-policy` plugin (on the `fs/*` event gate).
- Text reads no longer return backend-numbered line records or `full`/`partial` views; authorization is based on version freshness, so a windowed read can authorize edit when the file is unchanged.
@@ -111,12 +111,12 @@ It keeps the interface/implementation/consumer discipline, consumer-never-import
## Later extension
-The seam was later extended with direct directory listing by [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md). That follow-up is tracked separately so this RFC's acceptance criteria continue to describe the fsspec-style refit that originally shipped.
+The seam was later extended with direct directory listing by [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md). That follow-up is tracked separately so this Agent Note's acceptance criteria continue to describe the fsspec-style refit that originally shipped.
## Alternatives considered
- **Byte-level fsspec (`cat`/`open` handing back raw bytes)** — rejected: the seam is deliberately text-storage, half a level up, so UTF-8 decoding, binary/NUL rejection, and guarded text mutations live once in the provider and the policy layer never touches raw bytes or separates stale checks from the mutation critical section.
-- **A concrete `ctx.fileContext` method service** — this RFC's original policy shape; reworked by [the event-gate RFC](../architecture/2026-06-26-file-context-as-event-gate.md) into the gate plugin, so the tool is never method-coupled to the policy.
+- **A concrete `ctx.fileContext` method service** — this Agent Note's original policy shape; reworked by [the event-gate Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md) into the gate plugin, so the tool is never method-coupled to the policy.
- **Keeping `readPage` and `full`/`partial` view authorization on the provider** — the pre-refit shape the Supersedes section reverses: view completeness is not what edit safety needs, version freshness is, and the view rule made large files past the read cap impossible to edit.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md b/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
similarity index 80%
rename from docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
rename to .agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
index a272dfe0b2..c79202b95a 100644
--- a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
+++ b/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md
@@ -1,4 +1,4 @@
-# RFC: Stop mirroring the token stream as an agent event
+# Agent Note: Stop mirroring the token stream as an agent event
Status: implemented
@@ -17,7 +17,7 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
The only thing the emit added over the session event was the live `Agent` handle, and the sole consumer discarded it (its handler signature was `(_agent, _turn, _step, chunk)`).
-This is the same duplication the [boundary-mirror removal](2026-06-20-remove-agent-boundary-mirror-events.md) eliminated for turn/step boundaries: a consumer had two sources of truth for one durable fact, and every change had to touch both. That RFC deferred the chunk stream ("`assistant/chunk` persistence remains load-bearing, so the chunk stream could later be evaluated as a mirror, but that is a separate decision") rather than bundling it in. This RFC is that separate decision.
+This is the same duplication the [boundary-mirror removal](2026-06-20-remove-agent-boundary-mirror-events.md) eliminated for turn/step boundaries: a consumer had two sources of truth for one durable fact, and every change had to touch both. That Agent Note deferred the chunk stream ("`assistant/chunk` persistence remains load-bearing, so the chunk stream could later be evaluated as a mirror, but that is a separate decision") rather than bundling it in. This Agent Note is that separate decision.
The premise the deferral hinged on is settled: chunk persistence is authoritative and staying. The proposal to stop persisting chunks and keep only a transient live stream event was [rejected](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) — high-fidelity replay, partial failed streams, and snapshot replay all depend on the persisted `assistant/chunk` feed. So `assistant/chunk` on `session/event` is the durable, load-bearing token stream, and `agent/stream-chunk` is a pure redundant mirror of it.
@@ -32,7 +32,7 @@ Remove `agent/stream-chunk` from the agent event taxonomy. The token stream is r
Removed: `agent/stream-chunk`.
Not touched:
-- `assistant/chunk` (the durable session event) — the authoritative token stream, kept exactly as-is. This RFC removes the LIVE MIRROR, not the persistence (the persistence-removal proposal was separately rejected — see above).
+- `assistant/chunk` (the durable session event) — the authoritative token stream, kept exactly as-is. This Agent Note removes the LIVE MIRROR, not the persistence (the persistence-removal proposal was separately rejected — see above).
- `agent/steering` — not touched by THIS decision (a control signal, not the token stream). Its durable twin is `steering/message`, and the mirror emit was removed by its own follow-up: [Remove the `agent/steering` mirror emit](2026-07-04-remove-agent-steering-mirror.md).
- `agent/status`, `agent/error`, `agent/created`/`agent/disposed`, `agent/queued`, `agent/session-start` — lifecycle/control events that are not transcript data and have no durable duplicate.
@@ -42,4 +42,4 @@ Not touched:
## Consequences
-A plugin can no longer observe token deltas from an `Agent`-first event. It subscribes to `session/event` and filters `assistant/chunk` (the `Agent` handle, if needed, is recovered from a session-id→agent map built from `agent/created`/`agent/disposed`, exactly as boundary consumers already do). No production consumer needed the live `Agent` at chunk time; this is the same acceptable trade the boundary-mirror removal made.
+A plugin can no longer observe token deltas from an `Agent`-first event. It subscribes to `session/event`, filters `assistant/chunk`, and looks up the corresponding live handle directly with `ctx.agents.get(session.id)` when needed. No production consumer needed the live `Agent` at chunk time; this is the same acceptable trade the boundary-mirror removal made.
diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.md b/.agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md
similarity index 87%
rename from docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.md
rename to .agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md
index 73645e8fda..df63f2a78f 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md
@@ -1,4 +1,4 @@
-# RFC: Drop the `image` content block until a path can honor it
+# Agent Note: Drop the `image` content block until a path can honor it
Status: implemented
@@ -20,7 +20,7 @@ The recorded fallback, had review landed on keeping the slot: keep `ImageBlock`
## Verification
-No harness `ImageBlock` is constructed outside RFC records. ACP's independent inbound-image rejection remains tested, while adapter, codec, and compaction default branches are covered with plugin-defined block types.
+No harness `ImageBlock` is constructed outside Agent Note records. ACP's independent inbound-image rejection remains tested, while adapter, codec, and compaction default branches are covered with plugin-defined block types.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md b/.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
similarity index 71%
rename from docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
rename to .agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
index 95a5a487b5..dadab43f76 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md
@@ -1,4 +1,4 @@
-# RFC: Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path
+# Agent Note: Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path
Status: implemented
@@ -13,10 +13,10 @@ Both knobs were adapter-symmetric, so removal shed them from both twins together
## Decision
-- `prefill` is removed from `GenerateOptions`, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in [core.md](../../../core-data-structures/core.md), and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance ([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md)) states the rule generically — a `GenerateOptions` field your provider cannot honor throws `LlmError(..., 'UNSUPPORTED')` — instead of using prefill as the example. The [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record prefill as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md).
+- `prefill` is removed from `GenerateOptions`, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in [core.md](../../../../docs/core-data-structures/core.md), and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance ([adding-an-llm-adapter.md](../../../../docs/cookbook/adding-an-llm-adapter.md)) states the rule generically — a `GenerateOptions` field your provider cannot honor throws `LlmError(..., 'UNSUPPORTED')` — instead of using prefill as the example. The [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record prefill as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md).
- `strict` is removed from `ToolSchema`, `DefineToolOptions`, `defineTool`, the `schemas()` allowlist, the deepseek serializer branch and its wire-type field, and the tool-catalog renderer's `Strict:` row. The pi-ai payload fixup is simplified to the unconditional scrub of pi-ai's own per-tool strict default (pi-ai stamps `strict: false` on every serialized tool; the hand-rolled twin sends no such field, so the scrub survives for wire parity, pinned by its serializer test). The setter tests and the core.md paste line are gone; both `GenerateOptions` and `ToolSchema` keep their rows in `scripts/type-equiv.manifest.json`, since each type survives minus a field.
-This RFC deliberately does NOT touch `temperature`, `stop`, or `maxTokens`: those are honored end-to-end by both adapters and are the natural first targets of a request-mutating hook plugin on `agent/request`.
+This Agent Note deliberately does NOT touch `temperature`, `stop`, or `maxTokens`: those are honored end-to-end by both adapters and are the natural first targets of a request-mutating hook plugin on `agent/request`.
## Alternatives considered
@@ -26,7 +26,7 @@ This RFC deliberately does NOT touch `temperature`, `stop`, or `maxTokens`: thos
## Verification
-`rg prefill` returns only RFC records (this one and the [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s producer-gated consequence); a tool-schema-scoped `rg strict` returns only this RFC, the surviving pi-ai scrub, and unrelated prose such as `strictEqual`. Both adapters' contract tests pass without the guards, and the pi-ai fixup still scrubs the library's strict default — wire parity pinned by its serializer tests.
+`rg prefill` returns only Agent Note records (this one and the [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s producer-gated consequence); a tool-schema-scoped `rg strict` returns only this Agent Note, the surviving pi-ai scrub, and unrelated prose such as `strictEqual`. Both adapters' contract tests pass without the guards, and the pi-ai fixup still scrubs the library's strict default — wire parity pinned by its serializer tests.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md b/.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
similarity index 52%
rename from docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
rename to .agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
index 35868be2fd..80faa2ac07 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md
@@ -1,4 +1,4 @@
-# RFC: Drop the unconsumed web observation surface — the `providers-change` event and the status methods
+# Agent Note: Drop the unconsumed web observation surface — the `providers-change` event and the status methods
Status: implemented
@@ -7,11 +7,11 @@ Status: implemented
`WebService` exposes an observation surface no production code observes:
- **`web/providers-change`** (`packages/web/web/src/index.ts`) is declared and emitted on every provider registration and disposal, and each registration effect's rollback yield is ordered BEFORE the emit solely so a throwing change listener unwinds the registration. No listener exists outside the package's own two unit tests (one of which exists to pin that rollback ordering).
-- **`searchStatus()` / `fetchStatus()` and the `WebCapabilityStatus` union** (same package) have zero production callers: `dsh-tool-web` executes directly through `ctx.web.search()`/`fetch()` and surfaces unavailability as the structured `WebError` codes the seam throws at execution time (`packages/web/tool-web/src/search.ts`, `packages/web/tool-web/src/fetch.ts`); the only status callers are the web packages' own tests. The prose in `packages/web/tool-web/README.md` and [architecture.md](../../../architecture.md) claims the tool "reads only the aggregated `searchStatus()`/`fetchStatus()`" — drift that survives only because nothing checks prose against call sites.
+- **`searchStatus()` / `fetchStatus()` and the `WebCapabilityStatus` union** (same package) have zero production callers: `dsh-tool-web` executes directly through `ctx.web.search()`/`fetch()` and surfaces unavailability as the structured `WebError` codes the seam throws at execution time (`packages/web/tool-web/src/search.ts`, `packages/web/tool-web/src/fetch.ts`); the only status callers are the web packages' own tests. The prose in `packages/web/tool-web/README.md` and [architecture.md](../../../../docs/architecture.md) claims the tool "reads only the aggregated `searchStatus()`/`fetchStatus()`" — drift that survives only because nothing checks prose against call sites.
The seam's own design starves both surfaces of consumers: tool registration follows product ENABLEMENT, not provider availability (`packages/web/tool-web/src/index.ts`), and provider selection resolves at execution time, never cached — so there is no cache to invalidate, no registration set to recompute, and no caller that needs an availability probe distinct from executing and routing the structured error. HMR cleanup is carried by the effect disposers themselves.
-This mirrors [drop the unconsumed `llm/adapter-change` event](../../implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md), which removed the same notification shape, the same rollback-before-emit machinery, and the same listener-throw test from `LlmService`. That RFC's keep/cut criterion — keep `tools/change` for its plausible user-facing tool-list consumer, cut the boot-time backend-registry signal — puts a web-provider registry squarely on the cut side; the status methods are the same judgment applied to a pull surface instead of a push one.
+This mirrors [drop the unconsumed `llm/adapter-change` event](2026-06-20-drop-unconsumed-llm-adapter-change-event.md), which removed the same notification shape, the same rollback-before-emit machinery, and the same listener-throw test from `LlmService`. That Agent Note's keep/cut criterion — keep `tools/change` for its plausible user-facing tool-list consumer, cut the boot-time backend-registry signal — puts a web-provider registry squarely on the cut side; the status methods are the same judgment applied to a pull surface instead of a push one.
## Decision
@@ -21,11 +21,11 @@ Remove the registry-change event, aggregated status methods and type, and their
### Why not keep it?
-The web seam RFC specified both deliberately — the event as a minimal HMR-visibility signal, the status methods as the tool's aggregated diagnostics — and a future provider-status panel is imaginable. But the same RFC's other choices starved them: derived-on-call selection and enablement-based registration leave no consumer that CAN need either, the shipped tool demonstrates the real pattern (execute and route the structured error), and the drifted README sentence shows the promised consumer never materialized. Per AGENTS.md "RFCs are proposals, not golden truth", these are the parts of that proposal the code has since shown to over-reach; a future observer reintroduces the smallest signal or query it actually consumes, shaped by that consumer.
+The web seam Agent Note specified both deliberately — the event as a minimal HMR-visibility signal, the status methods as the tool's aggregated diagnostics — and a future provider-status panel is imaginable. But the same Agent Note's other choices starved them: derived-on-call selection and enablement-based registration leave no consumer that CAN need either, the shipped tool demonstrates the real pattern (execute and route the structured error), and the drifted README sentence shows the promised consumer never materialized. Per AGENTS.md "Agent Notes are proposals, not golden truth", these are the parts of that proposal the code has since shown to over-reach; a future observer reintroduces the smallest signal or query it actually consumes, shaped by that consumer.
## Verification
-No `providers-change`, `searchStatus`, `fetchStatus`, or `WebCapabilityStatus` spelling survives outside RFC history; the catalog is fresh (`verify-cordis-catalog` green); registration/disposal HMR-safety tests prove cleanup through execution behavior; and the tool-web README plus the architecture paragraph describe the execution-time error-routing contract the tool actually has.
+No `providers-change`, `searchStatus`, `fetchStatus`, or `WebCapabilityStatus` spelling survives outside Agent Note history; the catalog is fresh (`verify-cordis-catalog` green); registration/disposal HMR-safety tests prove cleanup through execution behavior; and the tool-web README plus the architecture paragraph describe the execution-time error-routing contract the tool actually has.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md b/.agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
similarity index 84%
rename from docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
rename to .agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
index d3775754b9..c44201b4e6 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md
@@ -1,7 +1,9 @@
-# RFC: Fold the stdio UI helper into the stdio app
+# Agent Note: Fold the stdio UI helper into the stdio app
Status: implemented
+The later [redundant-agent removal](2026-07-20-remove-stdio-and-echo-agents.md) supersedes this package-placement decision and removes the folded package, app, and line-oriented surface entirely.
+
## Problem
The readline UI was a whole package (`@deepseek-ai/dsh-ui-stdio` under `packages/support/`) whose only runtime importer was the app package `@deepseek-ai/dsh-stdio-demo`. The examples reach the readline UI by loading the app, never by composing the helper themselves; every other repo reference was mechanical or descriptive surface that existed BECAUSE the package boundary existed — manifest and tsconfig entries, generated module-graph rows, dependency-graph and README rows, and doc comments naming the package. The ui group README recorded the support placement rationale ("exists chiefly for the examples and the coverage gate — `ui/` is reserved for surfaces shipped as product"), which left a standing tension: a shipped product app depending on a support package documented as NOT product surface.
@@ -10,7 +12,7 @@ The boundary bought package metadata, workspace and tsconfig references, module-
## Decision
-The helper lives in `@deepseek-ai/dsh-stdio` as the terminal-channel plugin (`packages/ui/stdio/src/index.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio/tests/stdio.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/coding-agent` keep proving the composed tree boots through the real Loader (the stdio package's plugin-shape unit suite pins the explicit `unwrapExports` assertion, since a bundle without `inject` would boot past a stray default rather than crash).
+The helper lives in `@deepseek-ai/dsh-stdio` as the terminal-channel plugin (`packages/ui/stdio/src/index.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio/tests/stdio.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/repl-agent` keep proving the composed tree boots through the real Loader (the stdio package's plugin-shape unit suite pins the explicit `unwrapExports` assertion, since a bundle without `inject` would boot past a stray default rather than crash).
The `packages/support/ui-stdio` package is gone: manifest, tsconfig references, module-graph rows, and README rows deleted; the doc comments that named the package (the example e2e module docs, `packages/README.md`, the support and todo READMEs, [the ui group README](../../../../packages/ui/README.md)) describe the in-package module.
diff --git a/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md b/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
new file mode 100644
index 0000000000..aa61e859d2
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md
@@ -0,0 +1,31 @@
+# Agent Note: Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)
+
+Status: implemented
+
+## Problem
+
+The merge-extensible vocabulary maps are designed to grow by declaration merging, and the codebase already states the admission policy on `TurnEndReasonMap` (`packages/core/session/src/types.ts`): a variant like `refusal` is "deliberately omitted until" an adapter or loop first emits it. Three declared vocabulary items violated that policy — each had no producer and no consumer, and two had not even a test:
+
+- **`CacheHint` and its `cache?: CacheHint` block fields** on `TextBlock`/`ToolResultBlock` (`packages/llm/llm/src/types.ts`; the image block carried a third such field, which left with it — see [the drop-image Agent Note](2026-07-04-drop-image-content-block.md)). Nothing constructed a block with `cache:` anywhere — src, tests, and doc pastes all came up empty — and neither adapter read `.cache`: DeepSeek prompt caching is automatic, so the adapters map `prompt_cache_hit_tokens` OUT of responses without ever sending a hint IN. This was Anthropic-style `cache_control` surface with no provider that could honor it.
+- **`MessageSourceMap.agent`** (`{ kind: 'agent'; agentId: string }`, same file). Zero constructors, tests included. Its intended producer shipped without it: the subagent backends send the parent's prompt to the child with no `source`, so it logs as `{ kind: 'user' }`, and the generic envelope renderer interpolates `source.kind` without ever routing on it.
+- **`TurnTriggerMap.continuation`** (`packages/core/session/src/types.ts`). The loop structurally cannot emit it — continuation happens *within* a turn as further steps, never as a new turn — and it constructs only `message` and `injection` triggers. The only writer was one hand-built test fixture needing an arbitrary non-message trigger (`packages/support/llm-replay/tests/llm-replay.spec.ts`), which an `injection` trigger serves equally; the only production trigger reader, the ACP bridge, filters on `kind === 'message'`.
+
+## Decision
+
+`CacheHint`, its `cache?` block fields, the `agent` message-source variant, and the `continuation` turn-trigger variant are deleted: the shipped vocabulary carries none of them. The llm-replay fixture uses an `injection` trigger (any non-`message` trigger serves its purpose). The type-equiv pastes in [core.md](../../../../docs/core-data-structures/core.md) and [session.md](../../../../docs/core-data-structures/session.md) match the pruned maps — both symbols keep their rows in `scripts/type-equiv.manifest.json`, since each map survives minus a member — and the [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record cache hints as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md).
+
+Each variant returns the day it gains a real producer, exactly as the maps are designed to grow: a caching feature re-adds `cache` together with the adapter that transmits it; subagent attribution re-adds `agent` together with the backend that stamps it and a consumer that routes on it; an auto-continue feature that genuinely starts new turns re-adds `continuation` with the plugin that emits it.
+
+## Alternatives considered
+
+### Why not keep them?
+
+The [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md) listed "cache hints … have a home" as a design consequence, and reserved slots do advertise intent. But an empty slot is contract surface every implementation and consumer must consider (must my adapter honor `cache`? must my renderer route `agent` sources?), and the sibling map's own JSDoc already rejects reservation-without-emitter — `refusal` and `max_turn_requests` are named as variants to add *when something first emits them*, not declared in advance. Holding already-declared dead variants to the same standard makes the vocabulary mean something: if it is in the map, something produces it.
+
+## Verification
+
+`rg` for `CacheHint`, the `agent` message-source spelling, and the `continuation` trigger spelling returns only Agent Note records (this one, and [the drop-image Agent Note](2026-07-04-drop-image-content-block.md)'s account of the image block's own `cache` field); the llm-replay fixture asserts the same replay behavior with an `injection` trigger; the core-data-structures pastes and the type-equiv manifest are in sync.
+
+## Consequences
+
+Nothing operational changed — nothing could construct these values. The mirror-event removals ([the boundary-mirror Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md), [the stream-chunk Agent Note](2026-07-02-remove-stream-chunk-mirror.md)) touch only transient `agent/*` events, never the durable vocabulary, so there is no collision. Elsewhere the admission policy already holds: `rejected`, `prompt/blocked`, and `hook/invoked`/`hook/result` each have live producers — this Agent Note extends the same bar to the three variants that lacked one. The image block's own `cache?` field belongs to [the drop-image Agent Note](2026-07-04-drop-image-content-block.md), which removed it together with the block; this Agent Note covers the two fields on the block types that remain.
diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md b/.agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
similarity index 93%
rename from docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
rename to .agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
index 0fc11a1c17..97652ef50c 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md
@@ -1,4 +1,4 @@
-# RFC: Prune write-only fields and a dead routing knob from the fs seam
+# Agent Note: Prune write-only fields and a dead routing knob from the fs seam
Status: implemented
@@ -13,7 +13,7 @@ The [fs seam split](2026-06-26-fsspec-style-fs-seam.md) moved read routing and p
## Decision
-Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. The [filesystem.md](../../../core-data-structures/filesystem.md) pastes, `packages/fs/fs/README.md`, and the test fakes that had to fabricate the removed fields shrink with the types.
+Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. The [filesystem.md](../../../../docs/core-data-structures/filesystem.md) pastes, `packages/fs/fs/README.md`, and the test fakes that had to fabricate the removed fields shrink with the types.
## Alternatives considered
@@ -23,7 +23,7 @@ A future permission/containment layer might want the pre-resolution path for err
## Verification
-The removed surfaces are gone — `STREAM_MIN_SIZE`/`streamMinSize` in `dsh-fs-local`, `FsTarget.inputPath`, `FsEditOutcome.replacements`/`.replaceAll`, and `FileReadOutcome.limit`/`.version` — while the request-side `replaceAll` (`FsEditRequest`) and the version fields on the other outcome types are untouched; the test fakes shrank with the types. `formatEditOutput`'s emitted text is unchanged for both `replace_all` branches, so no snapshot golden churned.
+The removed surfaces are gone — `STREAM_MIN_SIZE`/`streamMinSize` in `dsh-fs-local`, `FsTarget.inputPath`, `FsEditOutcome.replacements`/`.replaceAll`, and `FileReadOutcome.limit`/`.version` — while the request-side `replaceAll` (`FsEditRequest`) and the version fields on the other outcome types are untouched; the test fakes shrank with the types. `formatEditOutput`'s emitted text is unchanged for both `replace_all` branches, so no snapshot expected output churned.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md b/.agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
similarity index 58%
rename from docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
rename to .agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
index a0a383b255..ebfc774792 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
@@ -1,4 +1,4 @@
-# RFC: Remove the `agent/steering` mirror emit
+# Agent Note: Remove the `agent/steering` mirror emit
Status: implemented
@@ -8,23 +8,23 @@ Status: implemented
`agent/steering` duplicated the immediately preceding durable `steering/message` with the same payload. `agent/queued` remains the live-only signal because it fires before persistence and covers work that may be cancelled before entering the log.
-Steering carries real production traffic — the hook bridges' turn-continuation decisions inject their reasons through `inbox.steer()`, landing as durable `steering/message` events that the hook-matrix goldens pin — and every one of those consumers observes the durable event. Nothing observed the mirror.
+Steering carries real production traffic — the hook bridges' turn-continuation decisions inject their reasons through `inbox.steer()`, landing as durable `steering/message` events that the hook-matrix expected outputs pin — and every one of those consumers observes the durable event. Nothing observed the mirror.
## Decision
-`agent/steering` is removed from the agent event taxonomy: the declaration in `packages/core/agent/src/types.ts` (and its mention in the live-events JSDoc list there), the emit in `drainSteering` (whose then-unused `ctx` parameter went with it), the row in `packages/core/agent/README.md`, and the emit line in the loop-pseudocode blocks (the `packages/core/agent-loop/src/loop.ts` module doc and [architecture.md](../../../architecture.md)); the cordis catalog is regenerated without it. The one regression test pins source preservation on the durable `steering/message` event — the fact it pins lives on the log.
+`agent/steering` is removed from the agent event taxonomy: the declaration in `packages/core/agent/src/types.ts` (and its mention in the live-events JSDoc list there), the emit in `drainSteering` (whose then-unused `ctx` parameter went with it), the row in `packages/core/agent/README.md`, and the emit line in the loop-pseudocode blocks (the `packages/core/agent-loop/src/loop.ts` module doc and [architecture.md](../../../../docs/architecture.md)); the cordis catalog is regenerated without it. The one regression test pins source preservation on the durable `steering/message` event — the fact it pins lives on the log.
-Three implemented RFCs stated the retention, and each is amended per [implemented/AGENTS.md](../AGENTS.md) to point here as the record of the removal: the [boundary RFC](2026-06-20-remove-agent-boundary-mirror-events.md)'s retained-list entry, the [stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md)'s scope clause, and the [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md)'s transient-emit enumeration.
+Three implemented Agent Notes stated the retention, and each is amended per [implemented/AGENTS.md](../AGENTS.md) to point here as the record of the removal: the [boundary Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md)'s retained-list entry, the [stream-chunk Agent Note](2026-07-02-remove-stream-chunk-mirror.md)'s scope clause, and the [event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md)'s transient-emit enumeration.
## Alternatives considered
### Why not keep it?
-"It is a control signal, not a boundary" — but the taxonomy's operative distinction is mirrored-vs-live-only, not control-vs-boundary, and this event mirrored. A consumer that wants enqueue-time notification has `agent/queued` (with its steering flag); a consumer that wants drain-time notification is by definition asking for the moment `steering/message` is appended, which `session/event` delivers with the same payload plus durability. The rejected [retire-mid-turn-steering RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) defended the steering *capability* — `steer()`, the durable event, continuation forcing — all of which this removal keeps untouched.
+"It is a control signal, not a boundary" — but the taxonomy's operative distinction is mirrored-vs-live-only, not control-vs-boundary, and this event mirrored. A consumer that wants enqueue-time notification has `agent/queued` (with its steering flag); a consumer that wants drain-time notification is by definition asking for the moment `steering/message` is appended, which `session/event` delivers with the same payload plus durability. The rejected [retire-mid-turn-steering Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) defended the steering *capability* — `steer()`, the durable event, continuation forcing — all of which this removal keeps untouched.
## Verification
-The `agent/steering` spelling survives only in RFC prose (this RFC, the three amended RFCs above, and the frozen [rejected steering-capability RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md), whose text records the proposal it declined); the catalog is regenerated; the retargeted test pins source preservation on `steering/message`.
+The `agent/steering` spelling survives only in Agent Note prose (this Agent Note, the three amended Agent Notes above, and the frozen [rejected steering-capability Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md), whose text records the proposal it declined); the catalog is regenerated; the retargeted test pins source preservation on `steering/message`.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md b/.agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
similarity index 76%
rename from docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
rename to .agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
index 79abfab2a0..f054f168a2 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md
@@ -1,4 +1,4 @@
-# RFC: Share the app bins' boot glue instead of maintaining twin copies
+# Agent Note: Share the app bins' boot glue instead of maintaining twin copies
Status: implemented
@@ -10,13 +10,13 @@ The stdio and ACP bins duplicated environment loading, fail-loud handling, entry
The helpers live once, in [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) (`packages/ui/app-boot`, in the `ui` group because the bins are published artifacts whose runtime dependency must itself be published, not `support/`): `resolveConfigPath` (snapshot-aware, the single path resolver for both bins), `loadEnv`, `installFailLoud`, `assertEntriesLoaded`, and `boot`, each parameterized by the bin's diagnostic prefix and injectable at its side-effect seams (the warn sink, the process slice) so the unit suite covers every branch — including `boot()` driven in-process against the real Loader with relative-specifier configs, both the settled-tree happy path and the fiber-less-entry rejection. The package carries the per-file 100% coverage gate; the loader-failure lore has one home.
-Each `bin.ts` is a thin self-executing composition over the shared helpers plus its app-specific lifecycle (the ACP bin: replay-mode env skipping and the stdin-EOF dispose; the stdio bin: nothing extra). The bins stay coverage-excluded and export nothing; the published-artifact guards are unchanged — the built-bin smokes still run each bin under plain node in a node_modules-shaped temp dir (now symlinking `ui/app-boot` too) and still assert the missing-config non-zero exit, per the "real entry path means the published artifact" defensive pattern. The [extract-example-app-packages RFC](../architecture/2026-06-20-extract-example-app-packages.md)'s bin-ownership facts are amended accordingly.
+Each `bin.ts` is a thin self-executing composition over the shared helpers plus its app-specific lifecycle (the ACP bin: replay-mode env skipping and the stdin-EOF dispose; the stdio bin: nothing extra). The bins stay coverage-excluded and export nothing; the published-artifact guards are unchanged — the built-bin smokes still run each bin under plain node in a node_modules-shaped temp dir (now symlinking `ui/app-boot` too) and still assert the missing-config non-zero exit, per the "real entry path means the published artifact" defensive pattern. The [extract-example-app-packages Agent Note](../architecture/2026-06-20-extract-example-app-packages.md)'s bin-ownership facts are amended accordingly.
## Alternatives considered
### Why not keep the duplication?
-The bins were framed as independently-owned published artifacts, and a new package carries fixed overhead (manifest, README, tsconfig reference, publint surface) comparable to the deduplicated line count. But app-vs-app sharing was never weighed by the RFC that created the bins — it consolidated three example `start.ts` copies INTO the bins and stopped there; the drift was observed fact; and the coverage-gap argument is independent of the dedup argument: this was the only nontrivial runtime logic in the repo exempt from the per-file 100% gate. The recorded fallback (extracting only the pure logic into per-app modules) would have ended the exemption but kept two homes for the lore.
+The bins were framed as independently-owned published artifacts, and a new package carries fixed overhead (manifest, README, tsconfig reference, publint surface) comparable to the deduplicated line count. But app-vs-app sharing was never weighed by the Agent Note that created the bins — it consolidated three example `start.ts` copies INTO the bins and stopped there; the drift was observed fact; and the coverage-gap argument is independent of the dedup argument: this was the only nontrivial runtime logic in the repo exempt from the per-file 100% gate. The recorded fallback (extracting only the pure logic into per-app modules) would have ended the exemption but kept two homes for the lore.
## Consequences
diff --git a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md b/.agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md
similarity index 81%
rename from docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md
rename to .agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md
index 72172144d0..82830b1366 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md
@@ -1,12 +1,12 @@
-# RFC: Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics
+# Agent Note: Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics
Status: implemented
## Problem
-Four pieces of the `dsh-hook-protocol`/bridge contract missed the discipline the [subagent-observe-enrich RFC](../feature/2026-06-30-subagent-observe-enrich.md) records — it dropped an `agentType` lifecycle field for lacking a consumer, and these failed the same test:
+Four pieces of the `dsh-hook-protocol`/bridge contract missed the discipline the [subagent-observe-enrich Agent Note](../feature/2026-06-30-subagent-observe-enrich.md) records — it dropped an `agentType` lifecycle field for lacking a consumer, and these failed the same test:
-1. **`HookDialect`'s `'native'` variant** (`packages/hooks/hook-protocol/src/types.ts`) had zero producers — the bridges stamp `'claude'` and `'codex'`; the only `'native'` constructor anywhere was the lib's own unit test. The field's own JSDoc defines `dialect` as "the bridge that ran it", and native is not a bridge: the [interception-seams RFC](../feature/2026-06-30-interception-seams.md) records that native hooks are not a package and that "a native plugin can already use the typed Decisions" without the durable hook log, and the flagship native-plugin worked example asserts exactly that (no `hook/*` events at all).
+1. **`HookDialect`'s `'native'` variant** (`packages/hooks/hook-protocol/src/types.ts`) had zero producers — the bridges stamp `'claude'` and `'codex'`; the only `'native'` constructor anywhere was the lib's own unit test. The field's own JSDoc defines `dialect` as "the bridge that ran it", and native is not a bridge: the [interception-seams Agent Note](../feature/2026-06-30-interception-seams.md) records that native hooks are not a package and that "a native plugin can already use the typed Decisions" without the durable hook log, and the flagship native-plugin worked example asserts exactly that (no `hook/*` events at all).
2. **`HookOutput.suppressOutput`** (same file) was parsed by the codec and discarded on every path: no bridge branch, no merge fold, no warn, no deferred-list row — uniquely among its parsed-but-unhonored siblings, each of which carries a stated deferral (`updatedInput` → a logged warn plus the [pre-tool-input-rewrite proposal](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md); `systemMessage` → a logged warn plus a README deferred row; `continue`/`stopReason` → a `TODO(hook-continue-false)` anchor plus the `'stop'` decision record). Structurally there is nothing to suppress: hook stdout never enters any transcript (context flows only via `additionalContext`; the log records only `decision`/`stderrSummary`), so a hook author setting `suppressOutput: true` got silent nothing with no warn.
3. **`defaultTimeoutMs` was double-defaulted in both bridge configs with a floating literal** — a schema `.default(600_000)` AND a `?? 600_000` fallback (`packages/hooks/hooks-claude/src/index.ts`, `packages/hooks/hooks-codex/src/index.ts`), two homes per bridge for one protocol-level constant, so the bridges could silently drift apart on the shared default. *The proposal's original remedy — delete the knob outright — was overtaken by the no-hardcoded-tunables audit, which kept the knob as the explicit bridge-owned config (and added `stderrSummaryMaxChars` beside it); what remained to fix was the literal's home.*
4. **The `hook/result` semantics lived in the bridges, twice, not in the lib that owns the event.** `summarize()` — the stderr truncation rule — was byte-identical in `packages/hooks/hooks-claude/src/index.ts` and `packages/hooks/hooks-codex/src/index.ts`, and so was the decision-string rule `output.decision ?? (output.continue === false ? 'stop' : 'pass')`; yet `dsh-hook-protocol` declared `hook/result`, documented `stderrSummary` as "truncated" without owning the truncation, and documented the decision values without owning the mapping. If one bridge drifted (a different cap, a different fallback), the shared durable event's semantics would fork silently.
@@ -27,4 +27,4 @@ Unsupported vocabulary can return when a real consumer exists. `durationMs` rema
## Consequences
-The `dialect`, `suppressOutput`, tunables, and semantics changes are invisible on the wire and in the goldens. The cost was churn in `dsh-hook-protocol` and both bridges — cheap under the pre-release stance, and cheaper than letting two copies of a durable event's semantics age apart.
+The `dialect`, `suppressOutput`, tunables, and semantics changes are invisible on the wire and in the expected outputs. The cost was churn in `dsh-hook-protocol` and both bridges — cheap under the pre-release stance, and cheaper than letting two copies of a durable event's semantics age apart.
diff --git a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md b/.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
similarity index 83%
rename from docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
rename to .agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
index 191ddd833f..7d1065d250 100644
--- a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
+++ b/.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
@@ -1,4 +1,4 @@
-# RFC: Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback
+# Agent Note: Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
Two pieces of `dsh-acp` surface were unreachable from any shipped configuration:
-1. **`AcpConfig.agentName` / `agentVersion`** (`packages/ui/acp/src/index.ts`). The shipped app package hands the bridge only `{ model }` (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot golden — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
+1. **`AcpConfig.agentName` / `agentVersion`** (`packages/ui/acp/src/index.ts`). The shipped app package hands the bridge only `{ model }` (`packages/examples/acp-demo/src/index.ts`), so no leaf `cordis.yml` — the only production config surface — could set the knobs at all; they were settable solely by direct-mounting the bridge, which only a unit test did. Every snapshot expected output — the hook-matrix scenarios included — pins the schema defaults (`deepseek-harness-acp` / `0.0.1`). The pair also carried a live `TODO(double-default)`: the literals existed twice (schema `.default(...)` plus `??` fallbacks), with the TODO asking to pick one home.
2. **The `toolKindFor` name heuristic** (same file) special-cased `bash*`/`read*`/`write`/`edit*` tool names in the generic-fallback path. Since the [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md), every first-party tool those arms matched ships its own `presentCall` carrying its kind, and the presenter-less production tools (`subagent`, `subagent_fork`) fell through to `other` anyway. The arms were production-reachable only when a tool declined to present its own call — a `presentCall` that THROWS (the containment fallback), or model arguments that fail the tool's schema so `defineTool`'s `presentCall` wrapper returns `undefined` (e.g. a `bash` call missing the required `description`) — and the bridge's own module doc states the design rule the heuristic violated: "the bridge never special-cases tool names".
## Decision
diff --git a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md b/.agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
similarity index 91%
rename from docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
rename to .agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
index 0907a63417..a719e20f1e 100644
--- a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
+++ b/.agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md
@@ -1,4 +1,4 @@
-# RFC: Drop unconsumed skill provider events
+# Agent Note: Drop unconsumed skill provider events
Status: implemented
@@ -14,7 +14,7 @@ Skill discovery reads the current provider map on demand, provider registration
The skill registry declares and emits no provider-membership events. Provider registration and disposal remain direct effect-owned state changes that synchronously invalidate completed catalogs; lookup and discovery read the current provider map on demand. Tests observe cleanup through provider lookup and collected output rather than lifecycle notifications.
-The generated event catalog, API catalog, and producer/consumer matrix omit the deleted notifications. The skill-system RFC and package documentation describe registration through its direct effect-owned state and cache-invalidation contract.
+The generated event catalog, API catalog, and producer/consumer matrix omit the deleted notifications. The skill-system Agent Note and package documentation describe registration through its direct effect-owned state and cache-invalidation contract.
## Alternatives considered
diff --git a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md b/.agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
similarity index 98%
rename from docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
rename to .agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
index 8ece4214b7..68ced83876 100644
--- a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
+++ b/.agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md
@@ -1,4 +1,4 @@
-# RFC: Prune unused web seam fields
+# Agent Note: Prune unused web seam fields
Status: implemented
diff --git a/.agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md b/.agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md
new file mode 100644
index 0000000000..97a89e5be7
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md
@@ -0,0 +1,33 @@
+# Agent Note: Simplify session-log representation
+
+Status: implemented
+
+## Problem
+
+The session log maintains two representations that cost more machinery than their consumers require: a pseudo-linked surface and custom request-header deltas.
+
+`SurfaceManager` stores the same order in an array, a seq map, and mutable `prev`/`next` links. Production never reads either link: compact's tool-pairing balance answers from per-cut balances cached in surface order. Replacement already uses `indexOf`, so the links do not make its dominant operation constant-time. A seq array with linear replacement lookup has the same asymptotic replacement cost and one representation to validate.
+
+The request-header subsystem implements a custom system/tool delta codec and transmission-decision layer even though its contract says deltas are an encoding optimization, not a reconstructability requirement. Retaining the initial/resume full snapshot at each loop-instance boundary, then writing a canonical full `request/header` whenever that instance's assembled header changes, preserves replay while deleting `SystemDelta`, `ToolsDelta`, round-trip fallback, and the durable `request/header-delta` variant. Codec-only vocabulary disappears with the codec, not because its individual arms were invalid.
+
+The implementation retains append and replacement `sourceEventSeqs`, crash-repair provenance, and all `SessionStartSource` variants because those fields have an audit/interception role that zero current readers does not overturn.
+
+## Decision
+
+`SurfaceManager.nodes` is a `readonly number[]` of event sequences; the public `SurfaceNode` shape, node links, and seq-to-node map are removed. The internal replace-generation signal remains. The complete `foldSurface()` read used by session-query returns the same number-array representation plus replacement metadata without making the incremental manager retain history. Tool-pairing balance and compaction use event sequences and surface positions; the compact-owned per-cut balance cache does not depend on node links.
+
+Request headers use canonical full snapshots only. Initial and resume anchors remain full snapshots even when unchanged; an in-instance change appends another full `request/header` with reason `change`. The delta event, codec types, diff/apply helpers, and codec-only `fallback` reason are removed. Request reconstruction selects the latest snapshot.
+
+`SESSION_FORMAT_VERSION` remains pinned at `0`, so seed, append, and persistence-load validation explicitly reject old v0 `request/header-delta` events and full snapshots carrying the removed `fallback` reason. There is no compatibility fold or migration. JSONL and SQLite tests pin this fail-loud boundary, and the ACP snapshot harness represents legitimate mid-session changes as full pinned headers and full readable prompts.
+
+## Alternatives considered
+
+**Keep linked nodes and compact deltas for possible scale.** Links could help a future cursor API, and deltas can reduce logs when large tool schemas change by a small amount. No shipped cursor uses the links, while full snapshots trade disk size for substantially simpler correctness. If header volume proves material, compression or a measured canonical-delta scheme can be designed around real traces.
+
+## Verification
+
+Unit coverage pins ordered-surface append/replace behavior, tool pairing, compaction, full-header folding/logging, request reconstruction, and dev invariants. Seed validation plus JSONL and SQLite load tests reject the legacy event before replay. The keyless ACP suite exercises record, refresh, replay, changed-header pinning, and the sandbox mode-switch fixture in the new shape.
+
+## Consequences
+
+Full headers increase log volume, and linear replacement lookup could be slower on very large surfaces. Replacements were already linear because the prior implementation called `indexOf`; benchmarks are deferred until real traces show the simpler array is a bottleneck. The format version remains `0`, so explicit legacy-event rejection is a permanent part of the pre-release format boundary. In return, surface order and request-header state each have one representation, deleting link maintenance, maps, codec arms, round-trip fallback, and delta-aware snapshot normalization.
diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml
new file mode 100644
index 0000000000..e441d0bfb1
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml
@@ -0,0 +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-17-one-send-one-turn.md: 86c056b53700d0e0c02e04a99cf044fb311f5840
+2026-07-17-one-send-one-turn.zh.md: 3ef9973480481d11d1183760c9fc1f3c247629f4
diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md
new file mode 100644
index 0000000000..86c056b537
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md
@@ -0,0 +1,45 @@
+# Agent Note: Remove implicit batching from ordinary sends
+
+Status: implemented
+
+English | [中文](2026-07-17-one-send-one-turn.zh.md)
+
+## Problem
+
+Suppose a caller submits message A and then message B with two `Agent.send()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work.
+
+That grouping depends on timing rather than caller intent. Calls from one synchronous stack, neighboring microtasks, event listeners, and model callbacks could be grouped differently even though every caller used the same API.
+
+This grouping changes behavior, not just the number of model calls. One ordinary turn owns prompt admission, `turn/start`, `turn/end`, and a durability checkpoint. If message B shares message A's turn, B can enter A's model request instead of first seeing A's closed result in the session log. Allowing one message while blocking another also requires a mixed state that no caller requested.
+
+## Decision
+
+The rule is simple: each successful `send()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two sends are never silently combined.
+
+Before enqueueing an item, `send()` checks the agent state and makes a detached, deeply frozen snapshot of the content and resolved source. After enqueueing it, `send()` publishes `agent/queued`.
+
+If messages A and B are both processed, B's turn starts only after A records `turn/end` and A's durability checkpoint settles. B's request therefore sees whatever closed result A left in the same session log. A checkpoint error is reported, but settlement only releases this ordering barrier; it does not make a failed write durable. Broad `cancel()`, disposal, or a failure before `turn/start` can instead discard an unstarted item without opening an empty turn.
+
+Prompt admission decides one message at a time. An allowed prompt becomes that turn's `user/message`; a blocked prompt records one durable `prompt/blocked` and closes its one-message turn as `rejected`. Mixed-batch and all-blocked-batch branches do not exist.
+
+The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in a separate steering FIFO. While a turn remains open, the loop records that input at the next steering checkpoint, which comes before either a model request or the decision whether to continue. Steering makes another step the default, but continuation or terminal policy can still stop before the step starts. Steering left after the turn closes and its durability checkpoint settles becomes later queued input; terminal `agent/turn-stop`, cancellation, or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item.
+
+`inject()` continues to add model-facing context without submitting an ordinary message; its existing turn-enclosure and flush behavior stays unchanged. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary and steering input and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message. Several one-message turns can share one `running` interval, including turn close and its checkpoint, so `running` does not prove that a turn is open.
+
+## Alternatives considered
+
+**Keep automatic ordinary-send batching to reduce model calls.** This can improve throughput when producers outpace the driver, but it makes turn boundaries depend on scheduling and lets a later message run before the preceding turn closes and reaches its checkpoint. The decision keeps the predictable boundary and accepts the extra calls. Any future batching feature needs an explicit caller-visible contract backed by measurements.
+
+## Verification
+
+- Unit and property tests submit sends from the same stack, neighboring microtasks, different producers, and reentrant callbacks; every message gets its own FIFO-ordered turn.
+- A built-stdio test submits two lines and observes two model requests and two turn boundaries.
+- Delayed and rejected first-turn checkpoints keep the next turn waiting and prove that its request sees the preceding assistant result.
+- Failure-path tests cover prompt veto, listener failure, broad cancellation, disposal, and failure before `turn/start`; recorded turns stay balanced, messages do not merge, and surviving queued work still drains.
+- Separate tests cover open-turn, post-turn-close, and idle `steer()`, plus `inject()`, whole-agent status, and `whenIdle()`.
+
+## Consequences
+
+Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion or cancellation handle; broad cancellation can discard the entire unstarted tail, while status and quiescence remain agent-wide observations.
+
+The trade-off is more model requests and more checkpoints. A busy queue can take longer to drain and can grow under sustained producers. Ordinary-send batching returns only through an explicit, measured contract.
diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md
new file mode 100644
index 0000000000..3ef9973480
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md
@@ -0,0 +1,45 @@
+# Agent Note: 删除普通 send 的隐式批处理
+
+Status: implemented
+
+[English](2026-07-17-one-send-one-turn.md) | 中文
+
+## 问题
+
+假设调用方连续两次调用 `Agent.send()`,先提交消息 A,再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待,就把 A、B 放进同一个轮次。调用方明明调用了两次,agent loop(智能体循环)却悄悄把它们变成一个工作单元。
+
+这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。
+
+这种分组改变的不只是模型调用次数。一个普通轮次包含提示词准入、`turn/start`、`turn/end` 和持久性检查点。如果消息 B 与消息 A 共用轮次,B 可能直接进入 A 的模型请求,而不是先看到 A 在会话日志中已经关闭的结果。若系统允许一条消息、阻止另一条消息,还需要引入调用方没有请求的混合状态。
+
+## 决策
+
+规则很简单:一次成功的 `send()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。
+
+队列项入队之前,`send()` 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,`send()` 发布 `agent/queued`。
+
+如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。
+
+提示词准入每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词记录一条持久的 `prompt/blocked`,并让自己的单消息轮次以 `rejected` 关闭。实现中不存在混合批次或全阻止批次分支。
+
+上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入独立的 steering(中途引导)FIFO。只要当前轮次仍然打开,agent loop 就会在下一个 steering 检查点记录该输入;该检查点位于模型请求或继续轮次的决策之前。收到 steering 会把再执行一步作为默认选择,但继续轮次的策略或终止策略仍可在该步骤开始前停止。轮次关闭且其持久性检查点处理结束后,剩余的 steering 会成为后续排队输入;终止性的 `agent/turn-stop`、取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。
+
+`inject()` 继续添加面向模型的上下文,而不提交普通消息;其现有的轮次封闭与持久化刷新行为保持不变。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,该区间还可能覆盖轮次关闭及其检查点,因此 `running` 不表示轮次一定处于打开状态。
+
+## 曾考虑的替代方案
+
+**保留普通 send 的自动批处理,以减少模型调用。** 当消息进入队列的速度超过驱动器的处理速度时,这种做法可以提高吞吐量,但会让轮次边界取决于调度,并让后一条消息在前一轮关闭且到达检查点之前运行。本决策保留可预测的边界,并接受额外调用。未来若要加入批处理功能,必须提供调用方可见的显式契约,并有测量结果作为依据。
+
+## 验证
+
+- 单元测试和性质测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。
+- stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。
+- 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。
+- 失败路径测试覆盖提示词否决、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。
+- 其他测试分别覆盖轮次打开时、轮次关闭后和空闲时的 `steer()`,以及 `inject()`、面向整个 agent 的状态和 `whenIdle()`。
+
+## 后果
+
+普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成或取消句柄;广义取消可以丢弃整个尚未启动的队尾,状态和静止性也仍是面向整个 agent 的观察。
+
+代价是模型请求和检查点都会增加。繁忙队列可能需要更长时间才能清空;如果生产方持续提交消息,队列也可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。
diff --git a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml
new file mode 100644
index 0000000000..2b0b5c067d
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml
@@ -0,0 +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-19-retire-subagent-mock-package.md: 4a7fa32fdb0d8e656d61c39491a49bbd85e0adf3
+2026-07-19-retire-subagent-mock-package.zh.md: 7de72abb18050fb737000a2013e514dde3dae521
diff --git a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md
new file mode 100644
index 0000000000..4a7fa32fdb
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md
@@ -0,0 +1,34 @@
+# Agent Note: Retire the standalone subagent mock package
+
+Status: implemented
+
+English | [中文](2026-07-19-retire-subagent-mock-package.zh.md)
+
+## Problem
+
+`@deepseek-ai/dsh-subagent-mock` was a configurable test double packaged as a workspace plugin. Its only external consumers were the `tool-subagent` unit suite and the tool-catalog generator; no runtime package, example, snapshot configuration, or real provider loaded it.
+
+That narrow fixture carried a manifest, exports, peer and development dependencies, project references, package README obligations, Loader composition tests, module-graph membership, and documentation exceptions. The tool-catalog generator mounted it only to make production consumers register their schemas and never executed a child.
+
+## Decision
+
+The standalone package is deleted. Its scripted child behavior now lives in `packages/subagent/tool-subagent/tests/scripted-provider.ts`, where tests mount the real `SubagentService`, provider registry, tool implementation, and task runtime while replacing only the nondeterministic child boundary.
+
+The local fixture retains deterministic replies, structured results, stop reasons, cancellation before and after publication, conversation-inheritance descriptors, and effect-scoped disposal. Package-specific Schemastery and Loader-export tests disappear because the fixture is no longer a deployable plugin.
+
+The tool-catalog generator registers a minimal local `SubagentProvider` descriptor before mounting `ToolSubagent` or the workflow engine. The descriptor cannot start a child; it exists only to satisfy production load-time dependencies while harvesting schemas from the real consumers.
+
+Workspace project references, package dependencies, lockfile entries, graph metadata, support-package prose, config-catalog entries, and README gate exceptions no longer name the retired package.
+
+## Alternatives considered
+
+**Keep a reusable mock package for future tests.** Reuse never materialized outside one test file and one generator. A future second behavioral consumer can extract a shared fixture after its contract is known; pre-packaging it made test infrastructure look like a supported backend.
+
+**Generate subagent schemas without mounting production consumers.** Hand-constructing or importing schemas would weaken the catalog check that the real registry and tool composition expose the documented shape. A minimal provider descriptor preserves that check without carrying executable fake-backend behavior.
+
+## Consequences
+
+- The workspace has one fewer deployable package and no test-only node in the capability or module graphs.
+- `tool-subagent` tests retain foreground, background-task, lifecycle, cancellation, reply, stop-reason, and structured-result coverage through production services.
+- Tool-catalog output remains generated from production registrations and is byte-for-byte unchanged.
+- Runtime and example packages gain no dependency on test fixtures.
diff --git a/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md
new file mode 100644
index 0000000000..7de72abb18
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md
@@ -0,0 +1,34 @@
+# Agent Note: 撤销独立的 subagent mock 包
+
+Status: implemented
+
+[English](2026-07-19-retire-subagent-mock-package.md) | 中文
+
+## 问题
+
+`@deepseek-ai/dsh-subagent-mock` 曾是一个以工作区插件形式发布的可配置测试替身。它仅有两个外部消费方:`tool-subagent` 单元测试和工具目录生成器;运行时包、示例、快照配置和真实提供方都不会加载它。
+
+这个用途狭窄的 fixture(测试前置数据)需要维护 manifest(元数据清单)、导出、对等依赖(peer dependency)与开发依赖、项目引用、包(package)README 契约、Loader 组合测试、模块图成员关系以及文档例外。工具目录生成器挂载它,只是为了让生产消费方注册 schema,并不会执行子 agent。
+
+## 决策
+
+删除独立包。脚本化子 agent 行为现位于 `packages/subagent/tool-subagent/tests/scripted-provider.ts`;测试挂载真实的 `SubagentService`、提供方注册表、工具实现和任务运行时,只替换具有不确定性的子 agent 边界。
+
+本地 fixture 保留确定性回复、结构化结果、停止原因、发布前后的取消、对话继承描述和作用域化的 dispose(资源释放)覆盖。由于 fixture 不再是可部署插件,删除包专用的 Schemastery 与 Loader 导出测试。
+
+工具目录生成器在挂载 `ToolSubagent` 或工作流引擎之前,注册一个最小本地 `SubagentProvider` 描述。该描述无法启动子 agent;它只用于满足生产消费方的加载时依赖,同时从真实消费方提取 schema。
+
+工作区项目引用、包依赖、锁文件条目、图元数据、支持包说明、配置目录条目和 README 门禁例外不再提及已撤销的包。
+
+## 备选方案
+
+**为未来测试保留可复用 mock 包。** 除一个测试文件和一个生成器外,复用需求始终没有出现。未来产生第二个行为消费方时,可以在共享契约明确后再提取 fixture;提前将其打包会使测试基础设施看起来像受支持的后端。
+
+**不挂载生产消费方,直接生成 subagent schema。** 手工构造或直接导入 schema,会削弱目录门禁对真实注册表与工具组合是否公开文档结构的校验。最小提供方描述能保留该校验,而无需携带可执行的虚假后端行为。
+
+## 影响
+
+- 工作区减少一个可部署包,能力图与模块图也不再包含测试专用节点。
+- `tool-subagent` 测试继续通过生产服务覆盖前台、后台任务、生命周期、取消、回复、停止原因和结构化结果。
+- 工具目录输出仍根据生产注册生成,并保持字节级一致。
+- 运行时包与示例包都不会依赖测试 fixture。
diff --git a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml
new file mode 100644
index 0000000000..cd03e02285
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml
@@ -0,0 +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-19-use-one-session-surface-manager.md: dee1a2a1cb6642730c87035de071d77ad38bd238
+2026-07-19-use-one-session-surface-manager.zh.md: ce538f1569c91e317af347d2ac20db624215eac8
diff --git a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md
new file mode 100644
index 0000000000..dee1a2a1cb
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md
@@ -0,0 +1,37 @@
+# Agent Note: Use one surface manager per session
+
+Status: implemented
+
+English | [中文](2026-07-19-use-one-session-surface-manager.zh.md)
+
+## Problem
+
+`Session` maintained two `SurfaceManager` instances over the same append-only event log. One validated seed and append candidates, while a second lazy instance independently folded committed events for `session.surface`, derived messages, compaction, and workspace context. Once the public surface had been read, every later event advanced duplicate node and replacement-generation state without creating a separate authority or failure boundary.
+
+## Decision
+
+Each `Session` owns one eagerly constructed `SurfaceManager`. Seed and append acceptance call `validateNext()` on that manager before committing an event, and `session.surface` returns the same object through this readonly contract:
+
+```ts
+export interface SessionSurface {
+ readonly nodes: readonly number[]
+ readonly replaceGeneration: number
+}
+```
+
+Candidate validation remains atomic. `validateNext()` may synchronize committed log entries, but it only plans the uncommitted candidate. The candidate enters manager state after `log.push()` and the next delta synchronization, so surface validation failures and pre-commit `internal/dispatch` vetoes leave no phantom node or replacement generation.
+
+`foldSurface()` remains the detached full-log replay function for offline validation and reconstruction. It uses the same transitions and agrees with the live manager for every committed prefix without sharing mutable state.
+
+## Alternatives considered
+
+**Keep acceptance and projection state separate.** Separate instances appeared to isolate public reads from validation, but callers already receive borrowed surface state and the declared readonly contract prevents ordinary mutation. Duplicating the manager was not a runtime trust boundary.
+
+**Recompute the public surface from the full log on every access.** This removed duplicate cached state but gave up incremental derivation and made repeated request construction scale with complete session history.
+
+## Consequences
+
+- Acceptance, `session.surface`, derived messages, compaction, and workspace context observe one incremental state.
+- `Session.surface` exposes no validation method, while its object identity and borrowed readonly node array remain stable.
+- A hostile cast can still corrupt borrowed state; JavaScript callers that deliberately bypass the readonly contract remain outside the supported same-process boundary.
+- Surface, seed, dispatch-veto, request-reconstruction, compaction, and workspace-context tests exercise the shared manager and detached replay paths.
diff --git a/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md
new file mode 100644
index 0000000000..ce538f1569
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 每个会话只使用一个表层管理器
+
+Status: implemented
+
+[English](2026-07-19-use-one-session-surface-manager.md) | 中文
+
+## 问题
+
+`Session` 曾针对同一份仅追加事件日志维护两个 `SurfaceManager` 实例。一个实例负责校验种子事件和追加候选事件,另一个延迟创建的实例则独立折叠已提交事件,供 `session.surface`、派生消息、压缩(compaction)和工作区上下文使用。一旦读取公共表层,之后的每个事件都会推进两份重复的节点状态与替换代数状态,却没有形成独立真源或失败边界。
+
+## 决策
+
+每个 `Session` 主动创建并只持有一个 `SurfaceManager`。种子事件与追加事件的接纳流程在提交事件之前调用该管理器的 `validateNext()`,`session.surface` 则通过以下只读契约返回同一个对象:
+
+```ts
+export interface SessionSurface {
+ readonly nodes: readonly number[]
+ readonly replaceGeneration: number
+}
+```
+
+候选事件校验仍保持原子性。`validateNext()` 可以同步已提交的日志事件,但对尚未提交的候选事件只制定变更计划。候选事件在 `log.push()` 之后、下一次增量同步时才进入管理器状态,因此表层校验失败或提交前 `internal/dispatch` 否决都不会留下虚假节点或替换代数。
+
+`foldSurface()` 仍是离线校验与重建使用的分离式完整日志回放函数。它使用相同的状态转换,并且对每个已提交前缀都与活跃管理器一致,但不共享可变状态。
+
+## 备选方案
+
+**继续分离接纳状态与投影视图。** 两个独立实例看似能够隔离公共读取和校验,但调用方取得的本来就是借用的表层状态,声明的只读契约会阻止普通修改。复制管理器并不能构成运行时信任边界。
+
+**每次读取都根据完整日志重新计算公共表层。** 该方案能消除重复缓存状态,但会放弃增量派生,使每次请求构造都随完整会话历史增长。
+
+## 影响
+
+- 接纳流程、`session.surface`、派生消息、压缩和工作区上下文观察同一份增量状态。
+- `Session.surface` 不暴露校验方法,同时保持对象标识和借用的只读节点数组稳定。
+- 恶意类型断言仍可破坏借用状态;刻意绕过只读契约的 JavaScript 调用方不属于受支持的同进程边界。
+- 表层、种子、调度否决、请求重建、压缩和工作区上下文测试覆盖共享管理器与分离回放路径。
diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
new file mode 100644
index 0000000000..91e9b078ad
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
@@ -0,0 +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-20-remove-stdio-and-echo-agents.md: 2aba8193710c96d3726b91062bfa43d039b4cabf
+2026-07-20-remove-stdio-and-echo-agents.zh.md: 2c3916683f4743384a2ce4104319da26145837fe
diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
new file mode 100644
index 0000000000..2aba819371
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.md
@@ -0,0 +1,45 @@
+# Agent Note: Remove the stdio and Echo agents
+
+Status: implemented
+
+English | [中文](2026-07-20-remove-stdio-and-echo-agents.zh.md)
+
+## Problem
+
+DeepSeek Harness exposed two redundant product agents beside the TUI and Headless coding agents. The line-oriented stdio agent duplicated terminal interaction and non-interactive execution with a mixed prompt/output protocol. Echo duplicated Headless as a network-free mock model plus one teaching tool, making a test fixture into a user-facing agent and the default quick-start path.
+
+Both agents carried support surfaces beyond their leaf configurations. Stdio owned a UI plugin, app package, SDK interface, REPL leaf, prompt protocol, and Loader tests. Echo owned a runnable command, mock adapter, tool, CI demo gate, graph entry, teaching references, and a shared test fixture. Keeping any of those product paths would preserve the redundant agent indirectly.
+
+Standard input and output remain protocol boundaries for ACP, JSON-RPC, MCP, and child processes. Deterministic model adapters also remain valid inside tests. Those mechanisms do not justify a line-oriented or mock-only product agent.
+
+## Decision
+
+The stdio and Echo agents are removed without compatibility packages, modes, commands, or aliases. The stdio UI and app packages, `examples/repl-agent`, `examples/echo-agent`, `demo:repl`, `demo:echo`, their dedicated tests, and supporting manifests, gates, graphs, and documentation entries are deleted.
+
+The remaining application roles are explicit:
+
+- [`@deepseek-ai/dsh-tui-demo`](../../../../packages/examples/tui-demo/README.md) owns terminal-interactive execution. `examples/tui-agent` owns the complete coding composition, Code Mode overlay, PTY coverage, and terminal snapshots.
+- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) owns non-interactive execution. `examples/headless-agent` owns the real-model one-shot composition, replay snapshots, generic real-agent suites, and test-only keyless Loader fixtures.
+- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) and `@deepseek-ai/dsh-jsonrpc` own their framed protocol integrations.
+
+The SDK project model and create/config workflows replace the `stdio` run-interface option with `tui`; generated TUI projects compose `@deepseek-ai/dsh-tui` and create or resume one exact session. Repository-facing demo documentation requires a DeepSeek API key and leads with the real Headless or TUI agents.
+
+Keyless validation is test-owned. The Headless Loader smoke uses a fixture adapter to exercise a real tool round trip, the CLI built-bin suite pins output, persistence, failure, and signal semantics, and package-specific Loader tests keep deterministic adapters beside their scenarios. None is exposed as a runnable mock agent.
+
+## Verification
+
+TUI and Headless Loader coverage run the real app packages in source and built modes. TUI uses a pseudo-terminal; Headless proves its task/result and tool-call contracts. Generated graphs and repository searches reject stale package, command, leaf, and SDK-interface references.
+
+## Alternatives considered
+
+- **Keep the line agent only for pipes** — rejected because Headless has a bounded task contract, format-pure stdout, durable completion, and process exit status.
+- **Keep Echo as the keyless quick start** — rejected because the first product experience should exercise the real model and supported coding agent, not a scripted adapter with a bespoke tool.
+- **Keep Echo only as a CI demo command** — rejected because test-owned Headless fixtures cover the same Loader and built-artifact boundaries without preserving a mock product leaf.
+- **Remove every stdio or mock mechanism** — rejected because framed protocols, process I/O, and deterministic test adapters are independent infrastructure, not the removed agents.
+
+## Consequences
+
+- Interactive and non-interactive product execution each have one owner and one runnable coding leaf.
+- The repository has no keyless user-facing agent demo; local agent demos require `DEEPSEEK_API_KEY`.
+- CI retains keyless real-entry coverage through test fixtures rather than a product command.
+- Existing stdio-agent configurations, Echo commands, and SDK `--interface=stdio` invocations fail instead of being translated.
diff --git a/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
new file mode 100644
index 0000000000..2c3916683f
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
@@ -0,0 +1,45 @@
+# Agent Note: 移除 stdio 和 Echo agent
+
+Status: implemented
+
+[English](2026-07-20-remove-stdio-and-echo-agents.md) | 中文
+
+## 问题
+
+DeepSeek Harness 在 TUI 和 Headless coding agent 之外,还提供了两个重复的产品 agent(智能体)。面向行的 stdio agent 使用混合的提示符/输出协议,同时重复实现终端交互与非交互执行。Echo 则以无需联网的 mock 模型加一个教学工具重复实现 Headless,把测试 fixture(测试前置数据)变成面向用户的 agent 和默认快速上手路径。
+
+两个 agent 的配套实现都不止叶节点配置。stdio 拥有 UI 插件、app 包(package)、SDK 接口、REPL 叶节点、提示符协议和 Loader 测试。Echo 拥有可运行命令、mock 适配器、工具、CI 演示门禁、图谱条目、教学引用和共享测试 fixture。保留其中任何产品路径,都会间接保留这个重复的 agent。
+
+标准输入输出仍是 ACP、JSON-RPC、MCP 和子进程的协议边界。确定性模型适配器也仍可用于测试。这些机制不足以成为保留面向行或仅使用 mock 的产品 agent 的理由。
+
+## 决策
+
+彻底移除 stdio 和 Echo agent,不提供兼容包、模式、命令或别名。删除 stdio UI 包与 app 包、`examples/repl-agent`、`examples/echo-agent`、`demo:repl`、`demo:echo`、各自的专属测试,以及相关的 manifest(元数据清单)、门禁、图谱和文档条目。
+
+保留的应用角色均有明确归属:
+
+- [`@deepseek-ai/dsh-tui-demo`](../../../../packages/examples/tui-demo/README.md) 负责终端交互式执行。`examples/tui-agent` 拥有完整 coding 组装、Code Mode 覆盖层、PTY 覆盖和终端快照。
+- [`@deepseek-ai/dsh-cli-demo`](../../../../packages/examples/cli-demo/README.md) 负责非交互式执行。`examples/headless-agent` 拥有真实模型的单次任务组装、回放快照、通用真实 agent 测试套件,以及仅供测试使用的无密钥 Loader fixture。
+- [`@deepseek-ai/dsh-acp-demo`](../../../../packages/examples/acp-demo/README.md) 和 `@deepseek-ai/dsh-jsonrpc` 负责各自的分帧协议集成。
+
+SDK 工程模型与 create/config 工作流将 `stdio` 运行接口选项替换为 `tui`;生成的 TUI 工程组合 `@deepseek-ai/dsh-tui`,并创建或恢复一个确切会话。仓库中的演示文档要求 DeepSeek API key,并优先引导到真实的 Headless 或 TUI agent。
+
+无密钥验证由测试负责。Headless Loader 冒烟测试使用 fixture 适配器验证真实工具往返;CLI built-bin 测试套件固定输出、持久化、失败和信号语义;各包专属的 Loader 测试则将确定性适配器放在对应场景旁。其中任何一项都不会作为可运行的 mock agent 对外暴露。
+
+## 验证
+
+TUI 与 Headless 的 Loader 覆盖以源码和构建产物两种模式运行真实 app 包。TUI 使用伪终端;Headless 验证任务/结果契约和工具调用契约。生成图谱与仓库搜索会拒绝陈旧的包、命令、叶节点和 SDK 接口引用。
+
+## 曾考虑的替代方案
+
+- **仅为 pipe 保留面向行 agent**:不予采纳,因为 Headless 已提供有界任务契约、格式纯净的 stdout、持久完成边界和进程退出状态。
+- **保留 Echo 作为无密钥快速上手路径**:不予采纳,因为首次产品体验应使用真实模型和受支持的 coding agent,而不是带专用工具的脚本化适配器。
+- **只为 CI 演示命令保留 Echo**:不予采纳,因为由测试持有的 Headless fixture 可以覆盖相同的 Loader 和构建产物边界,无需保留 mock 产品叶节点。
+- **移除所有 stdio 或 mock 机制**:不予采纳,因为分帧协议、进程 I/O 和确定性测试适配器是独立基础设施,并不是被移除的 agent。
+
+## 后果
+
+- 交互式与非交互式产品执行分别只有一个归属方和一个可运行的 coding 叶节点。
+- 仓库没有面向用户的无密钥 agent 演示;本地 agent 演示需要 `DEEPSEEK_API_KEY`。
+- CI 通过测试 fixture 保留针对真实入口的无密钥覆盖,而不是依赖产品命令。
+- 既有 stdio agent 配置、Echo 命令和 SDK `--interface=stdio` 调用会直接失败,不会被转换。
diff --git a/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml
new file mode 100644
index 0000000000..edb7429454
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml
@@ -0,0 +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-20-unwrap-injected-content-envelopes.md: 32642660f7bcea748c349933b99552b1974922c5
+2026-07-20-unwrap-injected-content-envelopes.zh.md: a01a51e12cecca5bc46526ccca61dbe90eb3136f
diff --git a/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
new file mode 100644
index 0000000000..32642660f7
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
@@ -0,0 +1,41 @@
+# Agent Note: Project injected content verbatim, dropping the XML envelopes
+
+Status: implemented
+
+English | [中文](2026-07-20-unwrap-injected-content-envelopes.zh.md)
+
+## Problem
+
+Two families of injected session content rendered into the model transcript wrapped in XML envelopes: `steering/message` as `…` and `context/message` as `…` (the latter with a `'raw'` opt-out that skipped the wrapper). The envelopes aimed to tell the model "this is injected, not the user speaking."
+
+Two problems:
+
+- **No model is trained on these tags.** `` and `` are arbitrary markup no model was taught to read, so the framing adds tokens without a reliable effect and can actively mislead — recorded transcripts show a model treating a `` instruction as third-party metadata and refusing it while answering only the original prompt.
+- **The session surface is the wrong layer for framing.** The surface projects the durable log into the model transcript; deciding how content is worded is not its job. A caller that wants a particular frame formats its own content before injecting it — which the one heavy producer (`workspace-context`) already does, owning its complete `` frame and opting out of the `` wrapper with `envelope: 'raw'`. The remaining tag machinery (`ContextEnvelope`, an `envelope` field threaded through `InjectOptions`, `HookContext`, the `context/message` event, and the loop) served a distinction that belongs to the caller.
+
+## Decision
+
+Injected session content projects verbatim; the caller owns any framing. `deriveEventMessage` renders `user/message`, `context/message`, and `steering/message` through one shared case returning `{ role: 'user', content: event.data.content }`; their content blocks reach the model unchanged. `context/message`'s `source`/`meta` and `steering/message`'s `turn` stay in the durable event log but do not render.
+
+The `ContextEnvelope` type and every `envelope` field are removed — `context/message` in `SessionEventMap`, `InjectOptions`, `HookContext`, and the `inject()`/`additionalContexts` plumbing in `dsh-agent-loop`. `workspace-context` no longer requests `'raw'`; its self-framed content renders as before. The `renderTagged`/`renderContextEnvelope` helpers are deleted. `context/message.meta` still carries durable, model-hidden JSON state.
+
+The `source` attribution the envelopes carried is not lost — it remains on the durable events; it simply no longer renders into the transcript.
+
+## Alternatives considered
+
+- **Keep the `` envelope, unwrap only steering** — leaves the `ContextEnvelope`/`envelope` machinery alive for a framing bit no model reads, and keeps the inconsistency that the main producer already opts out of.
+- **Keep the envelope field for plugin-sourced content only** — splits one projection into two on `source.kind` for no observed benefit; a plugin steering the agent (hook-bridge continuation reasons) also wants the instruction followed, not labeled.
+- **Move the unwrapping into adapters** — the canonical projection is the model-visible contract ("model-visible ⟺ logged"); per-adapter divergence on framing would make the derived transcript adapter-dependent. Framing that a caller genuinely wants belongs in the caller's content, not in an adapter.
+
+## Consequences
+
+- Mid-turn steering and injected context reach the model with the same weight as an ordinary user prompt.
+- The transcript no longer distinguishes injected content from a user message; consumers that need the distinction read the durable event log, which keeps the event types, `source`, and `meta` intact.
+- The `hook-{cc,codex}-stop-continue` ACP snapshots were re-recorded: the old recordings captured the model refusing steering as third-party metadata, the fix's exact failure mode.
+- The [content-block-vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s tagged-envelope clause is amended to point here.
+
+## Deferred
+
+`workspace-context` already frames its own content: it emits a complete `…` block as the message content instead of leaning on a surface-level wrapper. That caller-owned pattern is the one to keep — the surface passes content through verbatim, and any framing lives in the producer's own content.
+
+Two framing paths existed — caller-baked framing (`workspace-context`'s ``) and surface-level wrapping (``/`` added by `deriveEventMessage`). This change removes the second, leaving only caller-owned framing. If labeled framing is wanted again, unify it through the event's `meta` map — the producer-attached, model-hidden metadata field — consumed by a dedicated renderer or adapter, rather than re-hardcoding a tag in `deriveEventMessage`. A producer declares the frame it wants in `meta`; one renderer applies it; the session-surface projection stays a verbatim pass-through.
diff --git a/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md
new file mode 100644
index 0000000000..a01a51e12c
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md
@@ -0,0 +1,41 @@
+# Agent Note: 注入内容逐字投影,去除 XML 封套
+
+Status: implemented
+
+[English](2026-07-20-unwrap-injected-content-envelopes.md) | 中文
+
+## 问题
+
+两类注入的会话内容在渲染进模型 transcript(文本记录)时被包在 XML 封套里:`steering/message` 包成 `…`,`context/message` 包成 `…`(后者有一个 `'raw'` 退出选项可跳过封套)。这些封套意在告诉模型「这是注入内容,不是用户在说话」。
+
+两个问题:
+
+- **没有模型在这些标签上训练过。** `` 和 `` 是任何模型都未被教会去读的任意标记,因此这层框架只是徒增 token 而没有可靠效果,还可能起反作用——已录制的 transcript 显示,模型会把 `` 指令当成第三方元数据而拒绝服从,只回答原始提示。
+- **session 表层是承载框架的错误层次。** 表层的职责是把持久日志投影为模型 transcript;决定内容如何措辞并不是它的事。想要特定框架的调用方可以在注入前自行格式化内容——唯一的重度生产方(`workspace-context`)本就这样做,它自带完整的 `` 框架,并用 `envelope: 'raw'` 退出 `` 封套。剩下的标签机制(`ContextEnvelope` 类型,以及贯穿 `InjectOptions`、`HookContext`、`context/message` 事件和 agent loop 的 `envelope` 字段)所服务的区分,本应归属调用方。
+
+## 决策
+
+注入的会话内容逐字投影,框架由调用方自行负责。`deriveEventMessage` 通过一个共享分支渲染 `user/message`、`context/message` 和 `steering/message`,都返回 `{ role: 'user', content: event.data.content }`;它们的内容块原样到达模型。`context/message` 的 `source`/`meta` 和 `steering/message` 的 `turn` 保留在持久事件日志中,但不渲染。
+
+`ContextEnvelope` 类型和所有 `envelope` 字段都被移除——包括 `SessionEventMap` 中的 `context/message`、`InjectOptions`、`HookContext`,以及 `dsh-agent-loop` 中 `inject()`/`additionalContexts` 的相关管线。`workspace-context` 不再请求 `'raw'`;它自带框架的内容渲染方式不变。`renderTagged`/`renderContextEnvelope` 辅助函数被删除。`context/message.meta` 仍携带持久的、对模型隐藏的 JSON 状态。
+
+封套曾携带的 `source` 归属并未丢失——它仍保留在持久事件上;只是不再渲染进 transcript。
+
+## 权衡的替代方案
+
+- **保留 `` 封套,只对 steering 去封套** —— 会为一个没有模型会读的框架位保留 `ContextEnvelope`/`envelope` 机制,并保留主要生产方本就退出的那种不一致。
+- **仅对插件来源的内容保留 envelope 字段** —— 会按 `source.kind` 把一条投影拆成两条,却没有观察到任何收益;插件引导 agent(智能体)时(钩子桥接器的轮次续行原因)同样希望指令被遵从,而不是被贴标签。
+- **把去封套的逻辑移入适配器** —— 规范投影就是模型可见契约(「模型可见 ⟺ 已记录」);让各适配器在框架上各行其是,会使派生的 transcript 依赖于适配器。调用方确实想要的框架应放进调用方自己的内容里,而不是适配器。
+
+## 结果
+
+- 中途引导与注入的 context 以与普通用户提示相同的权重到达模型。
+- transcript 不再区分注入内容与用户消息;需要这一区分的消费方读取持久事件日志,其中事件类型、`source` 和 `meta` 完整保留。
+- `hook-{cc,codex}-stop-continue` ACP 快照已重新录制:旧录制捕获的是模型把 steering 当作第三方元数据而拒绝服从,正是本次修复针对的失败模式。
+- [内容块词汇表 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md) 中关于带标签封套的条款已修订为指向本文。
+
+## 推迟事项
+
+`workspace-context` 已经自行为内容加框架:它把一个完整的 `…` 块作为消息内容发出,而不依赖表层封套。这种调用方自有的模式才是应保留的——表层逐字透传内容,任何框架都住在生产方自己的内容里。
+
+曾经存在两条框架路径——调用方自行加框架(`workspace-context` 的 ``),以及表层封套(`deriveEventMessage` 加上的 ``/``)。本次变更移除了后者,只留下调用方自有的框架。如果未来又需要带标签的框架,应由事件的 `meta` map(生产方附加、对模型隐藏的元数据字段)来统一它,交给专门的渲染器或适配器消费,而不是在 `deriveEventMessage` 中重新硬编码标签。生产方在 `meta` 中声明所需的框架,由一个渲染器统一施加;session 表层的投影始终保持逐字透传。
diff --git a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.md b/.agents/notes/implemented/testing/2026-06-11-property-based-testing.md
similarity index 89%
rename from docs/rfc/implemented/testing/2026-06-11-property-based-testing.md
rename to .agents/notes/implemented/testing/2026-06-11-property-based-testing.md
index 67404ab49a..06350753cd 100644
--- a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.md
+++ b/.agents/notes/implemented/testing/2026-06-11-property-based-testing.md
@@ -1,4 +1,4 @@
-# RFC: Property-based testing for protocol-shaped code
+# Agent Note: Property-based testing for protocol-shaped code
Status: implemented
@@ -20,8 +20,8 @@ Adopt `fast-check` (a root devDependency) with one `tests/properties.spec.ts` pe
## Consequences
- Generator quality is the value lever — the generators bias toward small index pools and short strings so collisions and interleavings are common.
-- **It already paid off:** the BlockAssembler stream found a real bug — a duplicate `block-end` at the same index overwrote an already-flushed block, so the streamed prefix disagreed with final `blocks()`. Fixed (first close wins, matching the existing straggler rule) with a dedicated regression test.
+- **It already paid off:** the BlockAssembler stream found a real bug — a duplicate `block-end` at the same index rewrote a completed block. Fixed (first close wins, matching the existing straggler rule) with a dedicated regression test.
- A property flake from a timeout is a finding, not something to retry away. The loop properties are deterministic by construction (settle on `agent/status`), so a hang is a real defect.
- Property tests supplement, not replace, the example tests that pin specific branches for the 100%-coverage gate.
-
+
diff --git a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md b/.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md
similarity index 69%
rename from docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md
rename to .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md
index 5f05c92317..c31739a41b 100644
--- a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md
+++ b/.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md
@@ -1,22 +1,22 @@
-# RFC: ACP snapshot tests — record-once / replay-deterministic
+# Agent Note: ACP snapshot tests — record-once / replay-deterministic
Status: implemented
## Problem
-Unit tests do not exercise the complete ACP subprocess transcript, while real-API tests are nondeterministic and key-gated. Editor-facing `session/update` output can therefore regress despite green unit coverage, as the [default-export postmortem](../../../postmortem/0001-acp-default-export-drops-inject.md) demonstrated.
+Unit tests do not exercise the complete ACP subprocess transcript, while real-API tests are nondeterministic and key-gated. Editor-facing `session/update` output can therefore regress despite green unit coverage, as the [default-export postmortem](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) demonstrated.
The blocker for a full-transcript test is the model: the agent's output is driven by a non-deterministic LLM, and a key-gated test that hits the real API on every run is neither deterministic nor CI-runnable. We want the fidelity of a real run with the determinism of a fixture.
-This RFC records the decision to add a third test tier — **snapshot tests** — and the design choices that make it deterministic, keyless-in-CI, and cheap to maintain.
+This Agent Note records the decision to add a third test tier — **snapshot tests** — and the design choices that make it deterministic, keyless-in-CI, and cheap to maintain.
## Decision
-A snapshot test boots the real ACP example, drives its stdio protocol from a deterministic script, and compares normalized output with committed goldens. A session log recorded once from the real API supplies all later model streams. The fixture is the product's ordinary persisted JSONL.
+A snapshot test boots the real ACP example, drives its stdio protocol from a deterministic script, and compares normalized output with committed expected outputs. A session log recorded once from the real API supplies all later model streams. The fixture is the product's ordinary persisted JSONL.
### The fixture is the persisted session JSONL
-Each scenario's `session.jsonl` is harvested from a real run. `assistant/chunk` events reproduce the model streams; tool, message, and boundary events capture the harness behavior. One ordinary session artifact therefore serves as both replay source and behavioral golden.
+Each scenario's `session.jsonl` is harvested from a real run. `assistant/chunk` events reproduce the model streams; tool, message, and boundary events capture the harness behavior. One ordinary session artifact therefore serves as both replay source and behavioral expected output.
### Replay derives the model script from the log
@@ -28,7 +28,7 @@ Each scenario's `session.jsonl` is harvested from a real run. `assistant/chunk`
```
{ kind: 'chunks', chunks: StreamChunk[] }
-| { kind: 'throw', chunks: StreamChunk[], message: string, code: string, status?: number }
+| { kind: 'throw', chunks: StreamChunk[], message: string, code: string }
| { kind: 'hang' }
```
@@ -40,20 +40,20 @@ Replay is positional and therefore permits only one in-flight model stream per s
### Recording harvests the log; keyless replay needs a providerless config
-Recording runs the scenario with the real `llm-deepseek` adapter and the JSONL persistence backend, then copies the produced `.jsonl` into the scenario dir. Per-event appends are durable, but the harness shuts the subprocess down gracefully (close stdin → `await ctx.dispose()`) before harvesting so the final events are flushed. `llm-replay` itself does no recording — it is replay-only.
+Recording runs the scenario with the real `llm-deepseek` adapter and the JSONL persistence backend configured with `persistenceCompression: 'none'`, then copies the produced `.jsonl` into the scenario dir. The explicit raw mode keeps committed replay fixtures line-readable while ordinary deployments use the backend's compressed default. Per-event appends are durable, but the harness shuts the subprocess down gracefully (close stdin → `await ctx.dispose()`) before harvesting so the final events are flushed. `llm-replay` itself does no recording — it is replay-only.
-Replay uses a `cordis.snapshot.yml` overlay that replaces the real adapter with `llm-replay` while retaining the live composition. Recording uses the ordinary config and a harness-supplied persistence root. Replay mode skips `.env` loading, so a stray API key cannot trigger a live call. See the [single-source config RFC](2026-07-04-single-source-acp-replay-config.md).
+Replay uses a `cordis.snapshot.yml` overlay that replaces the real adapter with `llm-replay` while retaining the live composition. Recording uses the ordinary config and a harness-supplied persistence root. Replay mode skips `.env` loading, so a stray API key cannot trigger a live call. See the [single-source config Agent Note](2026-07-04-single-source-acp-replay-config.md).
### Two surfaces: normalize, then compare
A snapshot run asserts **two** normalized surfaces, because the harness's external surfaces are distinct:
-1. The **stdout transcript** — the framed `session/update` JSON-RPC the editor sees. Catches regressions in the ACP bridge's event→update translation (`streamSessionEventUpdate`). Compared against a committed `stdout.golden.jsonl`.
-2. The **re-persisted session JSONL**, normalized and compared with `session.jsonl`. The same fixture is both replay source and expected log. Prompt text is scrubbed; one scenario per header class pins readable prompt and tool content as described in the [header-pinning RFC](2026-07-06-pin-request-header-content-in-one-scenario.md). Override scenarios derive model behavior solely from their sidecar.
+1. The **stdout transcript** — the framed `session/update` JSON-RPC the editor sees. Catches regressions in the ACP bridge's event→update translation (`streamSessionEventUpdate`). Compared against a committed `stdout.expected.jsonl`.
+2. The **re-persisted session JSONL**, normalized and compared with `session.jsonl`. The same fixture is both replay source and expected log. Prompt text is scrubbed; one scenario per header class pins readable prompt and tool content as described in the [header-pinning Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md). Override scenarios derive model behavior solely from their sidecar.
The surfaces are complementary: stdout covers bridge projection, while JSONL covers loop, tool, and boundary structure that the projection omits.
-Normalization replaces session, cwd, protocol-id, timestamp, path, and process volatility while preserving deterministic sequence numbers. Scenarios constrain real bash use to stable commands. The stdout golden remains wire-shaped JSONL and every raw line must parse as JSON. Vitest updates only the stdout golden; normalized session equality never overwrites the replay fixture.
+Normalization replaces session, cwd, protocol-id, timestamp, path, and process volatility while preserving deterministic sequence numbers. Scenarios constrain real bash use to stable commands. The stdout expected output remains wire-shaped JSONL and every raw line must parse as JSON. Vitest updates only the stdout expected output; normalized session equality never overwrites the replay fixture.
### Isolation: normalization now, sandbox later
@@ -65,11 +65,11 @@ Tool determinism comes from a temporary cwd, scrubbed environment, fresh non-log
### Two subcommands, replay in the default gate
-`pnpm run test:snapshot` replays committed fixtures keylessly; `test:snapshot:record` uses the real API and rewrites the harvested session log and stdout golden. Missing fixtures fail loud. Every scenario carries `input.json`, `stdout.golden.jsonl`, and `session.jsonl`; no-model cases use a header-only log. `replay.override.json` is required only for scenarios marked `overridden`, because its presence replaces derived replay. Fixture guards reject missing, mismatched, and orphaned files. Both commands accept scenario filters.
+`pnpm run test:snapshot` replays committed fixtures keylessly; `test:snapshot:record` uses the real API and rewrites the harvested session log and stdout expected output. Missing fixtures fail loud. Every scenario carries `input.json`, `stdout.expected.jsonl`, and `session.jsonl`; no-model cases use a header-only log. `replay.override.json` is required only for scenarios marked `overridden`, because its presence replaces derived replay. Fixture guards reject missing, mismatched, and orphaned files. Both commands accept scenario filters.
## Alternatives considered
-- **A hand-authored `llm.json` of model chunks** — the earlier draft; reusing the real session log makes the fixture a genuine product of the system rather than a hand-built mock, and doubles it as a behavioral golden.
+- **A hand-authored `llm.json` of model chunks** — the earlier draft; reusing the real session log makes the fixture a genuine product of the system rather than a hand-built mock, and doubles it as a behavioral expected output.
- **A byte-level HTTP-record library (Polly/nock/MSW)** — rejected: adapter-specific, awkward with streaming SSE, and lower-level than the thing under test.
- **Synthesizing throw/cancel entries from `turn/end {kind:'error'|'aborted'}`** — rejected: it couples `llm-replay` to loop-internal turn-closing semantics, and the `turn/end` reason is lossy (it cannot distinguish a thrown 401 from a finish-error); the explicit `replay.override.json` sidecar is the cleaner seam.
@@ -77,4 +77,4 @@ Tool determinism comes from a temporary cwd, scrubbed environment, fresh non-log
The new tier adds reviewed per-scenario input, session, stdout, optional override, and optional workspace fixtures. Workspace seeds are copied into the temporary cwd for both record and replay. In return the tier provides deterministic keyless transcript coverage through the real Loader and tool composition. The subprocess, input, workspace, normalization, and replay harness can support examples beyond ACP.
-This RFC relates to but does not supersede the [proposed determinism RFC](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md): that proposal's "universal replay fixture" re-derives session *message history* after every test (an internal-consistency invariant), whereas snapshot tests pin the *external protocol output*. They are complementary — one guards the event-sourcing invariant, the other guards the editor-facing contract.
+This Agent Note relates to but does not supersede the [proposed determinism Agent Note](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md): that proposal's "universal replay fixture" re-derives session *message history* after every test (an internal-consistency invariant), whereas snapshot tests pin the *external protocol output*. They are complementary — one guards the event-sourcing invariant, the other guards the editor-facing contract.
diff --git a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md
similarity index 90%
rename from docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md
rename to .agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md
index ea2eb6964a..36160de354 100644
--- a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md
+++ b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md
@@ -1,14 +1,14 @@
-# RFC: Real-API e2e in CI against the external DeepSeek API
+# Agent Note: Real-API e2e in CI against the external DeepSeek API
Status: implemented
## Problem
-The harness leans hard on real-API tests by policy: [docs/testing.md](../../../testing.md) argues that a no-key suite proves the plumbing but not the product, and the [ACP inject postmortem](../../../postmortem/0001-acp-default-export-drops-inject.md) is the standing proof — 178 keyless tests stayed green while a real editor session crashed instantly. The real-API e2e suite (`pnpm run test:e2e`, the `*.e2e.ts` files) exists precisely to close that gap: it drives the agent against the live DeepSeek API — real model calls, real bash tools, multi-turn, resume, ACP-over-stdio.
+The harness leans hard on real-API tests by policy: [docs/testing.md](../../../../docs/testing.md) argues that a no-key suite proves the plumbing but not the product, and the [ACP inject postmortem](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) is the standing proof — 178 keyless tests stayed green while a real editor session crashed instantly. The real-API e2e suite (`pnpm run test:e2e`, the `*.e2e.ts` files) exists precisely to close that gap: it drives the agent against the live DeepSeek API — real model calls, real bash tools, multi-turn, resume, ACP-over-stdio.
The default gate ([.github/workflows/ci.yml](../../../../.github/workflows/ci.yml)) is deliberately keyless: it carries no secret and runs for forks. `test:e2e` self-skips without a key (`describe.skipIf(!process.env.DEEPSEEK_API_KEY)`), so adding it there would report green without exercising the real suite. A separate secret-bearing workflow is required to make real-API coverage a merge signal.
-This RFC records the decision to add a **second, secret-consuming workflow** that runs the real-API suite in CI, and — because introducing the first CI secret into a repo that may later go public is a security/isolation decision — the threat model it relies on and what changes when the repo becomes public.
+This Agent Note records the decision to add a **second, secret-consuming workflow** that runs the real-API suite in CI, and — because introducing the first CI secret into a repo that may later go public is a security/isolation decision — the threat model it relies on and what changes when the repo becomes public.
## Decision
@@ -20,7 +20,7 @@ ci.yml's value is that it is keyless, forkable, and always-green: any contributo
### Cost is not the constraint; reliability is
-Internal inference cost is not the limiting constraint, so the workflow optimizes for coverage and signal. It runs every matching `*.e2e.ts` file on multiple triggers and every trusted PR, implementing the [docs/testing.md](../../../testing.md) with-key policy.
+Internal inference cost is not the limiting constraint, so the workflow optimizes for coverage and signal. It runs every matching `*.e2e.ts` file on multiple triggers and every trusted PR, implementing the [docs/testing.md](../../../../docs/testing.md) with-key policy.
### Triggers: trusted events only
@@ -93,6 +93,6 @@ None of these require changing the workflow to go public; they are operational s
A second CI workflow and the first repo secret to maintain. The real-API suite now gates merges (pre-merge on trusted PRs, post-merge on the main branch) and runs nightly, so a real break in the agent's interaction with the external API surfaces in CI rather than only in a developer's local run — at the cost of real (but internally free) API calls on every trusted PR and merge. The preflight makes secret misconfiguration self-announcing instead of silently disabling the net.
-The design carries a documented constraint surface: the `pull_request` trigger's key-exposure tradeoff (drop it to harden), the `if:` gate's dependence on the author-based Dependabot test, and the hard prohibition on `pull_request_target`. The going-public checklist above is the operational companion — this RFC is the place a future maintainer should re-read before changing the trigger set or flipping repo visibility, rather than re-deriving the fork/secret model from scratch.
+The design carries a documented constraint surface: the `pull_request` trigger's key-exposure tradeoff (drop it to harden), the `if:` gate's dependence on the author-based Dependabot test, and the hard prohibition on `pull_request_target`. The going-public checklist above is the operational companion — this Agent Note is the place a future maintainer should re-read before changing the trigger set or flipping repo visibility, rather than re-deriving the fork/secret model from scratch.
The scheduled trigger auto-disables after 60 days of repo inactivity (a GitHub behavior); push/PR/dispatch are backstops, and an active monorepo will not hit it. Runner egress to `https://api.deepseek.com` is assumed — GitHub-hosted `ubuntu-latest` has it; an egress-restricted self-hosted runner would need connectivity confirmed before relying on the nightly.
diff --git a/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md b/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
new file mode 100644
index 0000000000..b17ecad098
--- /dev/null
+++ b/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md
@@ -0,0 +1,35 @@
+# Agent Note: Use `session.jsonl` as the only snapshot session-log artifact
+
+Status: implemented
+
+## Problem
+
+Model-driving ACP snapshot scenarios ship both `session.jsonl` and `session.expected.jsonl`. For normal recorded scenarios, `session.jsonl` is the replay fixture harvested from a real run, and the replay test normalizes the newly persisted log and compares it to `session.expected.jsonl`. In the current fixtures, the two normalized logs are identical for ordinary recorded scenarios.
+
+Authored override scenarios (`error-finish`, `cancel`) currently use `replay.override.json` to drive model behavior and keep `session.jsonl` as a minimal dummy fixture, while `session.expected.jsonl` holds the expected persisted log. The override file is a JSON array of `ReplayEntry` objects: `{ "kind": "chunks", "chunks": StreamChunk[] }`, `{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string }`, or `{ "kind": "hang" }`. That split is also unnecessary: when an override sidecar exists, `llm-replay` replaces the derived script and does not need `session.jsonl` for model chunks, so `session.jsonl` can still be the expected session-log artifact for the scenario.
+
+## Decision
+
+The `session.expected.jsonl` concept is removed entirely. Every scenario has at most one committed session-log artifact, `session.jsonl`:
+
+- For recorded scenarios, `session.jsonl` remains the raw harvested log. Replay still derives model chunks from it, and the snapshot test compares the replay run's normalized persisted log against normalized `session.jsonl`.
+- For authored override scenarios, `replay.override.json` drives model behavior and `session.jsonl` holds the expected produced session log. The replay adapter ignores the fixture for model chunks when the override exists, so the same file can be the expected log without affecting replay behavior.
+- For no-model scenarios, `session.jsonl` can stay as the minimal fixture needed to boot `llm-replay`; no session-log comparison is needed unless the scenario creates a persisted session.
+
+Stdout expected outputs remain unchanged; they are the editor-facing projection and are not redundant with the session fixture.
+
+## Alternatives considered
+
+**Normalizing both sides against a shared (replay-run) context** — rejected: `normalizeSessionLog` scrubs cwd by exact string match, so the fixture's recorded cwd would survive unscrubbed and every compare would fail. Each side normalizes against its own header-derived context — the implementation note below carries the mechanics.
+
+## Verification
+
+`session.expected.jsonl` appears nowhere in the snapshot harness, fixtures, orphan guards, or docs; the snapshot test derives the expected session log from `session.jsonl` for every model scenario; authored sidecar scenarios commit their expected produced log as `session.jsonl` with `replay.override.json` as the model-behavior override; and the orphan-fixture guards know which files each scenario kind requires. The [ACP snapshot tests Agent Note](2026-06-19-acp-snapshot-tests.md) describes the reduced fixture set.
+
+## Consequences
+
+Reviewers lose one artifact name that made the expected persisted log visually separate from the replay fixture. The stdout expected output still protects the editor transcript, and comparing replay output to `session.jsonl` preserves the loop/persistence regression check without duplicating files.
+
+## Implementation note
+
+Each side is normalized against its own header values because recording and replay have different ids, paths, and timestamps. `fixtureContext()` derives the fixture context from its header, making already-normalized fixtures idempotent. Session logs use plain equality rather than file-snapshot updates, so comparison never rewrites fixtures.
diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
similarity index 90%
rename from docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
rename to .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
index 14db415b1b..93280ed62e 100644
--- a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
+++ b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
@@ -1,10 +1,10 @@
-# RFC: Persist the seed boundary so fork-child replay routes correctly
+# Agent Note: Persist the seed boundary so fork-child replay routes correctly
Status: implemented
## Problem
-The [per-session snapshot replay RFC](2026-06-22-subagent-snapshot-replay.md) made the snapshot tier express a nested-agent shape: a parent plus one recorded log per in-process subagent, each replayed as its own script keyed by calling session. It noted (§ Scope, final bullet) that a fork snapshot was "a trivial future addition, not a gap in the keying." That was wrong about a fork child specifically — not the keying, but the *script derivation*.
+The [per-session snapshot replay Agent Note](2026-06-22-subagent-snapshot-replay.md) made the snapshot tier express a nested-agent shape: a parent plus one recorded log per in-process subagent, each replayed as its own script keyed by calling session. It noted (§ Scope, final bullet) that a fork snapshot was "a trivial future addition, not a gap in the keying." That was wrong about a fork child specifically — not the keying, but the *script derivation*.
A subagent script is derived from a recorded session log by [`deriveReplayScript`](../../../../packages/support/llm-replay): it groups the log's `assistant/chunk` events by `(turn, step)` into one replay entry per `stream()` call. This is correct for a **spawn** child, whose log contains only its own model calls.
diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.md b/.agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
similarity index 55%
rename from docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
rename to .agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
index b2c39047b9..272e62ba77 100644
--- a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
+++ b/.agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md
@@ -1,10 +1,10 @@
-# RFC: Record fork and mixed spawn+fork snapshot scenarios
+# Agent Note: Record fork and mixed spawn+fork snapshot scenarios
Status: implemented
## Problem
-The [seed-boundary RFC](2026-06-22-fork-child-replay-seed-boundary.md) made fork-child replay route correctly: `dsh-llm-replay` derives a child's script from the events at or after its persisted `seedLength` boundary, so a fork child's inherited parent prefix is not replayed as the child's own model calls. But it shipped with **no recorded fork scenario** — the slice was exercised only by `llm-replay`'s unit tests (a synthetic child fixture) and a persistence round-trip test. The full-transcript snapshot tier, the one net that boots the real `acp-agent` and replays an end-to-end nested transcript, had only spawn children (`subagent-spawn`, `subagent-multi`). A fork-routing regression that left the unit tests green would still have escaped the tier built to catch transcript regressions.
+The [seed-boundary Agent Note](2026-06-22-fork-child-replay-seed-boundary.md) made fork-child replay route correctly: `dsh-llm-replay` derives a child's script from the events at or after its persisted `seedLength` boundary, so a fork child's inherited parent prefix is not replayed as the child's own model calls. But it shipped with **no recorded fork scenario** — the slice was exercised only by `llm-replay`'s unit tests (a synthetic child fixture) and a persistence round-trip test. The full-transcript snapshot tier, the one net that boots the real `acp-agent` and replays an end-to-end nested transcript, had only spawn children (`subagent-spawn`, `subagent-multi`). A fork-routing regression that left the unit tests green would still have escaped the tier built to catch transcript regressions.
The snapshot infrastructure to express a fork scenario was already in place — both in-process backends are wired into `cordis.yml` / `cordis.snapshot.yml` as two model-facing tools (`subagent` → spawn, `subagent_fork` → fork), the harness harvests every child log, and replay forwards per-child fixtures keyed by `seedLength`. What was missing was a *recorded scenario* that drives a fork child through it.
@@ -13,11 +13,11 @@ The snapshot infrastructure to express a fork scenario was already in place —
Record two scenarios against the real API, both replayed keyless in the default gate:
- **`subagent-fork`** — the parent completes a turn that establishes a fact, then delegates one subtask via `subagent_fork`. The fork child inherits the conversation (its log carries a non-zero `seedLength`), so it can answer from the parent's context. This is the focused regression: the child fixture's `seedLength` is the boundary the replay slice depends on, recorded from a real fork rather than hand-synthesized.
-- **`subagent-mixed`** — the parent completes a turn, then delegates once via `subagent` (a fresh spawn child, `seedLength` 0) and once via `subagent_fork` (a fork child, non-zero `seedLength`) in one transcript. This is the mixed spawn+fork scenario the seed-boundary and per-session-replay RFCs both named as a future addition: one transcript exercises both transports and both branches of the slice (`seedLength` 0 = no-op, `seedLength > 0` = trim the inherited prefix), with the two children ordered spawn-then-fork by `createdAt`.
+- **`subagent-mixed`** — the parent completes a turn, then delegates once via `subagent` (a fresh spawn child, `seedLength` 0) and once via `subagent_fork` (a fork child, non-zero `seedLength`) in one transcript. This is the mixed spawn+fork scenario the seed-boundary and per-session-replay Agent Notes both named as a future addition: one transcript exercises both transports and both branches of the slice (`seedLength` 0 = no-op, `seedLength > 0` = trim the inherited prefix), with the two children ordered spawn-then-fork by `createdAt`.
### Why a completed turn-1 is required
-The fork backend seeds the child with the parent's **balanced completed-turn prefix** ([`completedTurnPrefix`](../../../../packages/subagent/subagent-fork)). A parent that forks on its very first turn has no completed turn to inherit, so the seed is empty (≡ a fresh spawn, `seedLength` 0) — which would NOT exercise the slice. Both scenarios therefore use a two-prompt input: the first prompt completes a turn (establishing a codeword the child is later asked to recall), the second delegates the fork. The recalled codeword in the child's transcript is incidental to the model's behavior; the load-bearing artifact is the child fixture's recorded `seedLength`, which the replay slice consumes.
+The fork backend seeds the child with the parent's **balanced completed-turn prefix**. A parent that forks on its very first turn has no completed turn to inherit, so the seed is empty (≡ a fresh spawn, `seedLength` 0) — which would NOT exercise the slice. Both scenarios therefore use a two-prompt input: the first prompt completes a turn (establishing a codeword the child is later asked to recall), the second delegates the fork. The recalled codeword in the child's transcript is incidental to the model's behavior; the load-bearing artifact is the child fixture's recorded `seedLength`, which the replay slice consumes.
## Consequences
@@ -26,4 +26,4 @@ The fork backend seeds the child with the parent's **balanced completed-turn pre
- Out-of-process (ACP) subagent replay remains a different shape (each child is its own process with its own replay) and is still tracked as `TODO(acp-subagent-replay)` — these scenarios are in-process only.
- Re-recording (`pnpm run test:snapshot:record`) regenerates all four fork/spawn fixtures from the live API; the two new scenarios self-skip without a key like every recorded scenario.
-
+
diff --git a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md b/.agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md
similarity index 92%
rename from docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md
rename to .agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md
index 8135f6afd1..d21a081f9b 100644
--- a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md
+++ b/.agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md
@@ -1,17 +1,17 @@
-# RFC: Per-session snapshot replay for nested agents
+# Agent Note: Per-session snapshot replay for nested agents
Status: implemented
## Problem
-The snapshot tier (`pnpm run test:snapshot`) boots the real `acp-agent` subprocess, replays a recorded session through [`dsh-llm-replay`](../../../../packages/support/llm-replay), and diffs the normalized stdout transcript + re-persisted session log against committed goldens. It is the only tier that exercises the full editor-facing transcript end to end.
+The snapshot tier (`pnpm run test:snapshot`) boots the real `acp-agent` subprocess, replays a recorded session through [`dsh-llm-replay`](../../../../packages/support/llm-replay), and diffs the normalized stdout transcript + re-persisted session log against committed expected outputs. It is the only tier that exercises the full editor-facing transcript end to end.
It was built for ONE session per process, and that assumption is wired into two places:
- **`dsh-llm-replay` keyed nothing.** It served the Nth `llm/stream` call the Nth recorded entry from a single global cursor. With a parent agent AND an in-process subagent both streaming on one context, the calls interleave and the single cursor hands the child the parent's script (and vice versa).
- **The harness harvested one log.** `findSessionLog` walked the sessions root and returned the FIRST `.jsonl` it found. A subagent runs as a second `Session` with its own log in the same cwd bucket, so the child's transcript was silently dropped.
-This was the `TODO(subagent-snapshots)` deferral recorded in the [subagent seam RFC](../../implemented/feature/2026-06-21-subagent-capability-seam.md): the in-process backends (PR2) shipped with unit + e2e coverage, but the full-transcript snapshot tier could not express a nested-agent shape until this infrastructure landed. This RFC is that stacked follow-up.
+This was the `TODO(subagent-snapshots)` deferral recorded in the [subagent seam Agent Note](../feature/2026-06-21-subagent-capability-seam.md): the in-process backends (PR2) shipped with unit + e2e coverage, but the full-transcript snapshot tier could not express a nested-agent shape until this infrastructure landed. This Agent Note is that stacked follow-up.
## Decision
diff --git a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md b/.agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md
similarity index 74%
rename from docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md
rename to .agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md
index edfcc18585..7c2c33460c 100644
--- a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md
+++ b/.agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md
@@ -1,10 +1,10 @@
-# RFC: Hook snapshot matrix — end-to-end goldens for both bridges
+# Agent Note: Hook snapshot matrix — end-to-end expected outputs for both bridges
Status: implemented
## Problem
-The hook bridges — [`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude) (7 Claude Code hook points) and [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex) (5 Codex points) — map external hook commands onto the harness interception seams. They carry deep unit and coverage-spec coverage (every decision arm, every payload dialect, driven against a mocked seam) plus one key-gated e2e (`hooks.e2e.ts`, a live `PreToolUse` block). But the full-transcript snapshot tier — the one net that boots the real `acp-agent` subprocess, replays a recorded session keyless, and diffs the normalized ACP stdout + re-persisted log against committed goldens — covered exactly ONE hook: a Claude `UserPromptSubmit` block (`hook-cc-promptsubmit-block`).
+The hook bridges — [`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude) (7 Claude Code hook points) and [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex) (5 Codex points) — map external hook commands onto the harness interception seams. They carry deep unit and coverage-spec coverage (every decision arm, every payload dialect, driven against a mocked seam) plus one key-gated e2e (`hooks.e2e.ts`, a live `PreToolUse` block). But the full-transcript snapshot tier — the one net that boots the real `acp-agent` subprocess, replays a recorded session keyless, and diffs the normalized ACP stdout + re-persisted log against committed expected outputs — covered exactly ONE hook: a Claude `UserPromptSubmit` block (`hook-cc-promptsubmit-block`).
That is the tier a mocked unit test structurally cannot be: it exercises the REAL bridge translating a REAL hook process's outcome into the REAL seam decision, then the REAL loop's reaction, rendered exactly as an editor sees it. A bridge-translation or loop-structure regression that left every unit green would still escape it for every hook point but one — and for the Codex bridge, the ACP example did not even LOAD it, so no Codex hook could fire end-to-end at all.
@@ -29,20 +29,22 @@ Thirteen scenarios under `examples/acp-agent/tests/snapshots/`, naming `hook-
+
diff --git a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md b/.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md
similarity index 90%
rename from docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md
rename to .agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md
index 70730f0382..3de645deeb 100644
--- a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md
+++ b/.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md
@@ -1,16 +1,16 @@
-# RFC: Single-source the acp-agent replay config
+# Agent Note: Single-source the acp-agent replay config
Status: implemented
## Problem
-`examples/acp-agent` shipped two hand-maintained configs: `cordis.yml` (the live tree) and a `cordis.snapshot.yml` that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line `llm-deepseek` stanza versus the two-line `llm-replay` stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapshot tier would silently exercise a different app than the one that ships — the ["green units, broken product" class of gap](../../../postmortem/0001-acp-default-export-drops-inject.md) the snapshot tier exists to close, reintroduced one level up, with reviewer vigilance as the only defense.
+`examples/acp-agent` shipped two hand-maintained configs: `cordis.yml` (the live tree) and a `cordis.snapshot.yml` that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line `llm-deepseek` stanza versus the two-line `llm-replay` stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapshot tier would silently exercise a different app than the one that ships — the ["green units, broken product" class of gap](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) the snapshot tier exists to close, reintroduced one level up, with reviewer vigilance as the only defense.
## Decision
`cordis.snapshot.yml` includes the live config, disables the named DeepSeek adapter by id and name, and inserts the replay adapter. Every other entry therefore comes from the shipping tree. Replay selects the overlay; recording still boots `cordis.yml`, and the load guard permits the intentionally disabled entry.
-One vendored-plugin fact the overlay depends on, deliberately: the include applies `patches` when it loads the file — its `refresh()`/`internal/update` paths re-read without re-patching — which is exactly enough for a one-shot replay boot (the replay app loads no `hmr` and nothing rewrites the config mid-run). The snapshot suite is the proof: all scenarios pass unchanged on the overlay, byte-identical goldens included.
+One vendored-plugin fact the overlay depends on, deliberately: the include applies `patches` when it loads the file — its `refresh()`/`internal/update` paths re-read without re-patching — which is exactly enough for a one-shot replay boot (the replay app loads no `hmr` and nothing rewrites the config mid-run). The snapshot suite is the proof: all scenarios pass unchanged on the overlay, byte-identical expected outputs included.
## Alternatives considered
diff --git a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md b/.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
similarity index 69%
rename from docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
rename to .agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
index 42c67320f6..1dcfc2e085 100644
--- a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
+++ b/.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md
@@ -1,4 +1,4 @@
-# RFC: Pin request-header content in one snapshot scenario
+# Agent Note: Pin request-header content in one snapshot scenario
Status: implemented
@@ -8,11 +8,11 @@ An ACP snapshot suite needs to prove the exact composed system prompt and tool-s
## Decision
-Exactly one scenario per header-composition class is flagged `pinsHeader`. Its directory splits the pin by review format: `system-prompt.golden.md` contains the normalized composed prompt as ordinary Markdown, `tool-schemas.golden.json` contains the complete initial schemas and later schema edits as structured JSON, and `session.jsonl` retains config, reason, and any model-visible prefix while storing `header.system` and `header.tools` as `"{{system}}"` / `"{{tools}}"`. Every other JSONL uses the same prompt and tool tokens and also tokenizes session-prefix content. The pin mechanics live in [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md), whose suite factory enforces one pin per class.
+Exactly one scenario per header-composition class is flagged `pinsHeader`. Its directory splits the pin by review format: `system-prompt.expected.md` contains the normalized full prompt sequence as ordinary Markdown, `tool-schemas.expected.json` contains the corresponding complete schema sequence as structured JSON, and `session.jsonl` retains config, reason, and any model-visible prefix while storing `header.system` and `header.tools` as `"{{system}}"` / `"{{tools}}"`. Every other JSONL uses the same prompt and tool tokens and also tokenizes session-prefix content. The pin mechanics live in [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md), whose suite factory enforces one pin per class.
-The pure `scrubSystemPrompts` and `scrubToolSchemas` normalizers apply to every stored session fixture and independently tokenize initial-header content plus header-delta bulk. `scrubRequestHeaders` also tokenizes session-prefix content for non-pinning scenarios while retaining structural facts: system-delta positions and arity, added/removed/changed tool names, prefix message count, field presence, config, and reason. Record and refresh write-back apply the appropriate scrub before writing JSONL and regenerate both sidecars from the normalized live header and deltas, so neither path can reintroduce prompt/schema bulk into JSONL or leave a review artifact stale.
+The pure `scrubSystemPrompts` and `scrubToolSchemas` normalizers independently tokenize every stored full header. `scrubRequestHeaders` also tokenizes session-prefix content for non-pinning scenarios while retaining header count, field presence, config, reason, and prefix message count. Record and refresh write-back apply the appropriate scrub before writing JSONL and regenerate both sidecars from the normalized live full-header sequence, so neither path can reintroduce prompt/schema bulk into JSONL or leave a review artifact stale.
-Guards make the split self-enforcing. On disk, every `session*.jsonl` is a fixed point of both prompt and schema scrubbers, only non-pinning fixtures must be fixed points of the full header scrub, both sidecars exist exactly beside pinning fixtures in canonical newline-terminated formats, and each class has one pin. Live, every `request/header` produced by a parent, spawn child, fork child, initial request, or resume must match the reconstructed pin after volatile-value normalization; the pinning run's prompt and schema deltas must also match their sidecars. A header without a string prompt, without an array-valued tool list, or with an undeclared `request/header-delta` fails loud.
+Guards make the split self-enforcing. On disk, every `session*.jsonl` is a fixed point of both prompt and schema scrubbers, only non-pinning fixtures must be fixed points of the full header scrub, both sidecars exist exactly beside pinning fixtures in canonical newline-terminated formats, and each class has one pin. Live, every `request/header` produced by a parent, spawn child, fork child, initial request, resume, or in-instance change must match the reconstructed class sequence after volatile-value normalization. A header without a string prompt, without an array-valued tool list, or beyond the pin's declared changed-header count fails loud.
One pin covers the whole suite because every session — parent, spawn child, fork child — composes the identical tool list and the identical prompt modulo cwd, and the uniformity guard fails the suite the moment that stops holding. If header composition ever becomes session-dependent by design (a restricted subagent toolset, say), the divergent shape gets its own pinning scenario.
@@ -22,11 +22,11 @@ One pin covers the whole suite because every session — parent, spawn child, fo
- **Scrub at compare time only, keeping fixtures raw** — lets compares pass while committed fixtures retain stale duplicate content and rewrite wholesale on the next recording. Stored tokens state honestly what each JSONL does not pin.
- **Scrub everywhere, pin nowhere** — loses the only end-to-end record of the composed header as actually sent (prompt assembly, registered-tool order, full schemas). The generated tool catalog documents each tool in isolation; only a real fixture pins the composed set.
- **Keep the one full pin entirely in JSONL** — removes suite-wide duplication but leaves prompt and schema changes as one escaped line. Markdown and structured JSON give each surface its natural review format without weakening the reconstructed-header assertion.
-- **Slim the session log itself (log a content digest, store the header elsewhere)** — violates the reconstructability contract: the product log must reproduce each request bit-for-bit ([reconstructable-requests RFC](../architecture/2026-07-05-reconstructable-requests.md)). Header bulk is a test-artifact concern, solved in test normalization; the live log is untouched.
+- **Slim the session log itself (log a content digest, store the header elsewhere)** — violates the reconstructability contract: the product log must reproduce each request bit-for-bit ([reconstructable-requests Agent Note](../architecture/2026-07-05-reconstructable-requests.md)). Header bulk is a test-artifact concern, solved in test normalization; the live log is untouched.
## Verification
-The suite replays every scenario against the split pins. Unit coverage exercises the independent and full scrubbers, both sidecar formats, record/refresh regeneration, normalized prompt/schema extraction, fixed-point enforcement, required-file symmetry, reconstructed-header uniformity, and delta rejection.
+The suite replays every scenario against the split pins. Unit coverage exercises the independent and full scrubbers, both full-header sidecar formats, record/refresh regeneration, normalized prompt/schema extraction, fixed-point enforcement, required-file symmetry, reconstructed-header uniformity, and changed-header count rejection.
## Consequences
diff --git a/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md b/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md
new file mode 100644
index 0000000000..aadeaeea30
--- /dev/null
+++ b/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md
@@ -0,0 +1,38 @@
+# Agent Note: Extract the ACP snapshot suite into a support package
+
+Status: implemented
+
+## Problem
+
+The ACP snapshot tier ([snapshot Agent Note](2026-06-19-acp-snapshot-tests.md)) was built from three modules living inside one example's test directory: `snapshot-harness.ts` (boot the real bin subprocess, drive it over ACP JSON-RPC, harvest the persisted logs), `snapshot-normalize.ts` (the pure expected-output normalizers), and the ~150-line scenario body plus fixture guards in `acp.snapshot.ts` (record/replay modes, the stdout expected-output and log comparisons, the pinned-header uniformity guard, the orphan/required-file/single-pin meta-tests).
+
+A second ACP example wanting snapshot coverage — the sandbox/approval composition is the immediate consumer — could only copy those modules, forking exactly the logic that must not drift: record write-back, header scrubbing, child-session harvest ordering. The spawn/client glue was also triplicated across `acp.e2e.ts`, `hooks.e2e.ts`, and the harness. Location decided test rigor: the per-file 100% coverage gate measures `packages/*/*/src` only, so none of this machinery was measured — the same gap that had moved `dsh-llm-replay` out of `examples/` into [packages/support](../../../../packages/support/README.md). And the harness's ACP client hardcoded `requestPermission → cancelled`, so an approval round-trip — the headline behavior of the sandbox composition — could not be expressed at the snapshot tier at all.
+
+## Decision
+
+The machinery lives in [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md) (`@deepseek-ai/dsh-acp-snapshot`); an example's `*.snapshot.ts` is its scenario table, its agent paths, and one factory call, over its own `snapshots/` fixtures and `cordis.snapshot.yml` overlay ([single-source replay config](2026-07-04-single-source-acp-replay-config.md)). Reading `DSH_SNAPSHOT` stays at that edge — the library takes a resolved `mode`.
+
+**`src/launcher.ts`** — `launchAcpTestAgent` owns the common unbuilt-process boundary: absolute tsx loader resolution, `TSX_TSCONFIG_PATH`, isolated harness homes, stdio wiring, a raw-byte stdout tee, stderr and update capture, fail-closed permission fallback, update waiters, and graceful or signalled shutdown. Snapshot scenarios and ordinary e2e suites supply the same `AgentUnderTest` (`binScript`, `configPath`, `tsconfigPath`); a test that plays a user supplies only its permission handler. The ACP and hook e2e suites plus the sandbox/approval e2e suite use this launcher instead of rebuilding the SDK client boundary.
+
+**`src/harness.ts`** — `runScenario` and the input-script/result types layer deterministic steps, temp workspaces, snapshot environment, and persisted-log harvest over the launcher. Its `session/request_permission` handler consumes an optional `InputScript.permissionAnswers` FIFO queue, each entry selecting by option **kind** (ids are agent-issued randoms a committed script cannot know; kinds are the ACP-stable vocabulary, mapped to the offered `optionId` at answer time); an absent or exhausted queue answers `cancelled`, and a kind the request never offered rejects the run — the agent itself is answered `cancelled`, so the scenario bug fails the harness rather than being absorbed as an agent-side denial. This is what lets an approval suite drive allow/reject round-trips deterministically from `input.json`.
+
+**`src/normalize.ts`** — the pure normalizers, hook-free by policy: when a future event carries a new volatile field (an approval duration, say), the shared normalizer learns it in the same change, keeping one home for what "normalized" means rather than per-suite scrub extensions.
+
+**`src/suite.ts`** — the `Scenario` type and `defineAcpSnapshotSuite(options)`, registering the per-scenario compares, record/refresh fixture write-back, the header pin with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL a `scrubSystemPrompts` fixed point, non-pinning fixtures also `scrubRequestHeaders` fixed points). A scenario directory's `session.jsonl` plus contiguous `session..jsonl` siblings are its ordered primary/child inventory, so the scenario table declares policy without duplicating a child count. The pinned-header contract ([pinned-header Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md)) is per-suite: each header class flags exactly one `pinsHeader` scenario, whose `system-prompt.expected.md` and JSONL tool list split the composed header into reviewable artifacts; the uniformity guard compares both against every live header in that class. A pinning scenario declares any legitimate changed-header count, and its Markdown artifact records every full changed prompt. The pure helpers (`sessionFixtureNames`, `fixtureContext`, `normalizedHeaders`, `normalizedSystemPrompts`, `formatSystemPromptSnapshot`, `headerChangeCount`) are exported from the module for direct unit coverage.
+
+## Alternatives considered
+
+- **Copy the modules into each example** — the fork this Agent Note exists to prevent: the record/guard logic is exactly the code that must stay byte-identical across suites, and examples are outside the coverage gate, so each copy is also unmeasured.
+- **A shared module directory under `examples/`** — keeps the code outside the coverage gate and forces relative imports across example boundaries, against the package-name import convention; `examples/` leaves stay thin by design.
+- **A `/testing` subpath export of `dsh-acp-demo`** — couples test infrastructure into a product package's surface and dependency set; `packages/support/` exists precisely for real-but-lower-compatibility dev/test packages, with `dsh-llm-replay` as the precedent this package completes.
+- **Export raw test-body functions instead of a suite factory** — each example would re-own the `describe`/`it` skeleton (~80 lines of registration boilerplate per suite) for no flexibility gain; the factory keeps consumers to a scenario table plus one call, and the exported pure helpers preserve unit-testability inside the factory design.
+- **An injectable ACP `Client` factory instead of declarative `permissionAnswers`** — maximally flexible, but it leaks SDK client construction to every consumer and reopens per-example drift in exactly the layer being unified; a declarative queue keeps `input.json` the single scripting surface and compatible with expected-output normalization.
+- **Generalize beyond ACP (a transport-agnostic snapshot harness)** — no second transport exists; the harness is ACP-shaped end to end (SDK client, JSON-RPC frames, `session/update` waiters), and a speculative abstraction would be a seam split ahead of any consumer.
+
+## Testing
+
+Extraction parity was proven mechanically: after the move, `pnpm run test:snapshot` matched the base commit's result with zero byte changes under `examples/acp-agent/tests/snapshots/`. The package's `src/` holds per-file 100% statements/branches/functions/lines under the gating unit run, driven through the real launcher by a scripted fake ACP bin (`tests/fixtures/fake-acp-agent.ts`, behavior scripted per scenario via a `behavior.json` beside the fixture): `harness.spec.ts` directly covers launcher defaults, captures, update waiting, shutdown, and environment/config variants, then covers every scenario step op, both expect-error arms, the permission queue (selection, fallback, impossible-click), workspace seeding, and the harvest ordering/noise/fallback branches; `suite.spec.ts` runs the factory for real at collection time — a replay suite over committed synthetic fixtures and a record suite over a temp copy (write-back never touches the committed tree; `ACP_SNAPSHOT_SPEC_BOOTSTRAP=1` re-bootstraps it) — plus direct cases for the pure helpers. The fake bin substitutes the `session/new` cwd, not `process.cwd()`, into scripted logs, matching what the real bin's header carries (darwin realpaths `/var/folders/…` to `/private/var/folders/…`).
+
+## Consequences
+
+A new example gets the whole snapshot tier from a scenario table plus fixtures, while an ordinary ACP e2e gets the same tested process/client boundary from one launcher call. The costs: `suite.ts` imports vitest, so the package entry is importable only inside a vitest run — a shape no other package has, stated in its README; and each suite pins its own ~8 KB header fixture (a genuinely distinct composition deserves its own pin; an identical one would be caught by that suite's uniformity guard).
diff --git a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml
new file mode 100644
index 0000000000..133198a4d2
--- /dev/null
+++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml
@@ -0,0 +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-18-tui-terminal-state-snapshots.md: 8e86588f69fdb9d615232252ecf57309d440f1cd
+2026-07-18-tui-terminal-state-snapshots.zh.md: b70a46830f44e9da663e30745fcdb7ad281592da
diff --git a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
new file mode 100644
index 0000000000..8e86588f69
--- /dev/null
+++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
@@ -0,0 +1,70 @@
+# Agent Note: Snapshot semantic terminal state for the TUI
+
+Status: implemented
+
+English | [中文](2026-07-18-tui-terminal-state-snapshots.zh.md)
+
+## Problem
+
+The TUI is a stateful renderer. Its user-visible result depends on ANSI parsing, differential frames, wrapping, scrollback, viewport position, terminal width, focus, cursor state, and each tool's presentation intent. Unit tests that collect `Terminal.write()` fragments can prove event handling, but they cannot prove the final screen a terminal displays. The same screen may also be emitted through different write fragments, so pinning those fragments creates false regressions.
+
+Component-line snapshots stop before ANSI reaches a terminal and miss cursor movement, clearing, styling, overlay composition, and reflow. Raster screenshots include font and platform rendering noise that is unrelated to the TUI contract. A completed flow built by directly appending plausible session events has another blind spot: it proves the renderer accepts those shapes, not that the production agent loop and tool implementations produce them.
+
+The TUI therefore needs a deterministic, reviewable representation of terminal state, recorded model journeys that execute the real downstream stack, and a smaller test at the real process and PTY boundary.
+
+## Decision
+
+TUI coverage has four complementary layers:
+
+1. `packages/ui/tui/tests/tui.spec.ts` tests event mapping, input routing, disposal, and error behavior directly.
+2. `packages/ui/tui/tests/tui.snapshot.ts` mounts the production TUI against a headless terminal emulator for transient states that a completed session log cannot retain: in-flight streaming, pending tool calls, overlays, expansion, compaction reflow, errors, and shutdown.
+3. `examples/tui-agent/tests/tui.snapshot.ts` replays committed JSONL session logs through the production agent loop and real tools, then compares the resulting semantic terminal state.
+4. `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` boots the real Loader composition in a PTY, drives a scripted conversation through streaming and `ask_user_question`, and verifies startup, input, exit, failure reporting, and terminal restoration.
+
+The runnable TUI has its own `examples/tui-agent` leaf beside the Headless and ACP leaves. It owns the interactive coding backends and tools directly and loads `@deepseek-ai/dsh-tui-demo`; TUI snapshots and PTY tests live with that leaf. The [redundant-agent removal](../simplification/2026-07-20-remove-stdio-and-echo-agents.md) owns this consolidation.
+
+### Recorded-session replay
+
+Each example-level scenario directory owns `session.jsonl`, optional child logs `session..jsonl`, and `terminal.expected.txt`. The primary log supplies user-authored `user/message` prompts and the recorded `assistant/chunk` sequence. `dsh-llm-replay` derives one model-call script per session, binds child logs to fresh child sessions, and is the only mocked boundary. The agent loop, bash and filesystem implementations, Code Mode worker, subagent provider, workflow worker, Cordis tools, presenters, and TUI are production implementations.
+
+The suite rejects a journey when its tool-call sequence differs, an expected event count is missing, a tool result is an error, a turn ends in error, a workflow lifecycle is incomplete, or the live child-session count differs from the fixture set. These assertions prevent an attractive terminal expected output from hiding a failed or bypassed production path.
+
+The live-model fixtures use `DSH_SNAPSHOT=record`; record mode rewrites their primary and child JSONL logs and terminal expected outputs. The deterministic Cordis toolchain keeps an authored complete JSONL script because reliably coercing a live model through five exact tool boundaries and two children is not a stable recording contract. `DSH_SNAPSHOT=refresh` replays every committed script keylessly and rewrites only derived terminal expected outputs. Plain replay compares without writing, and unknown mode values fail loud.
+
+### Semantic terminal projection
+
+The package-local `HeadlessTerminal` implements the same pi-tui `Terminal` interface as the process terminal and feeds every ANSI write into the pinned `@xterm/headless` parser. Snapshot code waits for synchronized frames to quiesce before reading state, so a checkpoint represents a completed screen rather than a timer-dependent write prefix.
+
+Each expected output projects dimensions, active-buffer and viewport coordinates, lifecycle and cursor state, rows, wrap markers, and non-default style ranges into text. Scroll-heavy cards capture the used buffer; overlays capture the visible viewport. Text and style remain separate so a reviewer can distinguish content changes from presentation changes without decoding ANSI bytes.
+
+Every checkpoint enforces theme independence across the complete terminal state: no RGB colors, no palette entries beyond ANSI 0–15, and no explicit background colors. Reverse video remains valid for selection because it uses terminal defaults. Both suites own closed inventories that reject missing scenarios, missing checkpoints, and orphaned expected output files.
+
+### Required scenario matrix
+
+| Layer | Scenario | Contract pinned |
+|---|---|---|
+| Recorded journey | Multi-turn conversation | Recorded reasoning/text chunks, two input turns, retained history, token totals, and idle editor state |
+| Recorded journey | Todo plan | Real `todo_write` execution, result card, and persistent plan rendering |
+| Recorded journey | Bash terminal card | Real local executor output, description, exit status, and completed terminal card |
+| Recorded journey | Parallel filesystem reads | Two calls from one assistant message, real file contents, ordering, and separate completed cards |
+| Recorded journey | Code Mode | Real `run_code` worker execution, two `tool/code-dispatch` events, captured program output, and completed card |
+| Recorded journey | Dynamic workflow | Real workflow worker, phase lifecycle, replayed child session, structured return value, and completed card |
+| Recorded journey | Cordis dynamic toolchain | Real mount, Code Mode inspect, direct subagent, workflow child, unmount, and all production presenters |
+| Transient state | Streaming and pending advanced calls | In-flight reasoning/text plus pending Code Mode, workflow, and Cordis cards that disappear from completed logs |
+| Transient state | Cards, interaction, layout, failure, and shutdown | Collapsed/expanded card families, question validation, compaction replacement, resize reflow, help/errors, cursor restoration, and terminal stop |
+
+## Alternatives considered
+
+- **Snapshot raw terminal writes** — rejected because differential rendering may change write boundaries without changing the screen, while cursor and clear sequences are unreadable in review.
+- **Snapshot component render lines before terminal output** — rejected because it does not test ANSI parsing, cursor movement, overlays, viewport behavior, or independent components in one frame.
+- **Build every completed flow by appending session events** — rejected because a hand-authored event sequence can drift from the agent loop, tool execution, child-session binding, or worker behavior while its presentation test stays green. Direct event construction remains limited to transient renderer states.
+- **Reuse ACP stdout expected outputs as the TUI oracle** — rejected because a recorded model journey is transport-neutral but its presentation is not. TUI scenarios own terminal expected outputs while using the same JSONL replay vocabulary.
+- **Commit raster screenshots** — rejected because fonts, glyph metrics, antialiasing, and host terminal themes make them platform-sensitive and make semantic style changes difficult to review.
+- **Use only PTY end-to-end tests** — rejected because raw PTY output is a stream of historical drawing operations, not queryable final state. PTY tests retain the real Loader/input/teardown boundary, while the emulator owns broad state coverage.
+
+## Consequences
+
+- Completed advanced snapshots now fail when the real Code Mode, workflow, subagent, filesystem, bash, or Cordis path breaks, rather than accepting a fabricated result event.
+- TUI visual regressions produce readable cell-and-style diffs, while JSONL fixtures retain the exact model chunks that made the production path execute.
+- The emulator uses xterm's proposed buffer API. An xterm upgrade requires rerunning and reviewing the semantic projection; terminal-specific behavior still needs the PTY smoke.
+- Expected outputs deliberately encode wrapping and viewport behavior at fixed sizes. Intentional layout changes use keyless refresh, while model-journey changes use record mode and review both JSONL and terminal diffs.
diff --git a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md
new file mode 100644
index 0000000000..b70a46830f
--- /dev/null
+++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md
@@ -0,0 +1,70 @@
+# Agent Note: TUI 语义终端状态快照
+
+Status: implemented
+
+[English](2026-07-18-tui-terminal-state-snapshots.md) | 中文
+
+## 问题
+
+TUI 是有状态的渲染器。用户最终看到的结果取决于 ANSI 解析、差分帧、换行、回滚缓冲、视口位置、终端宽度、焦点、光标状态,以及各工具的呈现意图。收集 `Terminal.write()` 片段的单元测试可以验证事件处理,却无法验证终端最终显示的画面。同一画面也可能由不同的写入片段产生,因此固定这些片段会制造误报。
+
+组件行快照止于 ANSI 进入终端之前,无法覆盖光标移动、清屏、样式、浮层组合和重排。栅格截图会带入与 TUI 契约无关的字体和平台渲染噪声。直接追加看似合理的会话事件来构造完整流程还存在另一处盲区:这种测试只能证明渲染器接受这些数据形态,无法证明生产环境的 agent loop(智能体循环)和工具实现会生成这些事件。
+
+因此,TUI 既需要确定、便于评审的终端状态表示,也需要通过已录制模型流程执行真实下游组件,并保留一项范围更小、覆盖真实进程与 PTY 边界的测试。
+
+## 决策
+
+TUI 覆盖分为四个互补层次:
+
+1. `packages/ui/tui/tests/tui.spec.ts` 直接测试事件映射、输入路由、资源释放和错误行为。
+2. `packages/ui/tui/tests/tui.snapshot.ts` 将生产 TUI 挂载到无界面终端模拟器,覆盖完整会话日志无法保留的瞬态:进行中的流式输出、待完成工具调用、浮层、展开状态、压缩重排、错误和关闭过程。
+3. `examples/tui-agent/tests/tui.snapshot.ts` 通过生产 agent loop 和真实工具回放已提交的 JSONL 会话日志,再比较生成的语义终端状态。
+4. `examples/tui-agent/tests/tui-keyless-smoke.e2e.ts` 在 PTY 中启动真实 Loader 组合,驱动一段经过流式输出和 `ask_user_question` 的脚本化会话,并验证启动、输入、退出、失败报告和终端恢复。
+
+可运行 TUI 在 `examples/tui-agent` 中拥有独立叶节点,与 Headless 和 ACP 叶节点并列。它直接拥有交互式 coding 后端与工具,并加载 `@deepseek-ai/dsh-tui-demo`;TUI 快照和 PTY 测试也归属这个叶节点。[移除重复 agent 的决策](../simplification/2026-07-20-remove-stdio-and-echo-agents.md)负责此次整合。
+
+### 已录制会话回放
+
+每个示例级场景目录都包含 `session.jsonl`、可选的子会话日志 `session..jsonl`,以及 `terminal.expected.txt`。主日志提供用户来源的 `user/message` 提示词和已录制的 `assistant/chunk` 序列。`dsh-llm-replay` 为每个会话派生一份模型调用脚本,并将子日志绑定到新建的子会话;这是测试中唯一的 mock 边界。agent loop、bash 与文件系统实现、Code Mode worker、subagent 提供方、工作流 worker、Cordis 工具、呈现器和 TUI 都使用生产实现。
+
+如果工具调用顺序不符、预期事件数量不足、工具结果报错、轮次以错误结束、工作流生命周期不完整,或者实时子会话数量与 fixture(测试前置数据)集合不一致,测试都会失败。即使终端预期输出表面正确,这些断言也能阻止失败或被绕过的生产路径混入结果。
+
+真实模型 fixture 通过 `DSH_SNAPSHOT=record` 更新;录制模式会重写其主会话与子会话 JSONL 日志以及终端预期输出。确定性的 Cordis 工具链保留一份人工编写的完整 JSONL 脚本,因为要求真实模型稳定经过五个指定工具边界和两个子会话并不是可靠的录制契约。`DSH_SNAPSHOT=refresh` 会无密钥回放所有已提交脚本,并且只重写派生的终端预期输出。普通回放只比较而不写入,未知模式值会快速失败。
+
+### 语义终端投影
+
+包内的 `HeadlessTerminal` 实现与进程终端相同的 pi-tui `Terminal` 接口,并把每次 ANSI 写入交给固定版本的 `@xterm/headless` 解析器。读取状态前,快照代码会等待同步帧稳定,因此每个检查点表示已经完成的画面,而不是依赖计时的写入前缀。
+
+每份预期输出把终端尺寸、活动缓冲区和视口坐标、生命周期与光标状态、各行、换行标记以及非默认样式区间投影为文本。滚动内容较多的卡片捕获已使用缓冲区;浮层捕获可见视口。文本和样式相互分离,评审人无需解码 ANSI 字节即可区分内容变化与呈现变化。
+
+每个检查点还会对完整终端状态强制执行主题无关性:禁止 RGB 颜色、禁止 ANSI 0–15 以外的调色板项,也禁止显式背景色。选择行使用终端默认色进行反显,因此仍然有效。两套测试都拥有封闭清单,会拒绝缺失的场景、缺失的检查点和遗留预期输出文件。
+
+### 必需场景矩阵
+
+| 层次 | 场景 | 固定的契约 |
+|---|---|---|
+| 已录制流程 | 多轮会话 | 已录制的推理与文本分片、两轮输入、保留历史、token 总量和空闲编辑器状态 |
+| 已录制流程 | Todo 计划 | 真实 `todo_write` 执行、结果卡片和持久计划渲染 |
+| 已录制流程 | Bash 终端卡片 | 真实本地执行器输出、说明、退出状态和已完成终端卡片 |
+| 已录制流程 | 并行文件读取 | 同一条 assistant 消息中的两次调用、真实文件内容、顺序和两个独立完成卡片 |
+| 已录制流程 | Code Mode | 真实 `run_code` worker 执行、两条 `tool/code-dispatch` 事件、捕获的程序输出和已完成卡片 |
+| 已录制流程 | 动态工作流 | 真实工作流 worker、阶段生命周期、回放的子会话、结构化返回值和已完成卡片 |
+| 已录制流程 | Cordis 动态工具链 | 真实挂载、Code Mode 检查、直接 subagent、工作流子会话、卸载和全部生产呈现器 |
+| 瞬态 | 流式输出与待完成高级调用 | 进行中的推理和文本,以及完整日志中不会保留的待完成 Code Mode、工作流和 Cordis 卡片 |
+| 瞬态 | 卡片、交互、布局、失败和关闭 | 折叠与展开的卡片族、问题校验、压缩替换、尺寸重排、帮助与错误、光标恢复和终端停止 |
+
+## 曾考虑的替代方案
+
+- **快照原始终端写入**:不予采纳,因为差分渲染可能在画面不变时改变写入边界,而且光标与清屏序列难以评审。
+- **快照进入终端输出之前的组件渲染行**:不予采纳,因为它无法测试 ANSI 解析、光标移动、浮层、视口行为,也无法测试独立组件在同一帧中的相互作用。
+- **通过追加会话事件构造所有完整流程**:不予采纳,因为人工编写的事件序列可能与 agent loop、工具执行、子会话绑定或 worker 行为发生偏差,但呈现测试仍然保持绿色。直接构造事件只用于渲染器瞬态。
+- **复用 ACP stdout 预期输出作为 TUI 判定依据**:不予采纳,因为已录制模型流程与传输方式无关,其呈现方式却并非如此。TUI 场景使用同一套 JSONL 回放词汇,但拥有独立的终端预期输出。
+- **提交栅格截图**:不予采纳,因为字体、字形度量、抗锯齿和宿主终端主题会使结果依赖平台,也会增加语义样式变更的评审难度。
+- **只使用 PTY 端到端测试**:不予采纳,因为原始 PTY 输出是一系列历史绘制操作,而不是可查询的最终状态。PTY 测试保留真实 Loader、输入与清理边界,模拟器负责广泛的状态覆盖。
+
+## 后果
+
+- 当真实 Code Mode、工作流、subagent、文件系统、bash 或 Cordis 路径损坏时,已完成高级快照会失败,不会继续接受伪造的结果事件。
+- TUI 视觉回归会产生便于阅读的单元格和样式 diff,而 JSONL fixture 会保留触发生产路径的确切模型分片。
+- 模拟器使用 xterm 的拟议缓冲区 API。升级 xterm 时必须重新运行并评审语义投影;终端特有行为仍需由 PTY 冒烟测试覆盖。
+- 预期输出有意固定指定尺寸下的换行与视口行为。预期布局变更使用无密钥刷新;模型流程变更使用录制模式,并同时评审 JSONL 与终端 diff。
diff --git a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md b/.agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md
similarity index 91%
rename from docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md
rename to .agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md
index 9eb4c585a4..91679a0a6c 100644
--- a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md
+++ b/.agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md
@@ -1,10 +1,10 @@
-# RFC: Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)
+# Agent Note: Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)
Status: proposed
## Problem
-The harness models its core vocabulary — content blocks, message sources, finish reasons, turn triggers, turn-end reasons, and session events — as **merge-extensible maps**: a TypeScript `interface` (e.g. `SessionEventMap`, `ContentBlockMap`) that plugins augment via declaration merging, with the public union derived as `Map[keyof Map]`. This is the repo's universal extension pattern, documented in [docs/architecture.md](../../../architecture.md) ("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`") and relied on by the `defineTool` `InferArgs` DSL and the `assertNever` exhaustiveness convention.
+The harness models its core vocabulary — content blocks, message sources, finish reasons, turn triggers, turn-end reasons, and session events — as **merge-extensible maps**: a TypeScript `interface` (e.g. `SessionEventMap`, `ContentBlockMap`) that plugins augment via declaration merging, with the public union derived as `Map[keyof Map]`. This is the repo's universal extension pattern, documented in [docs/architecture.md](../../../../docs/architecture.md) ("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`") and relied on by the `defineTool` `InferArgs` DSL and the `assertNever` exhaustiveness convention.
The pattern is **compile-time only**. The types vanish at runtime: there is no schema object to validate an incoming value against, parse untrusted input with, or enumerate at runtime. The [session-persistence contract](../../implemented/architecture/2026-06-14-session-persistence.md) exposes two consequences:
@@ -13,7 +13,7 @@ The pattern is **compile-time only**. The types vanish at runtime: there is no s
This raises whether the event vocabulary should move to **Zod** or another runtime-schema library so durable and plugin boundaries have runtime schemas rather than erased types.
-This RFC scopes that question without proposing an implementation.
+This Agent Note scopes that question without proposing an implementation.
## Why this is not a persistence change
@@ -30,7 +30,7 @@ A migration of the event/vocabulary surface to runtime schemas touches, at minim
- **The event producers** — 16 `session.append(...)` call sites in the loop — unchanged in shape but now validated at the boundary.
- **~7 switch-consumers** that branch on these unions: `deriveMessages` (`dsh-session`), `BlockAssembler` (`dsh-llm`), the `dsh-invariants` plugin, both LLM adapters (`dsh-llm-deepseek`, `dsh-llm-pi-ai`), and the tool schema layer (`dsh-tools`). The `assertNever`-on-closed-unions vs fall-through-on-extensible-unions convention (a documented lint rule) would need rethinking — runtime variants are not statically exhaustive.
- **The `defineTool` `InferArgs` DSL** (`dsh-tools`), which derives zero-cast `execute` arg types from a compile-time schema spec — the showcase of the current approach.
-- **Docs**: architecture.md (the pattern is described as foundational), [dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md), and any RFC that references the pattern.
+- **Docs**: architecture.md (the pattern is described as foundational), [dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md), and any Agent Note that references the pattern.
This is a repository-wide vocabulary redesign, not a persistence implementation detail.
@@ -56,11 +56,11 @@ Replace the merge-extensible maps with a runtime registry the producers contribu
## Proposal
-Defer. If runtime validation is wanted at the durable boundary, **Option B** (schemastery on closed header and metadata shapes) is the proportionate step within the existing convention. **Option C** is an architecture decision that requires its own implementation RFC, including a choice between Zod and schemastery.
+Defer. If runtime validation is wanted at the durable boundary, **Option B** (schemastery on closed header and metadata shapes) is the proportionate step within the existing convention. **Option C** is an architecture decision that requires its own implementation Agent Note, including a choice between Zod and schemastery.
## Acceptance criteria
-- Option C proceeds only through its own implementation RFC, never as a persistence side effect.
+- Option C proceeds only through its own implementation Agent Note, never as a persistence side effect.
- If Option B is taken up, the closed header/metadata shapes (the JSONL `isHeaderLine` guard and kin) validate through schemastery in place of hand-rolled guards, with the merge-extensible maps untouched.
## Risks
diff --git a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml
similarity index 61%
rename from docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml
rename to .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml
index 6e301ee06a..83a2d4d788 100644
--- a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml
+++ b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-15-sdk-project-editing-architecture.md: 985cc22c159c68801b78262aa96c7422bdfa1318
-2026-07-15-sdk-project-editing-architecture.zh.md: 6a194e8e5f193e62bfc283fd93a5fde0367fe196
+2026-07-15-sdk-project-editing-architecture.md: 8335af516dbaa85f4adb85286f976ce9be2c9da8
+2026-07-15-sdk-project-editing-architecture.zh.md: bec39cc896887678b2d3f74832a9d13d7b354d6e
diff --git a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
similarity index 99%
rename from docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
rename to .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
index 985cc22c15..8335af516d 100644
--- a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
+++ b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
@@ -1,4 +1,4 @@
-# RFC: SDK project editing architecture
+# Agent Note: SDK project editing architecture
Status: proposed
@@ -16,7 +16,7 @@ Structured files are modified through document objects, while one-shot text arti
## Terminology
-| Term | Usage in this RFC | Meaning |
+| Term | Usage in this Agent Note | Meaning |
|---|---|---|
| Feature | feature | A product unit curated and managed by the SDK; one feature may contain several feature options and contribute several Cordis config entries, npm dependencies, environment placeholders, and owned files |
| Feature option | feature option | A finite selectable implementation or configuration shape within one feature; feature rules may make options fixed, exclusive, or additive |
diff --git a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md
similarity index 99%
rename from docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md
rename to .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md
index 6a194e8e5f..bec39cc896 100644
--- a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md
+++ b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md
@@ -1,4 +1,4 @@
-# RFC: SDK 工程编辑架构
+# Agent Note: SDK 工程编辑架构
Status: proposed
diff --git a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md b/.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
similarity index 88%
rename from docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
rename to .agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
index 9657512082..e6c5d9eb1e 100644
--- a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
+++ b/.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md
@@ -1,10 +1,10 @@
-# RFC: Pre-tool input rewrite — a consistent design
+# Agent Note: Pre-tool input rewrite — a consistent design
Status: proposed
## Problem
-The [interception-seams RFC](../../implemented/feature/2026-06-30-interception-seams.md) defines `tools/pre-execute` as an allow/deny/ask gate over an execution whose identity is already protected and whose arguments are deeply frozen. Claude Code's `PreToolUse` hook also offers `updatedInput`, so a faithful bridge needs an explicit rewrite mechanism. A rewrite cannot be a mutation escape hatch on the existing execution object: it must keep the durable history, audit record, presentation, and executed value consistent.
+The [interception-seams Agent Note](../../implemented/feature/2026-06-30-interception-seams.md) defines `tools/pre-execute` as an allow/deny/ask gate over an execution whose identity is already protected and whose arguments are deeply frozen. Claude Code's `PreToolUse` hook also offers `updatedInput`, so a faithful bridge needs an explicit rewrite mechanism. A rewrite cannot be a mutation escape hatch on the existing execution object: it must keep the durable history, audit record, presentation, and executed value consistent.
## The problem: three readers of pre-execution arguments
diff --git a/docs/rfc/proposed/feature/2026-07-06-recallable-compaction.md b/.agents/notes/proposed/feature/2026-07-06-recallable-compaction.md
similarity index 94%
rename from docs/rfc/proposed/feature/2026-07-06-recallable-compaction.md
rename to .agents/notes/proposed/feature/2026-07-06-recallable-compaction.md
index 1de660ac31..3f54030f9d 100644
--- a/docs/rfc/proposed/feature/2026-07-06-recallable-compaction.md
+++ b/.agents/notes/proposed/feature/2026-07-06-recallable-compaction.md
@@ -1,4 +1,4 @@
-# RFC: Recallable compaction — index checkpoints, a state checkpoint, and in-session history recall
+# Agent Note: Recallable compaction — index checkpoints, a state checkpoint, and in-session history recall
Status: proposed
@@ -16,7 +16,7 @@ Split the checkpoint into two classes and make shadowed history reachable.
### Frozen index checkpoints
-Newly stale history splits into chunks by deterministic policy: accumulate toward `chunkTokens`, snap edges to balanced tool-pairing cuts (`isToolPairingBalanced`), prefer turn boundaries, and place the final boundary as close to the retain boundary as balance allows, so the trailing slice shrinks to roughly one turn. Each chunk is compacted by one `compactRegion` call into an **index stub** (`stubTokens`, ~100–200 tokens):
+Newly stale history splits into chunks by deterministic policy: accumulate toward `chunkTokens`, snap edges with `toolPairingBalancedBefore` / `toolPairingBalancedAfter`, prefer turn boundaries, and place the final boundary as close to the retain boundary as balance allows, so the trailing slice shrinks to roughly one turn. Each chunk is compacted by one `compactRegion` call into an **index stub** (`stubTokens`, ~100–200 tokens):
- two or three lines of what happened;
- a keyword line of low-frequency literal anchors — exact error strings, values, config keys — grouped by kind;
@@ -58,7 +58,7 @@ The design ships as a new backend `dsh-compact-recallable` on the existing `ctx.
- **Tool-result pruning** (the in-flight pruning service): its replacement nodes carry `sourceEventSeqs`; the same registry fold lists pruned results as recallable. Follow-up scope; neither blocks the other.
- **Provider-usage token accounting** (the in-flight move of compaction pressure onto provider-reported usage): supplies the guard's accounting; the implementation stacks after it.
-- **"Query sessions" backlog item**: the cross-session generalization; this RFC scopes to the live session with tool names and rendering chosen so that work extends rather than collides.
+- **"Query sessions" backlog item**: the cross-session generalization; this Agent Note scopes to the live session with tool names and rendering chosen so that work extends rather than collides.
- **Training**: when to recall is a learned behavior. The deterministic footers and keyword anchors give training a stable target, and recall usage is fully visible in the session log for trajectory export; benchmark and RL design proceed with the post-training side.
### Follow-ups
@@ -96,7 +96,7 @@ Specified during review, deferred until observation calls for them:
- Nothing commits before all summaries exist and the guard passes on like-for-like accounting; a guard failure commits nothing and does not fail the turn; a mid-commit kill resumed at the next pre-step completes the pass with the state region committed unconditionally, merge base read from the log; a legacy head checkpoint is adopted as state-class.
- `history_read` renders any logged checkpoint's span under budget with a working cursor; `history_search` covers every shadowed span with checkpoint-id snippets and coverage metadata, asserted in particular by finding content that exists only in a span shadowed by a superseded state checkpoint — the regression pin for trailing-slice reachability; both reject non-agent callers and never-existing ids or orphaned `compact/start` with typed errors; recalled content appears as ordinary `tool/result`s; request-reconstruction invariants pass over sessions with compaction plus recall; one keyless snapshot scenario covers compact-then-recall end to end; tool schemas and the prompt section are byte-identical across passes.
- On the long-horizon bench suite: task success does not regress against `compact-basic` at equal budgets; a handoff-fidelity probe (restate K known decisions and constraints after a pass) scores no worse; recall usage frequency and hit usefulness are reported per run via the dsh bench report pipeline, alongside the stub-directory attention measurement and cache-hit telemetry.
-- Seam JSDoc, the compaction capability-seam RFC, `architecture.md`, and the generated tool, config, persistence, and module-graph catalogs update in the same change; all budgets live in config; new source directories hold per-file 100% coverage with HMR disposal tests.
+- Seam JSDoc, the compaction capability-seam Agent Note, `architecture.md`, and the generated tool, config, persistence, and module-graph catalogs update in the same change; all budgets live in config; new source directories hold per-file 100% coverage with HMR disposal tests.
## Risks
diff --git a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md b/.agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
similarity index 60%
rename from docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
rename to .agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
index bf4e85e020..620f39c4a4 100644
--- a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
+++ b/.agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
@@ -1,10 +1,10 @@
-# RFC: Claude Code and Codex subagent backends (out-of-process delegation to external coding agents)
+# Agent Note: Claude Code and Codex subagent backends (out-of-process delegation to external coding agents)
Status: proposed
## Problem
-Add isolated subagent providers for Claude Code and Codex. The existing [named-provider seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) and [ACP backend](../../implemented/feature/2026-06-22-acp-subagent-backend.md) establish the process-boundary shape. A harness turn should be able to delegate a self-contained task to either product and receive its final answer without exposing parent secrets or inheriting host configuration from `~/.claude` or `~/.codex`.
+The subagent seam ([the seam Agent Note](../../implemented/feature/2026-06-21-subagent-capability-seam.md)) hosts multiple named providers on `ctx.subagents`, and the ACP backend ([the ACP backend Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md)) proved the seam generalizes across a process boundary; its Future-providers section explicitly named the Codex app-server and the Claude Code Agent SDK as mechanically similar siblings. Those two are the engines actually worth delegating to today: a harness turn should be able to hand a self-contained task to a real Claude Code or a real Codex — a separate product with its own model, tools, and sandbox — and get back one final answer, without the parent deployment leaking its secrets into the child or the child's behavior silently depending on whatever `~/.claude` / `~/.codex` state exists on the host machine.
## Proposal
@@ -12,9 +12,9 @@ Two sibling provider packages, structural variants of the ACP backend, plus one
- `@deepseek-ai/dsh-subagent-claude-code` — drives a Claude Code child through `@anthropic-ai/claude-agent-sdk`'s `query()` (the SDK runs in the parent process and spawns its bundled `claude` CLI as the subprocess). Provider name `claude-code`: the child is the Claude Code *product*, not an Anthropic model adapter — "claude" stays reserved for a future `dsh-llm` adapter.
- `@deepseek-ai/dsh-subagent-codex` — spawns `codex app-server` and drives one thread/turn over its JSON-RPC-over-stdio protocol with a hand-rolled newline-JSON client (~200–300 lines) in the package.
-- `@deepseek-ai/dsh-subagent-process` — a pure library (the `subagent-inprocess` precedent) extracting what `dsh-subagent-acp` already carries and both new backends need: the credential env scrub (`SENSITIVE_ENV_PATTERN`/`buildChildEnv`), the EOF → SIGTERM → SIGKILL dispose ladder, and new isolated-config-dir helpers (`mkdtemp` create, best-effort remove). The ACP backend migrates onto it; `bash-local`'s sibling copy is left alone to bound the change.
+- `@deepseek-ai/dsh-subagent-process` — a pure library (the `subagent-inprocess` precedent) extracting what `dsh-subagent-acp` already carries and both new backends need: the credential env scrub (`buildChildEnv`), the EOF → SIGTERM → SIGKILL dispose ladder, and new isolated-config-dir helpers (`mkdtemp` create, best-effort remove). The ACP backend migrates onto it; `bash-local`'s sibling copy is left alone to bound the change.
-Both providers follow the ACP backend contract: a fresh child per `start`, one prompt round-trip, no inherited parent context or advertised optional capabilities, ignored `request.parent` and `request.agentOptions`, and a random branded agent id. `result` never rejects; child failures map to stop reasons while the original error reaches the logger. Each mounts `dsh-tool-subagent` under a distinct tool name. The tool result is the only new model-visible artifact, so no new session event is required; workspace mutations remain ambient side effects outside transcript replay.
+Both providers copy the ACP backend's seam posture verbatim: fresh child per `start`, exactly one prompt round-trip, capabilities all `false`, `inheritsParentContext: false`, `request.parent`/`request.agentOptions` ignored, `id = SessionId(randomUUID())`, `result` never rejects — child-level failure flattens to a stop reason and the original error goes to `ctx.logger` via an `onError` spec callback. Model exposure is zero new code: `dsh-tool-subagent` is loaded once per provider with a distinct `toolName` (`subagent_claude_code`, `subagent_codex`). No new session events are needed — the only model-visible artifact is the tool result, so reconstructability holds exactly as it did for ACP. To be explicit about the boundary: the session log reconstructs the model-visible transcript, not workspace mutation history — a child granted write access mutates files as an ambient side effect outside the log, exactly as the bash tools and the ACP backend already do; replay reproduces requests, not the disk.
## Verified interface facts (pinned versions)
@@ -31,11 +31,11 @@ Both integration surfaces were verified against pinned implementations before th
## Isolation and credentials
-Authentication is API-key-only. Each run uses a fresh config directory (`CLAUDE_CONFIG_DIR` with `settingSources: []`, or `CODEX_HOME`) that is removed best-effort on dispose; config may instead select a persistent directory. The shared child-env helper forwards ordinary values such as `PATH`, `HOME`, `TMPDIR`, locale, and proxy settings, removes credential-shaped names, and overlays explicit `config.env`. Claude Code receives its API key through that overlay, while Codex receives it through `account/login/start` rather than a hand-written auth file.
+Deployments authenticate with API keys only, and the child must not see the host user's Claude Code / Codex configuration: behavior has to be a function of `cordis.yml` alone. Each run gets a fresh `mkdtemp` config dir — `CLAUDE_CONFIG_DIR` for Claude Code (paired with an explicit `settingSources: []`), `CODEX_HOME` for Codex — removed best-effort on dispose; a config field can pin a persistent dir instead. The child env reuses the ACP backend's `buildChildEnv` semantics verbatim via the extraction: the ambient env is forwarded MINUS credential-shaped vars (`/KEY|SECRET|TOKEN/i`), with `config.env` layered on top — so `PATH`, `HOME`, `TMPDIR`, locale, and proxy vars survive and the CLIs run normally, while only credential-shaped ambient vars are scrubbed (`ANTHROPIC_API_KEY` enters explicitly through `config.env` for Claude Code), and the Codex key travels via the `account/login/start` RPC into the isolated `CODEX_HOME` rather than a hand-written `auth.json`.
## Permission and approval policy
-Each backend exposes its engine's native policy vocabulary. Claude Code defaults to `permissionMode: default` with `permission: reject`; Codex defaults to `sandboxMode: read-only`, `approvalPolicy: never`, and the same rejected fallback. Examples opt into `acceptEdits` or `workspace-write`. Known approval, user-input, and elicitation requests receive the configured answer; unknown methods receive method-not-found and unknown notifications are consumed. No prompt reaches a human, and no child can wait indefinitely for unavailable input.
+Instead of collapsing to ACP's single `permission: allow|reject` knob, each backend exposes its engine's native vocabulary as config, with conservative defaults: Claude Code gets `permissionMode` (default `default`) plus `permission: allow|reject` (default `reject`) as the `canUseTool` auto-answer for whatever falls through; Codex gets `sandboxMode` (default `read-only`) and `approvalPolicy` (default `never`) plus the same `permission` fallback for approval requests that still arrive. Defaults are deliberately do-no-harm (the out-of-box child cannot write files); examples demonstrate opening up (`acceptEdits` / `workspace-write`). The mechanical rule: EVERY server-initiated request is settled programmatically and promptly — the enumerated approval/user-input/elicitation requests by the configured policy, an unknown request method with a JSON-RPC method-not-found error response (never left pending), unknown notifications consumed — so no child request can wedge a turn waiting on an answer that will never come. Prompts never reach a human in this cut, matching ACP.
## StopReason mapping
@@ -45,11 +45,11 @@ Liveness posture, stated explicitly: teardown timing is config, turn duration is
## Testing
-Coverage is required at each applicable tier:
+Named at every tier per the root AGENTS.md rule, and de-risked up front:
-- **Keyless unit/integration:** drive a fake Claude CLI through the real SDK and a scripted Codex app-server through the real wire client. At per-file 100% coverage, exercise round trips, every stop mapping, both cancellation paths and pre-abort, permission policies, unknown messages, spawn failure, reload cleanup, export shape, scrubbed environments, temporary-directory removal, and Codex auth precheck failure.
-- **With-key e2e:** each real engine performs file work under `acceptEdits` or `workspace-write`; skips name the missing binary or key and assert no child process remains.
-- **Snapshot:** deferred as `TODO(claude-code-subagent-replay)` and `TODO(codex-subagent-replay)` pending the process-specific replay shape described by the [subagent replay RFC](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md).
+- **Keyless unit/integration**, mirroring the ACP spec list per backend (round-trip and output accumulation, every stop mapping, both cancel paths, already-aborted, permission auto-answer under both policies, unknown-message tolerance, bad-command spawn failure, HMR provider cleanup, export shape, isolation assertions on child env and temp-dir removal; Codex adds the auth-precheck failure path). Claude Code's harness is a scripted fake `claude` executable behind `pathToClaudeCodeExecutable` driven by the REAL SDK — a spike already passed end-to-end keyless in 24ms (the fake CLI answers one `control_request/initialize` and speaks plain stream-json, ~40 lines). Codex's harness is a scripted mock app-server subprocess speaking the verified wire protocol, the `mock-acp-server.ts` shape.
+- **With-key e2e** per backend: the real engine does real file work verified on disk, under a pinned opened-up config so acceptance and the do-no-harm defaults don't collide — `permissionMode: 'acceptEdits'` for Claude Code, `sandboxMode: 'workspace-write'` + `approvalPolicy: 'never'` for Codex; self-skips report exactly what is missing (binary vs key). CI has no secrets, so these run locally per the with-key policy.
+- **Snapshot**: deferred as `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` — the same distinct replay shape the ACP backend deferred ([the per-session replay Agent Note](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md)); the keyless suites carry deterministic coverage meanwhile.
## Alternatives considered
@@ -59,7 +59,7 @@ The dispose ladder and env scrub require owning the child process (spawn args, e
### Why not a model-visible `subagent_type` parameter (one Task-style tool)?
-Claude Code's own Task tool puts the subagent type in the model-facing schema, selecting a prompt-plus-toolset persona. Here the choice is between EXECUTION ENGINES, and only the deployer knows which engines have credentials configured — so selection stays deployment config, preserving `dsh-tool-subagent`'s documented one-provider-per-tool contract. A persona-style type selector would be a separate RFC against the tool, not the backends.
+Claude Code's own Task tool puts the subagent type in the model-facing schema, selecting a prompt-plus-toolset persona. Here the choice is between EXECUTION ENGINES, and only the deployer knows which engines have credentials configured — so selection stays deployment config, preserving `dsh-tool-subagent`'s documented one-provider-per-tool contract. A persona-style type selector would be a separate Agent Note against the tool, not the backends.
### Why not login-state credentials and the user's own config?
@@ -71,7 +71,7 @@ Injecting a fake `query()` would mock our own boundary and leave the real SDK lo
### Why not ACP adapters (e.g. `claude-code-acp`) reusing the existing backend?
-Community shims wrap both engines in ACP, which would make them "just config" on `dsh-subagent-acp`. But that inserts an unofficial third-party layer between the harness and the engine, erases the native control surfaces this RFC exposes (permissionMode, sandboxMode/approvalPolicy, config-dir isolation, apiKey RPC), and trades first-party protocol stability for a shim's release cadence. First-party surfaces — the Agent SDK and the app-server — are the supported integration points.
+Community shims wrap both engines in ACP, which would make them "just config" on `dsh-subagent-acp`. But that inserts an unofficial third-party layer between the harness and the engine, erases the native control surfaces this Agent Note exposes (permissionMode, sandboxMode/approvalPolicy, config-dir isolation, apiKey RPC), and trades first-party protocol stability for a shim's release cadence. First-party surfaces — the Agent SDK and the app-server — are the supported integration points.
## Acceptance criteria
diff --git a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.md b/.agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md
similarity index 95%
rename from docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.md
rename to .agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md
index 7caceed4ba..f2e3e74ca1 100644
--- a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.md
+++ b/.agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md
@@ -1,4 +1,4 @@
-# RFC: Interactive side sessions and merge-back
+# Agent Note: Interactive side sessions and merge-back
Status: proposed
@@ -13,7 +13,7 @@ A **side session** is an ordinary live session forked at the source's last compl
- **Fork and attach:** create the child with the parent's balanced completed-turn prefix and stamp `parentSession` and `seedLength` in its metadata. This composes `ctx.agents.create({ seed, meta })`; it adds no core service or session-store method.
- **Advisor framing:** inject one plugin-sourced `context/message` after creation that tells the child to explain without mutating or continuing the task. Keeping the system prompt byte-identical preserves the provider prefix cache over inherited history.
- **Merge-back:** ask the child for a length-capped handback, then inject one plugin-sourced `context/message` into the parent. The next parent request sees it at its logged position, preserving replay and [request reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) without a new session event.
-- **Presentation:** invocation, session switching, and handback rendering belong to the first client-owned surface. This RFC specifies only the surface-independent mechanics.
+- **Presentation:** invocation, session switching, and handback rendering belong to the first client-owned surface. This Agent Note specifies only the surface-independent mechanics.
Rewind productization, session-tree views, a model-facing side-session tool, and `forkName`/`mergedInto` metadata are out of scope. A live-adapter spike validated source-log isolation, inherited context, a multi-turn child exchange, and merge-back visibility in the parent's next turn.
diff --git a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md b/.agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md
similarity index 97%
rename from docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md
rename to .agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md
index 36216f74d8..d643d223dd 100644
--- a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md
+++ b/.agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md
@@ -1,4 +1,4 @@
-# RFC: SQLite FTS5 session search
+# Agent Note: SQLite FTS5 session search
Status: proposed
@@ -44,7 +44,7 @@ Reconciliation may use stable fingerprints to avoid rewriting unchanged persiste
- Tests cover both search scopes, content-bearing results, chainable result filters, surface defaults, snippets, escaping, deterministic ties, pagination, scoped stale cursors, cancellation, dynamic persistence mount/unmount, and recovery after a failed transaction.
- A schema mismatch resets only the derived database.
- A keyless end-to-end test combines a real persistence backend with the real SQLite search package.
-- The RFC is amended to the measured tokenizer and public API actually implemented before moving to `implemented/`.
+- The Agent Note is amended to the measured tokenizer and public API actually implemented before moving to `implemented/`.
## Risks
diff --git a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md b/.agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md
similarity index 99%
rename from docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md
rename to .agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md
index 200ed50b1e..0c6516080a 100644
--- a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md
+++ b/.agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md
@@ -1,4 +1,4 @@
-# RFC: Stream workflow progress through tool calls
+# Agent Note: Stream workflow progress through tool calls
Status: proposed
diff --git a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml
similarity index 64%
rename from docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml
rename to .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml
index 36bb26161f..f64160a5a0 100644
--- a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml
+++ b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-2026-07-14-sdk-developer-projects.md: 0b5fe876f92153e1ccf5bd8fe383464f5087f4b1
-2026-07-14-sdk-developer-projects.zh.md: ec08f323acba1b9dc049182937fda34bfb50d4ee
+2026-07-14-sdk-developer-projects.md: aa5cf64d7dd33dea229d74c2ae45a9244ee70e3c
+2026-07-14-sdk-developer-projects.zh.md: 8f7d1de5b16f38019c802f07eda701cee72deb4f
diff --git a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md
similarity index 93%
rename from docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md
rename to .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md
index 0b5fe876f9..aa5cf64d7d 100644
--- a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md
+++ b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md
@@ -1,4 +1,4 @@
-# RFC: Developer-owned SDK projects
+# Agent Note: Developer-owned SDK projects
Status: proposed
@@ -44,7 +44,7 @@ The table is the developer-visible support set for this phase. A `required` feat
| Feature | Create state | Feature options | Constraints and relationships |
|---|---|---|---|
| `provider` | required | `deepseek` (default) / `custom` | DeepSeek collects an API key; custom also collects a base URL, and a CLI option may override the model name |
-| `app` | required | `stdio` (default) / `acp` / `embed` | Selects the run interface |
+| `app` | required | `tui` (default) / `acp` / `embed` | Selects the run interface |
| `spine` | required | `default` | Timer, the LLM seam, session storage, system prompt, the tool registry, the agent registry, and the agent loop |
| `bash` | required | `local` (default) / `sandbox` | The two feature options are exclusive and independent of the run interface, and both install the model-facing bash tool; sandbox installs the local sandbox provider and sandboxed bash backend |
| `persistence` | required | `jsonl` (default) / `sqlite` | Every project selects exactly one persistence backend |
@@ -59,9 +59,9 @@ The table is the developer-visible support set for this phase. A `required` feat
| `hooks` | optional | `claude` (default) / `codex`, multiple | Each feature option creates a separate editable configuration file |
| `guard` | optional | `repeat-tool` | Provides repeated-tool-call reminders |
| `timeout-policy` | optional | `default` | Applies a uniform policy to tools that declare timeout budgets |
-| `ask-user` | optional | `default` | Provides the `ask_user_question` tool; only `acp` and `stdio` can select it because those two feature options provide the injected user-interaction service |
+| `ask-user` | optional | `default` | Provides the `ask_user_question` tool; only `acp` and `tui` can select it because those two feature options provide the injected user-interaction service |
-Both `bash` feature options apply to ACP, stdio, and embed and are not selected by the run interface. The sandbox feature option writes no active config key and therefore keeps `dsh-bash-sandbox`'s `read-only` default. Generated `cordis.yml` includes a commented example that developers can change explicitly to `workspace-write`:
+Both `bash` feature options apply to ACP, TUI, and embed and are not selected by the run interface. The sandbox feature option writes no active config key and therefore keeps `dsh-bash-sandbox`'s `read-only` default. Generated `cordis.yml` includes a commented example that developers can change explicitly to `workspace-write`:
```yaml
- id: bash
@@ -72,11 +72,11 @@ Both `bash` feature options apply to ACP, stdio, and embed and are not selected
# workspaceRoot: !!js process.cwd()
```
-Feature contributions reference only single-plugin npm packages and never bundle packages such as `agent-spine-demo`, `stdio-demo`, or `acp-demo`. Plugins outside the table are not managed by create in this phase; advanced developers may still compose them by editing the ordinary project files directly.
+Feature contributions reference only single-plugin npm packages and never bundle packages such as `agent-spine-demo`, `tui-demo`, or `acp-demo`. Plugins outside the table are not managed by create in this phase; advanced developers may still compose them by editing the ordinary project files directly.
## Generated project
-With default answers, an npm project uses the DeepSeek provider, the stdio interface, local bash, JSONL persistence, and the preselected hmr, fs, todo, and skill features. Its initial tree is:
+With default answers, an npm project uses the DeepSeek provider, the TUI interface, local bash, JSONL persistence, and the preselected hmr, fs, todo, and skill features. Its initial tree is:
```text
my-agent/
@@ -106,7 +106,7 @@ Generated `package.json` provides the following scripts. `dev`, `build`, `start`
`dsh-sdk start` and `dsh-sdk dev` accept a module target and forward arguments after `--` unchanged to the project entrypoint. Generic argument parsing uses Node `parseArgs()` with zero schema: valued flags use `--key=value`, bare flags become `true`, and `--no-*` becomes `false`.
-- Stdio projects pass the selected model through `--model=` and create or resume an agent according to optional `--resume=`;
+- TUI projects pass the selected model through `--model=` and create or resume an agent according to optional `--resume=`;
- ACP uses protocol `session/load`
- Embed uses the model written into the generated code.
diff --git a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.zh.md b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md
similarity index 93%
rename from docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.zh.md
rename to .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md
index ec08f323ac..8f7d1de5b1 100644
--- a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.zh.md
+++ b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md
@@ -1,4 +1,4 @@
-# RFC: 开发者拥有的 SDK 工程
+# Agent Note: 开发者拥有的 SDK 工程
Status: proposed
@@ -44,7 +44,7 @@ create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `pl
| 功能 | create 状态 | 功能选项 | 限制与关系 |
|---|---|---|---|
| `provider` | required | `deepseek`(默认)/ `custom` | DeepSeek 收集 API key;custom 另收集 base URL,模型名可由 CLI 参数覆盖 |
-| `app` | required | `stdio`(默认)/ `acp` / `embed` | 选择运行接口 |
+| `app` | required | `tui`(默认)/ `acp` / `embed` | 选择运行接口 |
| `spine` | required | `default` | timer、LLM seam、会话存储、系统提示词、工具注册表、agent 注册表,以及 agent loop |
| `bash` | required | `local`(默认)/ `sandbox` | 两个功能选项互斥、与运行接口正交,且都安装面向模型的 bash 工具;sandbox 安装本地沙箱提供方和沙箱 bash 后端 |
| `persistence` | required | `jsonl`(默认)/ `sqlite` | 每个工程恰好选择一个持久化后端 |
@@ -59,9 +59,9 @@ create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `pl
| `hooks` | optional | `claude`(默认)/ `codex`,可多选 | 各功能选项生成独立的可编辑配置文件 |
| `guard` | optional | `repeat-tool` | 提供重复工具调用提醒 |
| `timeout-policy` | optional | `default` | 对声明超时预算的工具执行统一策略 |
-| `ask-user` | optional | `default` | 提供 `ask_user_question` 工具;注入的 user-interaction 服务由 acp/stdio 两个功能选项提供,因此仅这两个接口可选 |
+| `ask-user` | optional | `default` | 提供 `ask_user_question` 工具;注入的 user-interaction 服务由 acp/tui 两个功能选项提供,因此仅这两个接口可选 |
-`bash` 的两个功能选项都适用于 ACP、stdio 和 embed,不由运行接口决定。sandbox 功能选项不写任何生效的配置键,因而沿用 `dsh-bash-sandbox` 的 `read-only` 默认值;生成的 `cordis.yml` 保留注释示例,开发者可以显式改为 `workspace-write`:
+`bash` 的两个功能选项都适用于 ACP、TUI 和 embed,不由运行接口决定。sandbox 功能选项不写任何生效的配置键,因而沿用 `dsh-bash-sandbox` 的 `read-only` 默认值;生成的 `cordis.yml` 保留注释示例,开发者可以显式改为 `workspace-write`:
```yaml
- id: bash
@@ -72,11 +72,11 @@ create 还提供一次 `none / plugin / tool` 选择。`plugin` 固定生成 `pl
# workspaceRoot: !!js process.cwd()
```
-功能贡献只引用单插件 NPM 包,绝不引用 `agent-spine-demo`、`stdio-demo`、`acp-demo` 这类组合 NPM 包。表格之外的插件不由本期 create 管理;开发者仍可直接编辑普通工程文件进行高级组合。
+功能贡献只引用单插件 NPM 包,绝不引用 `agent-spine-demo`、`tui-demo`、`acp-demo` 这类组合 NPM 包。表格之外的插件不由本期 create 管理;开发者仍可直接编辑普通工程文件进行高级组合。
## 生成工程
-使用默认答案创建 npm 工程时,provider 为 DeepSeek,运行接口为 stdio,bash 为 local,持久化为 JSONL,hmr、fs、todo 与 skill 处于选中状态。初始目录树为:
+使用默认答案创建 npm 工程时,provider 为 DeepSeek,运行接口为 TUI,bash 为 local,持久化为 JSONL,hmr、fs、todo 与 skill 处于选中状态。初始目录树为:
```text
my-agent/
@@ -106,7 +106,7 @@ my-agent/
`dsh-sdk start` 与 `dsh-sdk dev` 可以接收模块 target,并把 `--` 后的参数原样转发给工程入口。通用参数解析使用 Node `parseArgs()` 的零 schema 模式:带值 flag 采用 `--key=value`,bare flag 转换为 `true`,`--no-*` 转换为 `false`。
-- stdio 工程通过 `--model=` 传入所选 model,并根据可选的 `--resume=` 创建或恢复 agent;
+- TUI 工程通过 `--model=` 传入所选 model,并根据可选的 `--resume=` 创建或恢复 agent;
- acp 使用协议 `session/load`
- embed 使用生成代码中的 model。
diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
new file mode 100644
index 0000000000..8b70484312
--- /dev/null
+++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
@@ -0,0 +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-17-sdk-follow-up-capabilities.md: 0f3ada6bdbb4ce933d14602cf59be9a51640e61c
+2026-07-17-sdk-follow-up-capabilities.zh.md: d0d0b3e6bcdf192e64f003dc9f6e90cc2bdb060b
diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
new file mode 100644
index 0000000000..0f3ada6bdb
--- /dev/null
+++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md
@@ -0,0 +1,118 @@
+# Agent Note: SDK follow-up capabilities
+
+Status: proposed
+
+English | [中文](2026-07-17-sdk-follow-up-capabilities.zh.md)
+
+## Problem
+
+The first SDK release creates and edits developer-owned Cordis projects through the shared model defined by the [developer-project Agent Note](2026-07-14-sdk-developer-projects.md) and the [project-editing architecture](../architecture/2026-07-15-sdk-project-editing-architecture.md). Its create and config workflows are interactive, external Cordis plugins require manual dependency and configuration edits, command-line telemetry has no owning boundary, and interactive branches lack a stable test strategy.
+
+These gaps are coupled. Create and config already share questions, feature configuration, and `ProjectEditSession`; adding separate automation paths would duplicate that domain logic. External-plugin installation must update both the package manager's files and `cordis.yml`. Telemetry must observe commands such as create and build that do not boot Cordis. Interactive testing must exercise Harness behavior without making terminal rendering a brittle product contract.
+
+## Proposal
+
+The SDK extends the existing prompt and project-editing boundaries instead of creating parallel workflows. A non-interactive prompt port and structured feature plan drive create and config, `dsh-sdk create ` delegates dependency resolution to the project package manager before mounting the resolved package through `ProjectEditSession`, launcher-side telemetry wraps `create-sdk` and every `dsh-sdk` command, and injected prompt streams provide the primary interactive-test seam.
+
+| Capability | Product entrypoint | Owning mechanism | Required outcome |
+|---|---|---|---|
+| Headless project creation | `create-sdk --config ` or `--config-json ` with optional `--json` | `HeadlessPromptPort`, structured project answers, and a complete feature plan | No terminal blocking; missing required input is explicit |
+| External Cordis plugin installation | `dsh-sdk create ` | Native package-manager `add` plus `ProjectEditSession` | The dependency and `cordis.yml` entry identify the package manager's resolved package |
+| Developer-cycle telemetry | `create-sdk` and every `dsh-sdk` command | Launcher-side consent, payload, redaction, anonymous identity, and delivery services | Reporting is best-effort and cannot change the command result |
+| Interactive regression coverage | Create and config tests | Injected `PromptPort` input/output and filesystem assertions | Tests cover Harness decisions and generated files without snapshotting terminal repainting |
+
+## Shared headless workflow
+
+### Structured input and lifecycle events
+
+Headless create accepts a JSON object either inline through `--config-json` or from a file through `--config`. Scalar fields supply the ordinary create answers, while `features` supplies the complete selected feature set, feature options, secrets, and dedicated values. Defaults remain valid only where the owning question declares one; the headless path never invents an answer for a required prompt.
+
+With `--json`, stdout is an NDJSON event stream. `done` means creation and any requested setup completed, `action-required` names an unanswered required prompt, and `error` reports another failure. Human-readable progress and package-manager output go to stderr so every stdout line remains parseable as one event. A caller responds to `action-required` by adding the missing value and running the command again.
+
+Create and config consume the same feature-plan shape. Create exposes it through the command-line inputs above; config uses it at the shared workflow boundary so a later automation entrypoint does not need a second feature-selection model.
+
+### Prompt and project-editing boundaries
+
+`PromptPort` remains the only boundary between SDK questions and an interaction implementation. `ClackPromptPort` handles terminals. `HeadlessPromptPort` consumes defaults exposed by the question contract and otherwise fails with the unanswered prompt; prefilled values normally prevent the port from being called.
+
+Both paths use the same `Question` objects, `FeatureConfigurator`, `SdkProject`, and `ProjectEditSession`. The headless path therefore changes how answers arrive, not how features are interpreted or files are committed.
+
+### Agent skill
+
+The repository ships a thin `SKILL.md` that teaches an agent to construct the structured input, request NDJSON, fill an `action-required` value, and retry. The skill invokes the public CLI and does not import an internal SDK API or introduce another project specification.
+
+## External Cordis plugin installation
+
+`dsh-sdk create ` accepts a package-manager-native npm specifier such as `pkg@version` or a GitHub specifier such as `github:owner/repo#ref`. After confirmation, it asks the project's package manager to add the source, compares the direct dependency names before and after the operation, reopens the project, and mounts each newly resolved package in `cordis.yml` through `ProjectEditSession`.
+
+The package manager owns source parsing, version or commit resolution, integrity data, lockfile updates, and any build policy. The SDK does not download or unpack a second copy through giget or pacote. An external plugin remains a dependency under `node_modules`; local plugin scaffolding remains a separate project-creation concern.
+
+## Launcher telemetry
+
+### Consent and collection
+
+Telemetry wraps the `create-sdk` initializer and the `dsh-sdk` launcher command lifecycle because project initialization, plugin creation, and build do not reliably boot Cordis. One event records the command name, duration, success, a random per-user anonymous identifier, and redacted `cordis.yml` and `package.json` text when those project files are eligible.
+
+Reporting is enabled unless a present telemetry config entry is explicitly disabled. `DO_NOT_TRACK` and CI deny reporting regardless of project configuration. A missing `cordis.yml` does not itself deny the event, but `package.json` content is included only when `cordis.yml` establishes that the directory is an SDK project.
+
+### Safety and delivery
+
+The payload builder never reads `.env`. It redacts secret-shaped keys and values, known token forms, PEM blocks, URL credentials, and high-entropy opaque strings in the two eligible text files. Redaction is a safety backstop rather than a guarantee; SDK projects must keep credentials in `.env`.
+
+The reporter uses a fixed endpoint and resolves every send path without throwing. Command dispatch records success or failure in a `finally` path, starts reporting after the command outcome is known, and drains within a bounded interval. Consent parsing, payload construction, storage, or network failures are swallowed only at this telemetry boundary and never alter the command's exit code.
+
+## Interactive workflow testing
+
+Create and config tests inject a `PromptPort` and scripted input/output streams into the existing workflows. Parameterized scenarios cover feature selection, feature options, secrets, cancellation, review, and apply behavior, then assert the resulting `cordis.yml` and other project files. The stable product assertion is the generated project state, not clack's ANSI redraw sequence.
+
+One or two optional real-PTY smoke tests may cover the shipped binary and TTY guard that injection cannot reproduce. Native PTY tooling does not belong on the required path unless it is reliable across the repository's supported Node and host versions.
+
+## Deferred work
+
+- Extend the headless create specification to express local `plugin` or `tool` scaffolding instead of defaulting that interactive choice to none.
+- Expose the telemetry opt-out in create and config while preserving the consent representation in which only a disabled telemetry entry is written.
+- Define whether GitHub source dependencies must be prebuilt or may run package-manager-controlled preparation scripts, and surface the policy before installation.
+- Replace the telemetry package's `.invalid` endpoint placeholder with the production endpoint before release.
+
+## Alternatives considered
+
+**Build a separate headless creation engine.** This would duplicate questions, feature requirements, configuration behavior, and project-editing rules. Reusing the prompt and edit-session boundaries keeps one implementation of project semantics.
+
+**Make a specification file the primary automation interface.** Agents can pass the same typed JSON object inline, while people and CI may still use a file. A file-only protocol adds persistence and cleanup without adding semantics.
+
+**Use `npx skills add` as the project creator.** The skills CLI installs Markdown skills; it does not create SDK projects or install npm packages. The agent skill therefore drives the SDK initializer instead of replacing it.
+
+**Fetch GitHub and npm sources through giget or pacote.** A second fetch layer would duplicate package-manager resolution, integrity, lockfile, and lifecycle policy. Native dependency specifiers keep those decisions in the selected package manager.
+
+**Implement telemetry as a Cordis runtime plugin.** Create and build do not necessarily boot Cordis, so a runtime plugin cannot observe the complete developer command cycle. The launcher is the boundary shared by those commands.
+
+**Derive the anonymous identifier from git metadata.** Repository remotes can identify a project or organization. A random per-user identifier supports aggregation without encoding repository identity.
+
+**Collect only aggregate counters.** Aggregate-only events reduce exposure but cannot answer which plugins, dependencies, and configuration shapes developers actually use. This proposal accepts collection of redacted project text and makes that exposure explicit.
+
+**Use real PTYs and transcript snapshots as the primary test strategy.** Native PTY dependencies and terminal repaint sequences add platform and rendering instability while mostly testing clack. Injected interaction plus generated-file assertions tests the SDK-owned behavior directly.
+
+## Acceptance criteria
+
+- Create runs without a TTY from a complete structured input, emits only NDJSON on stdout under `--json`, and reports missing required input as `action-required` without writing a partial project.
+- Create and config resolve the same feature-plan contract through the shared question, feature-configuration, and project-editing code paths.
+- `dsh-sdk create ` uses the selected project package manager, mounts the dependency name that operation actually added, and fails loudly when no new dependency can be identified.
+- The initializer and every `dsh-sdk` command reach one best-effort telemetry completion path; an explicit disabled entry, `DO_NOT_TRACK`, or CI prevents delivery, and telemetry failures never change the command result.
+- Telemetry never reads `.env`, withholds unrelated `package.json` content when no `cordis.yml` exists, redacts both eligible text payloads, and uses an identifier unrelated to git metadata.
+- Interactive tests cover create and config decisions through injected interaction and assert committed project files; any real-PTY coverage remains a narrow smoke layer.
+- The agent skill documents the public structured-input and event contracts without depending on private package exports.
+
+## Risks
+
+- Full redacted `cordis.yml` and `package.json` text still reveals plugin and dependency names, URLs, paths, and configuration values to the endpoint operator, and heuristic redaction can miss a secret.
+- Default-on reporting may surprise developers when no telemetry entry exists; the CLI must make the opt-out discoverable before release.
+- A package-manager add can change `package.json`, the lockfile, and installed files before `ProjectEditSession` mounts the plugin, so a later mount failure can leave dependency changes that require manual recovery.
+- GitHub dependencies may execute preparation or lifecycle code according to package-manager policy; an unresolved build policy is a supply-chain and reproducibility risk.
+- Injected prompt tests do not prove raw-mode, signal, or repaint behavior in a real terminal; the optional smoke layer must cover only those residual contracts.
+
+## References
+
+- [Vercel Eve](https://github.com/vercel/eve) and [Vercel Labs Skills](https://github.com/vercel-labs/skills) for the distinction between a headless initializer and skill distribution.
+- [npm package specifications](https://docs.npmjs.com/cli/v11/using-npm/package-spec), [pnpm add](https://pnpm.io/cli/add), and [Yarn add](https://yarnpkg.com/cli/add) for package-manager-native sources.
+- [`DO_NOT_TRACK`](https://donottrack.sh/) for the environment-level opt-out convention.
+- [Clack](https://github.com/bombshell-dev/clack) and [Vitest snapshots](https://vitest.dev/guide/snapshot) for injected prompts and generated-file assertions.
diff --git a/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
new file mode 100644
index 0000000000..d0d0b3e6bc
--- /dev/null
+++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
@@ -0,0 +1,118 @@
+# Agent Note: SDK 后续功能
+
+Status: proposed
+
+[English](2026-07-17-sdk-follow-up-capabilities.md) | 中文
+
+## 问题
+
+首个 SDK 版本通过[开发者工程 Agent Note](2026-07-14-sdk-developer-projects.md) 和 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md)定义的共享模型创建和编辑开发者拥有的 Cordis 工程。create 和 config 工作流仅支持交互调用,接入外部 Cordis 插件需要手工修改依赖和配置,命令行遥测没有明确的所属边界,交互分支也缺少稳定的测试策略。
+
+这些缺口彼此关联。create 和 config 已经共享问题、功能配置和 `ProjectEditSession`;若另建自动化路径,就会复制领域逻辑。安装外部插件必须同时修改包管理器文件和 `cordis.yml`。遥测需要观察 create、build 等不会启动 Cordis 的命令。交互测试需要覆盖 Harness 自身行为,同时避免把终端渲染固化成脆弱的产品契约。
+
+## 提案
+
+SDK 扩展现有提示词与工程编辑边界,不另建平行工作流。非交互式 `PromptPort` 实现和结构化功能计划驱动 create 与 config;`dsh-sdk create ` 先把依赖解析交给工程的包管理器,再通过 `ProjectEditSession` 挂载解析所得的包;启动器侧遥测包住 `create-sdk` 和每个 `dsh-sdk` 命令;交互测试主要通过注入的提示词输入输出流完成。
+
+| 功能 | 产品入口 | 所属机制 | 必须达到的结果 |
+|---|---|---|---|
+| Headless 工程创建 | `create-sdk --config ` 或 `--config-json `,可搭配 `--json` | `HeadlessPromptPort`、结构化工程答案和完整功能计划 | 不阻塞等待终端;明确报告缺失的必答输入 |
+| 外部 Cordis 插件安装 | `dsh-sdk create ` | 包管理器原生 `add` 加 `ProjectEditSession` | 依赖和 `cordis.yml` 配置项指向包管理器解析出的包 |
+| 开发周期遥测 | `create-sdk` 和每个 `dsh-sdk` 命令 | 启动器侧的上报条件判断、遥测内容构建、脱敏、匿名身份和传输服务 | 上报采用尽力而为语义,不能改变命令结果 |
+| 交互回归覆盖 | create 和 config 测试 | 注入的 `PromptPort` 输入输出和文件系统断言 | 测试覆盖 Harness 决策与生成文件,不快照终端重绘 |
+
+## 共享 headless 工作流
+
+### 结构化输入和生命周期事件
+
+Headless create 通过 `--config-json` 接收内联 JSON 对象,或通过 `--config` 从文件读取。标量字段提供普通 create 答案,`features` 提供完整的已选功能、功能选项、secret(密钥)和专用值。只有所属问题明确声明的默认值才有效;headless 路径绝不为必答问题臆造答案。
+
+使用 `--json` 时,stdout 是 NDJSON 事件流。`done` 表示创建及要求执行的安装和构建均已完成,`action-required` 指明一个尚未回答的必答问题,`error` 报告其他失败。面向人的进度信息和包管理器输出写入 stderr,确保 stdout 每一行都能解析成一个事件。调用方收到 `action-required` 后补充缺失值,再次运行命令。
+
+Create 和 config 使用相同的功能计划形状。create 通过上述命令行输入公开该形状;config 在共享工作流边界使用同一形状,使后续自动化入口无需另建功能选择模型。
+
+### Prompt 与工程编辑边界
+
+`PromptPort` 仍是 SDK 问题与交互实现之间的唯一边界。`ClackPromptPort` 负责终端交互。`HeadlessPromptPort` 使用问题契约公开的默认值,否则通过未回答问题快速失败;预填值通常会让流程根本不调用该 port。
+
+两条路径使用相同的 `Question` 对象、`FeatureConfigurator`、`SdkProject` 和 `ProjectEditSession`。因此,headless 路径只改变答案的到达方式,不改变功能解释或文件提交方式。
+
+### Agent skill
+
+仓库提供一份轻量 `SKILL.md`,指导 agent skill(智能体技能)构造结构化输入、请求 NDJSON、补充 `action-required` 指明的值并重试。该 skill 调用公开 CLI,不导入 SDK 内部 API,也不引入另一套工程规格。
+
+## 外部 Cordis 插件安装
+
+`dsh-sdk create ` 接受包管理器原生的 npm package specifier,例如 `pkg@version`,也接受 `github:owner/repo#ref` 等 GitHub package specifier。用户确认后,命令要求工程包管理器添加来源,对比操作前后的直接依赖名,重新打开工程,再通过 `ProjectEditSession` 把每个新增且已解析的包挂载进 `cordis.yml`。
+
+包管理器负责来源解析、版本或 commit 解析、`integrity` 数据、lockfile 更新和构建策略。SDK 不再通过 giget 或 pacote 下载、解压第二份副本。外部插件是 `node_modules` 下的依赖;本地插件脚手架仍属于独立的工程创建问题。
+
+## Launcher 遥测
+
+### Consent 与采集
+
+遥测包住 `create-sdk` 初始化命令与 `dsh-sdk` launcher 的命令生命周期,因为工程初始化、插件创建和 build 都不会稳定地启动 Cordis。每个事件记录命令名、时长、成败、随机生成的用户级匿名标识符,以及符合条件时经过脱敏的 `cordis.yml` 与 `package.json` 文本。
+
+除非当前存在的遥测配置项被明确禁用,否则允许上报。`DO_NOT_TRACK` 和 CI 无论工程配置如何都禁止上报。缺少 `cordis.yml` 本身不会禁止事件,但只有 `cordis.yml` 能证明目录是 SDK 工程时,遥测内容才包含 `package.json` 文本。
+
+### 安全与传输
+
+Payload 构建器绝不读取 `.env`。它会脱敏两个符合条件的文本文件中的疑似密钥键和值、已知 token 形式、PEM 块、URL 凭据和高熵不透明字符串。脱敏只是安全兜底,不能提供绝对保证;SDK 工程必须把凭据放进 `.env`。
+
+`TelemetryReporter` 使用固定 endpoint,每条发送路径都会正常结束且不抛错。命令分发通过 `finally` 路径记录成败,在命令结果已确定后启动上报,并在有界时间内等待传输结束。只有遥测边界会吞掉上报条件解析、遥测内容构建、存储或网络错误,这些错误绝不改变命令退出码。
+
+## 交互工作流测试
+
+Create 和 config 测试向现有工作流注入 `PromptPort` 和脚本化输入输出流。参数化场景覆盖功能选择、功能选项、secret、取消、评审和应用行为,再断言最终的 `cordis.yml` 及其他工程文件。稳定的产品断言是生成后的工程状态,不是 clack 的 ANSI 重绘序列。
+
+可以用一到两个可选的真实 PTY 冒烟测试覆盖注入无法复现的发布二进制和 TTY 检查。除非原生 PTY 工具在仓库支持的 Node 与宿主版本上足够可靠,否则它不进入必跑路径。
+
+## 延后工作
+
+- 扩展 headless create 规格,使其能表达本地 `plugin` 或 `tool` 脚手架,而不是把该交互选择默认为 none。
+- 在 create 和 config 中公开遥测关闭选项,同时保留只有禁用时才写入遥测配置项的上报许可表示。
+- 明确 GitHub 来源依赖必须预先构建,还是允许运行由包管理器控制的 preparation script(准备脚本),并在安装前向用户展示该策略。
+- 发布前把遥测包中的 `.invalid` endpoint 占位符替换为生产端点。
+
+## 曾考虑的替代方案
+
+**另建 headless 创建引擎。** 该方案会复制问题、功能依赖、配置行为和工程编辑规则。复用提示词与编辑会话边界,可以保证工程语义只有一份实现。
+
+**把规格文件作为主要自动化接口。** Agent 可以内联传入相同的类型化 JSON 对象,人和 CI 仍可选用文件。文件专用协议会增加持久化与清理工作,却不增加语义。
+
+**使用 `npx skills add` 创建工程。** Skills CLI 只安装 Markdown skill,不创建 SDK 工程,也不安装 npm 包。因此,agent skill 驱动 SDK 初始化命令,而不是取代它。
+
+**通过 giget 或 pacote 获取 GitHub 与 npm 来源。** 第二套获取层会复制包管理器的解析、完整性、lockfile 和生命周期策略。原生 package specifier 让这些决策留在所选包管理器中。
+
+**把遥测实现成 Cordis 运行时插件。** Create 和 build 不一定启动 Cordis,因此运行时插件无法观察完整的开发命令周期。Launcher 是这些命令共用的边界。
+
+**从 git 元数据派生匿名标识符。** 仓库的 git remote 可能识别工程或组织。随机的用户级标识符能够支持聚合,同时不编码仓库身份。
+
+**只采集聚合计数。** 仅聚合事件可以降低暴露,但无法回答开发者实际使用哪些插件、依赖和配置形状。本提案接受采集脱敏后的工程文本,并明确记录这项暴露。
+
+**把真实 PTY 和 transcript(文本记录)快照作为主要测试策略。** 原生 PTY 依赖与终端重绘序列会带来平台和渲染不稳定性,而且主要是在测试 clack。注入交互并断言生成文件,可以直接测试 SDK 拥有的行为。
+
+## 验收标准
+
+- Create 能依据完整结构化输入在没有 TTY 时运行;使用 `--json` 时 stdout 只输出 NDJSON;缺少必答输入时通过 `action-required` 报告,且不写入部分工程。
+- Create 和 config 通过共享的问题、功能配置和工程编辑代码路径解析相同的功能计划契约。
+- `dsh-sdk create ` 使用工程选定的包管理器,挂载该操作实际新增的依赖名;无法识别新增依赖时快速失败。
+- 初始化命令与每个 `dsh-sdk` 命令都进入同一条尽力而为的遥测收尾路径;明确禁用的配置项、`DO_NOT_TRACK` 或 CI 会阻止传输,遥测失败绝不改变命令结果。
+- 遥测绝不读取 `.env`;没有 `cordis.yml` 时不发送无关的 `package.json` 内容;两个符合条件的文本都经过脱敏;匿名标识符与 git 元数据无关。
+- 交互测试通过注入交互覆盖 create 和 config 决策,并断言已提交的工程文件;真实 PTY 覆盖只作为窄范围冒烟层。
+- Agent skill 说明公开的结构化输入与事件契约,不依赖包的私有导出。
+
+## 风险
+
+- 即使经过脱敏,完整的 `cordis.yml` 与 `package.json` 文本仍会向 endpoint 运营方暴露插件名、依赖名、URL、路径和配置值;启发式脱敏也可能漏掉 secret。
+- 没有遥测配置项时默认上报可能让开发者意外;发布前 CLI 必须让关闭方法易于发现。
+- 在 `ProjectEditSession` 挂载插件前,包管理器的 add 操作已经可能修改 `package.json`、lockfile 和安装文件;后续挂载失败会留下需要手工恢复的依赖改动。
+- GitHub 依赖可能按包管理器策略执行 preparation 或 lifecycle script;尚未解决的构建策略会带来供应链与可复现性风险。
+- 注入提示词交互的测试无法证明真实终端中的 raw mode、signal 或重绘行为;可选冒烟层只应覆盖这些残余契约。
+
+## 参考资料
+
+- [Vercel Eve](https://github.com/vercel/eve) 与 [Vercel Labs Skills](https://github.com/vercel-labs/skills) 用于区分 headless 初始化命令与 skill 分发。
+- [npm package specifications](https://docs.npmjs.com/cli/v11/using-npm/package-spec)、[pnpm add](https://pnpm.io/cli/add)和 [Yarn add](https://yarnpkg.com/cli/add)说明包管理器原生来源。
+- [`DO_NOT_TRACK`](https://donottrack.sh/)定义环境级关闭约定。
+- [Clack](https://github.com/bombshell-dev/clack) 和 [Vitest snapshots](https://vitest.dev/guide/snapshot) 说明注入提示词交互与生成文件断言。
diff --git a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.md b/.agents/notes/proposed/process/2026-06-11-api-extractor-reports.md
similarity index 81%
rename from docs/rfc/proposed/process/2026-06-11-api-extractor-reports.md
rename to .agents/notes/proposed/process/2026-06-11-api-extractor-reports.md
index a6f3bfb14c..b32dc7e2af 100644
--- a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.md
+++ b/.agents/notes/proposed/process/2026-06-11-api-extractor-reports.md
@@ -1,8 +1,8 @@
-# RFC: API extractor reports
+# Agent Note: API extractor reports
Status: proposed
-> Split out from the original "Doc-sync and API reports" RFC (2026-06-11). Parts 1-2 (doc-block typechecking, event-taxonomy verification) shipped — see [doc-sync enforcement](../../implemented/process/2026-06-11-doc-sync-enforcement.md). This is the deferred part 3, kept as a standalone proposal.
+> Split out from the original "Doc-sync and API reports" Agent Note (2026-06-11). Parts 1-2 (doc-block typechecking, event-taxonomy verification) shipped — see [doc-sync enforcement](../../implemented/process/2026-06-11-doc-sync-enforcement.md). This is the deferred part 3, kept as a standalone proposal.
## Problem
diff --git a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.md b/.agents/notes/proposed/process/2026-06-11-architectural-conformance.md
similarity index 93%
rename from docs/rfc/proposed/process/2026-06-11-architectural-conformance.md
rename to .agents/notes/proposed/process/2026-06-11-architectural-conformance.md
index e0d16b455d..006aa76ad1 100644
--- a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.md
+++ b/.agents/notes/proposed/process/2026-06-11-architectural-conformance.md
@@ -1,4 +1,4 @@
-# RFC: Architectural conformance — dependency rules and the adapter kit
+# Agent Note: Architectural conformance — dependency rules and the adapter kit
Status: proposed
@@ -31,4 +31,4 @@ dependency-cruiser config + CI step first (an hour of work, permanent guarantee)
Dep-cruiser rule maintenance as packages are added — keep rules pattern-based (`dsh-*`) rather than enumerated.
-
+
diff --git a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md b/.agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md
similarity index 97%
rename from docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md
rename to .agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md
index e787133026..a79f719751 100644
--- a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md
+++ b/.agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md
@@ -1,4 +1,4 @@
-# RFC: Supply chain checks and vendor drift verification
+# Agent Note: Supply chain checks and vendor drift verification
Status: proposed
diff --git a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.md b/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md
similarity index 70%
rename from docs/rfc/proposed/process/2026-06-20-discover-package-inventory.md
rename to .agents/notes/proposed/process/2026-06-20-discover-package-inventory.md
index c4ee6161bd..fa544d6ddb 100644
--- a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.md
+++ b/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md
@@ -1,10 +1,10 @@
-# RFC: Discover package inventories instead of maintaining static lists
+# Agent Note: Discover package inventories instead of maintaining static lists
Status: proposed
## Problem
-Package and gate inventories are repeated across TypeScript project references, package docs, CI prose, Knip overrides, and snapshot scenario metadata. Most restate package layout, manifest data, aggregate command contents, or fixture files. Each new package or scenario therefore creates avoidable synchronization points.
+Package and gate inventories are repeated across TypeScript project references, package docs, CI prose, and Knip overrides. Most restate package layout, manifest data, or aggregate command contents. Each new package therefore creates avoidable synchronization points.
The [package hierarchy](../../implemented/architecture/2026-06-20-package-hierarchy.md) already removed several of these by hand: `scripts/publint-all.ts` now derives its list from the `packages//` layout, and the two `tsconfig` `paths` maps collapsed to one `@deepseek-ai/dsh-*` wildcard. What remains is the inventory that cannot be globbed away — chiefly `tsconfig.build.json`'s project `references`, which TypeScript requires as an explicit array (no wildcard form).
@@ -16,7 +16,7 @@ Make the remaining package/gate inventories discoverable. A single canonical sou
The hierarchy does not need to encode every fact about a package, but it should encode the broad maintenance policy: core/product packages, integrations, capability seams, and support/test/example packages should not all require a hand-maintained exception list before scripts can tell them apart.
-Two of the cataloged items need no generator at all: folding the e2e entry glob into knip's default stanza deletes the per-package restatements outright, and `childSessions` can be discovered from each scenario's fixture directory, leaving the scenario table to declare only policy (`recorded`, `hasModelTurn`, `comparesLog`) — and even those track fixture-derivable facts today (`comparesLog` ⟺ the committed log has entries beyond its header line; `recorded` ⟺ `hasModelTurn` with no `replay.override.json` sibling), so each new scenario class keeps adding knobs the fixture directory already answers.
+One cataloged item needs no generator at all: folding the e2e entry glob into knip's default stanza deletes the per-package restatements outright.
## Acceptance criteria
@@ -25,10 +25,9 @@ Two of the cataloged items need no generator at all: folding the e2e entry glob
- Docs describe the source of truth rather than repeating generated inventories.
- CI invokes the aggregate commands and lets those commands own their sub-gate lists.
- `knip.json` carries a per-package override only where it encodes real information (an extra entry file, an ignored dependency), never a restatement of the default stanza.
-- Snapshot scenarios declare policy, not facts discoverable from their fixture directories.
## Risks
Discovery scripts can become too clever. The implementation should stay boring: read manifests, filter on explicit fields, print the resolved list, and fail loud. The payoff is removing manual inventory drift, not inventing a build system.
-
+
diff --git a/docs/rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md b/.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md
similarity index 96%
rename from docs/rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md
rename to .agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md
index 8098247fdf..6d4de215c9 100644
--- a/docs/rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md
+++ b/.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md
@@ -1,4 +1,4 @@
-# RFC: Periodic human-review maintenance for dsh-code-review
+# Agent Note: Periodic human-review maintenance for dsh-code-review
Status: proposed
@@ -8,7 +8,7 @@ The `dsh-code-review` skill records failure modes that require reviewer judgment
## Proposal
-Periodic out-of-repo maintenance. A private tool, kept on the skill maintainer's machine rather than committed to this repository, runs against a clean full-history checkout at refreshed `origin/master`. The intended scheduler runs daily with a two-UTC-day overlap; manual runs accept another `--since` duration or repeated `--pr` arguments for an explicit set. The scan is idempotent against the current skill and stores no repository cursor. The only repository file changed by promotion is [.agents/skills/dsh-code-review/SKILL.md](../../../../.agents/skills/dsh-code-review/SKILL.md); the draft PR carries a provenance summary so reviewers can audit the source feedback and adoption evidence without the private adapter logs.
+Periodic out-of-repo maintenance. A private tool, kept on the skill maintainer's machine rather than committed to this repository, runs against a clean full-history checkout at refreshed `origin/master`. The intended scheduler runs daily with a two-UTC-day overlap; manual runs accept another `--since` duration or repeated `--pr` arguments for an explicit set. The scan is idempotent against the current skill and stores no repository cursor. The only repository file changed by promotion is [.agents/skills/dsh-code-review/SKILL.md](../../../skills/dsh-code-review/SKILL.md); the draft PR carries a provenance summary so reviewers can audit the source feedback and adoption evidence without the private adapter logs.
```mermaid
flowchart TD
@@ -50,7 +50,7 @@ The promote helper starts from a clean checkout at refreshed `origin/master` and
### Where the mechanism lives
-The tool source, adapter binaries, provider credentials, and intended daily scheduler are kept private to the maintainer's machine rather than committed to this repository. This document specifies the protocol; the reference implementation is private infrastructure. The mechanism serves a single skill maintained by a single operator, so the ongoing cost of vetting mechanism edits through repository review outweighs any provenance benefit. If the mechanism is ever handed off to a second maintainer, that handoff is a follow-up RFC that revises this decision — the operator doc at [docs/cookbook/maintaining-dsh-code-review.md](../../../cookbook/maintaining-dsh-code-review.md) is the entry point for anyone taking over.
+The tool source, adapter binaries, provider credentials, and intended daily scheduler are kept private to the maintainer's machine rather than committed to this repository. This document specifies the protocol; the reference implementation is private infrastructure. The mechanism serves a single skill maintained by a single operator, so the ongoing cost of vetting mechanism edits through repository review outweighs any provenance benefit. If the mechanism is ever handed off to a second maintainer, that handoff is a follow-up Agent Note that revises this decision — the operator doc at [docs/cookbook/maintaining-dsh-code-review.md](../../../../docs/cookbook/maintaining-dsh-code-review.md) is the entry point for anyone taking over.
## Alternatives considered
@@ -80,4 +80,4 @@ Promotion from `proposed/` to `implemented/` requires all of the following to be
- **Two-non-candidate classifications routed to `excluded` without a dispute round.** When both classifiers say "not a candidate" but disagree on which non-candidate reason applies (for example `covered` vs `specific`), the item is excluded rather than re-evaluated. Both classifiers agree the item does not become new reviewer behavior, so a dispute round would not change the outcome.
- **Dual-reviewer independence beyond byte-hash distinctness is a deployment contract.** The tool refuses to run when the two commands resolve to byte-identical executables, but cannot verify that two distinct wrappers back different providers or models. Operators must configure independent primary and secondary adapters.
- **Best-effort compare-and-swap for candidate writes and rollback.** File-based CAS on POSIX is not truly atomic; the window is one event-loop tick. The tool targets single-user periodic maintenance and a truly concurrent editor is out of scope.
-- **Single-maintainer bus factor.** Because the mechanism lives on one machine, its interruption stops skill maintenance entirely until the operator restores service or hands off to a new maintainer through a follow-up RFC.
+- **Single-maintainer bus factor.** Because the mechanism lives on one machine, its interruption stops skill maintenance entirely until the operator restores service or hands off to a new maintainer through a follow-up Agent Note.
diff --git a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md
similarity index 84%
rename from docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md
rename to .agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md
index cca7c57b34..1f52bfac25 100644
--- a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md
+++ b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md
@@ -1,4 +1,4 @@
-# RFC: Prune dead public and result surface
+# Agent Note: Prune dead public and result surface
Status: proposed
@@ -6,14 +6,14 @@ Status: proposed
Several package-root exports, result fields, and convenience methods have no production consumer. They survive because tests import internals through public entry points or because a type anticipated a caller that never arrived. Each item is small in isolation, but together they enlarge the SDK contract, generated catalogs, documentation, and regression matrix without enabling a shipped path.
-The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and RFC prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/cordis/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory:
+The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and Agent Note prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/cordis/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory:
| Surface | Production evidence | Simplification |
| --- | --- | --- |
| `SurfaceManager.invalidate()` | Only its unit test calls it; seeding completes before the lazily-created manager exists and the session never replaces its log reference. | Delete it and its impossible wholesale-replacement contract. |
| `ToolExecutionResult.callId` | Every hook already receives the immutable `ToolExecution`; the loop and ACP correlate through the call/session event. No consumer reads the duplicate result field. | Remove the field, copy/mismatch guards, and tests that prove the duplicate cannot disagree. |
| `ReactLoopAgent` root export | Outside-package named imports are tests; production programs against `Agent` and creates/resumes through `ctx.agents`. | Return/interface-type `Agent` and make the concrete loop class package-internal; keep the deliberate synchronous config-only `AgentLoop.create()` path. |
-| `workflow-workerthread` protocol/runtime/session re-exports and named `WorkerWorkflowEngine` | Every package-name consumer uses the default engine; the workflow RFC already defines the worker wire protocol as private. | Keep the default plugin class/config contract; drop the duplicate named class export and keep protocol modules source-private. |
+| `workflow-workerthread` protocol/runtime/session re-exports and named `WorkerWorkflowEngine` | Every package-name consumer uses the default engine; the workflow Agent Note already defines the worker wire protocol as private. | Keep the default plugin class/config contract; drop the duplicate named class export and keep protocol modules source-private. |
| `code-runtime-worker` protocol/bootstrap re-exports | Outside-package production/e2e consumers use `WorkerCodeRuntime` and config, not `BootstrapPort`, `PatchableStream`, or worker message/boot types. | Keep the runtime class/config contract and make its wire/bootstrap vocabulary source-private. |
| ACP translation/presenter root exports | `agentOptions`, `streamSessionEventUpdate`, `todosToPlan`, `ToolPresenter`, `nullToolPresenter`, and `TerminalRendering` have only same-file or ACP-test consumers; the sole outside-package production consumer mounts the plugin namespace. | Keep `name`, `inject`, `Config`, `AcpConfig`, and `apply`; make translation/presentation helpers source-private and test them in-package. |
| `providerWording` and `completedTurnPrefix` root exports | Each has one same-package production caller; only the balanced-prefix helper has a same-package white-box test. | Make them source-private and test provider behavior. |
@@ -23,8 +23,9 @@ The production corpus is `packages/*/*/src`, example sources/config, and runtime
| `BlockAssembler.push()` return value | Both production callers ignore the returned completed block. | Return `void`; keep the deliberately public `blocks()`/`message()` contract. |
| `compactRegion`'s separate `session` argument | The fixed caller passes the same object already present as `agent.session`; the model-visible mount API can also call the method, but accepting two identities permits a mounted plugin to provide an incoherent pair. | Keep the manual-region seam while deliberately narrowing it to `agent.session` as the one source of truth. |
| `CompactionResult.startSeq`, `summarySeq`, `endSeq`, and `summary` | The production consumer reads only shadowed range/seq/token accounting; the durable log owns summary and event identity. | Remove the four result echoes while keeping both shared transcript renderers. |
-| `BasicCompactService` estimation/summarization visibility | No outside production caller invokes the five methods; the implemented RFC names only `estimateContentTokens()` and `summarize()` as subclass hooks. | Make those two `protected` and the three orchestration-only estimators private. |
+| `BasicCompactService` estimation/summarization visibility | No outside production caller invokes the five methods; the implemented Agent Note names only `estimateContentTokens()` and `summarize()` as subclass hooks. | Make those two `protected` and the three orchestration-only estimators private. |
| `CodeLogEntry.source`/`level` and `RunCodeMeta.dispatches` | Every production consumer maps logs to text; no presenter/model path reads the other fields or the persisted dispatch count. | Make code-runtime logs strings (or text-only entries) and remove result-meta dispatch plumbing; keep the local counter that mints deterministic dispatch ids. |
+| `CodeRuntime.language` and `CodeRuntime.isolation` | The worker backend supplies the only production values, while Code Mode and every other production caller invoke only `run()`. | Remove the unread descriptors while preserving the worker's language, isolation, budgets, cancellation, and disposal behavior. |
| `ToolNotFoundError.toolName`, `SystemPrompt.config`, and `BashTask.command` | Each stored public value has no production reader. | Drop the unread field while retaining error messages, resolved configuration behavior, and task lifecycle. |
| Backend package-root implementation helpers | The exact inventory below is called only through relative same-package imports. Production namespace imports mount the retained plugin contract without reading these properties; named root consumers are tests. | Retain each adapter/provider/service and its config/error contract; stop exporting the listed helper functions/constants at package roots. |
| Consumer package-root implementation helpers | The exact inventory below has only same-package production callers. Production namespace imports mount plugin contracts without reading helper properties; named root consumers are tests. | Retain plugin contracts and stable error codes; move tests to package-local modules or public behavior and stop exporting the listed helpers at package roots. |
@@ -50,8 +51,8 @@ Remove or demote every row as one bounded coordinated public-surface cleanup. Up
## Acceptance criteria
-- Exact-symbol searches show no removed surface outside this RFC and any implemented-RFC amendments.
-- Every surface listed in this RFC is absent or demoted as specified; deliberately retained extension/test contracts outside the inventory are unchanged.
+- Exact-symbol searches show no removed surface outside this Agent Note and any implemented-Agent Note amendments.
+- Every surface listed in this Agent Note is absent or demoted as specified; deliberately retained extension/test contracts outside the inventory are unchanged.
- Tool execution, compaction, both LLM adapters, both persistence backends, workflow isolation, and agent creation/resume retain their shipped behavior.
- Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass.
diff --git a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml
new file mode 100644
index 0000000000..50dfd13aab
--- /dev/null
+++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml
@@ -0,0 +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-19-make-jsonrpc-directional.md: 74de3c960a415a9a2601e57ec75f244ca753193d
+2026-07-19-make-jsonrpc-directional.zh.md: 76228ba56cfbd4fb86f39d0d0873d49edb13309b
diff --git a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md
new file mode 100644
index 0000000000..74de3c960a
--- /dev/null
+++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md
@@ -0,0 +1,46 @@
+# Agent Note: Make JSON-RPC completion and transport directional
+
+Status: proposed
+
+English | [中文](2026-07-19-make-jsonrpc-directional.zh.md)
+
+## Problem
+
+The JSON-RPC bridge models both endpoints as symmetric peers although the shipped protocol is directional. The TypeScript server accepts requests and emits responses or notifications, but its transport also implements unused outbound requests and inbound notification dispatch. The Python SDK sends requests and receives responses or notifications, but it also queues unused inbound server requests and exposes response helpers.
+
+`session/prompt` also reports one settled turn through two protocol shapes. The server emits `session.finished` and then returns the constant `{ accepted: true }`; the Python SDK discards that response and waits for the notification to recover the status. Because the response is written only after the handler returns, the notification necessarily precedes the constant response on the same stream.
+
+The unused halves add pending-request maps, generated IDs, request queues, close-time rejection paths, response helpers, and a second completion waiter without serving a production caller.
+
+## Proposal
+
+Specialize each endpoint to its actual role. The TypeScript transport will retain inbound requests, outbound responses, and outbound notifications. The Python client will retain outbound requests and inbound responses or notifications. Delete the opposite-direction request machinery from each side.
+
+Return the settled outcome directly from `session/prompt` as `{ status, reason }` after `agent.whenIdle()`. Delete `session.finished`, the constant acceptance response, and the Python post-response completion loop. `session.event` and subagent notifications still stream before the response, and durable session events remain the source for final-response reconstruction.
+
+## Implementation plan
+
+1. In `packages/ui/jsonrpc/src/server.ts`, replace `SessionPromptResult.accepted` with `status: 'ok' | 'error' | 'aborted'` and the captured `TurnEndReason`. `HarnessSdkServer.prompt()` will return `completed` as `ok`, `aborted` as `aborted`, and every other current or merge-extensible reason as `error`; reaching idle without a `turn/end` remains an invariant error. Remove only `session.finished`, leaving `session.event`, `subagent.started`, and `subagent.finished` unchanged.
+2. In `packages/ui/jsonrpc/src/transport.ts`, replace `JsonRpcTransportPeer` with a server-side notification surface and retain `onRequest()`, `notify()`, `start()`, `flush()`, and `close()`. Remove generated request IDs, the pending-response map, outbound `request()`, inbound response and notification dispatch, and close-time pending-request rejection. Incoming response- and notification-shaped frames will be ignored, while request result, method-not-found, and handler-error responses retain their current behavior and remain ordered after notifications emitted by the awaited handler.
+3. In `python/sdk/src/deepseek_harness/client.py`, `models.py`, and `__init__.py`, remove `IncomingRequest`, `_requests`, `notify()`, `next_request()`, `respond()`, and `respond_error()`. Add a public validated `SessionPromptResponse` carrying status and reason, return it from `session_prompt()`, and keep an explicit reader guard that ignores unexpected server-request frames instead of allowing them to match a response waiter.
+4. In `python/sdk/src/deepseek_harness/api.py`, build `TurnResult.status` and a new `TurnResult.reason` from `SessionPromptResponse`, then delete the `session.finished` branch and second completion loop. Keep the subscription open during the request and preserve `_request_raw()`'s final notification drain so the last `turn/end` event and any subagent notification written before the response are collected before `Session.run()` reconstructs the final assistant message.
+5. Replace the symmetric transport-pair cases in `packages/ui/jsonrpc/tests/transport.spec.ts` with raw client-input/server-output coverage, and update `server.spec.ts`, `plugin-apply.spec.ts`, and `built-scope-carrier.e2e.ts` for direct outcomes, ordering, overlap, shutdown, and the narrowed fake. Update `python/sdk/tests/test_client.py` for response-based settlement, unexpected-request-frame handling, callback and concurrency behavior, and the removed public helpers. Update the JSON-RPC and bilingual Python SDK READMEs, export JSDoc and declarations, `scripts/smoke-python-runtime.py`, and the Python single-executable snapshot.
+
+## Alternatives considered
+
+**Keep a generic symmetric JSON-RPC peer for future methods.** Server-initiated requests may eventually support interactive permissions, but no typed method or production consumer exists. The pre-release protocol can add the smallest required direction when that feature is designed instead of carrying an unexercised peer today.
+
+**Keep `session.finished` for streaming clients.** Turn settlement is not incremental data: the request response already marks the same boundary and follows all earlier notifications on the ordered stream. A second terminal notification creates two representations that clients must reconcile.
+
+## Acceptance criteria
+
+- The TypeScript endpoint cannot originate requests or consume notifications.
+- The Python endpoint cannot originate notifications or consume server requests.
+- `session/prompt` returns the authoritative `ok`, `error`, or `aborted` outcome and reason after turn settlement.
+- Session events and subagent lifecycle notifications emitted during the turn arrive before the response.
+- Same-session overlap rejection, framing, multibyte input, handler errors, flush, shutdown ordering, and final-response reconstruction retain their behavior.
+- TypeScript bridge tests, Python SDK tests, built JSON-RPC coverage, snapshots, and generated API documentation pass.
+
+## Risks
+
+This deliberately narrows the pre-release wire protocol. Raw clients listening only for `session.finished`, or embedders using the unused symmetric transport methods, must move to the prompt response. A future server-initiated request requires a new typed protocol addition rather than reusing generic dormant machinery.
diff --git a/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md
new file mode 100644
index 0000000000..76228ba56c
--- /dev/null
+++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md
@@ -0,0 +1,46 @@
+# Agent Note: 让 JSON-RPC 完成结果与传输方向单一化
+
+Status: proposed
+
+[English](2026-07-19-make-jsonrpc-directional.md) | 中文
+
+## 问题
+
+JSON-RPC 桥接层把两个端点都建模为对称的对等端,但实际协议具有固定方向。TypeScript 服务端接收请求并发出响应或通知,其传输层却还实现了未使用的出站请求和入站通知分发。Python SDK 发送请求并接收响应或通知,却还会把未使用的服务端入站请求放入队列,并公开响应辅助方法。
+
+`session/prompt` 还会用两种协议结构报告同一个已结束轮次。服务端先发出 `session.finished`,再返回常量 `{ accepted: true }`;Python SDK 丢弃该响应,转而等待通知以取得状态。响应只有在处理函数返回后才会写入,因此在同一条有序流上,通知必然先于这个常量响应。
+
+这些未使用的双向能力引入了待处理请求表、生成 ID、请求队列、关闭时的拒绝路径、响应辅助方法和第二套完成等待逻辑,却没有任何生产调用方使用。
+
+## 提案
+
+按实际角色收窄两个端点。TypeScript 传输层只保留入站请求、出站响应和出站通知。Python 客户端只保留出站请求以及入站响应或通知。删除两侧与实际方向相反的请求机制。
+
+在 `agent.whenIdle()` 完成后,由 `session/prompt` 直接返回 `{ status, reason }` 作为轮次结果。删除 `session.finished`、常量接纳响应以及 Python 中响应后的完成等待循环。`session.event` 与 subagent 通知仍在响应前流式发出,持久会话事件仍是最终响应重建的真源。
+
+## 实施计划
+
+1. 在 `packages/ui/jsonrpc/src/server.ts` 中,用 `status: 'ok' | 'error' | 'aborted'` 和捕获的 `TurnEndReason` 替换 `SessionPromptResult.accepted`。`HarnessSdkServer.prompt()` 把 `completed` 映射为 `ok`,把 `aborted` 映射为 `aborted`,把其他当前或可合并扩展的原因映射为 `error`;进入空闲状态却没有 `turn/end` 仍视为不变量错误。只删除 `session.finished`,保持 `session.event`、`subagent.started` 和 `subagent.finished` 不变。
+2. 在 `packages/ui/jsonrpc/src/transport.ts` 中,用服务端通知接口替换 `JsonRpcTransportPeer`,并保留 `onRequest()`、`notify()`、`start()`、`flush()` 和 `close()`。删除生成的请求 ID、待处理响应表、出站 `request()`、入站响应与通知分发,以及关闭时对待处理请求的拒绝逻辑。入站响应结构和通知结构将被忽略;请求结果、方法不存在与处理器错误响应保持原有行为,并继续排在被等待处理器发出的通知之后。
+3. 在 `python/sdk/src/deepseek_harness/client.py`、`models.py` 和 `__init__.py` 中,删除 `IncomingRequest`、`_requests`、`notify()`、`next_request()`、`respond()` 和 `respond_error()`。新增公开且经过校验的 `SessionPromptResponse` 来携带状态与原因,由 `session_prompt()` 返回该对象,并保留明确的读取保护:忽略意外的服务端请求帧,避免它们命中响应等待器。
+4. 在 `python/sdk/src/deepseek_harness/api.py` 中,根据 `SessionPromptResponse` 构造 `TurnResult.status` 和新增的 `TurnResult.reason`,再删除 `session.finished` 分支与第二个完成循环。请求期间保持订阅打开,并保留 `_request_raw()` 最后的通知排空步骤,确保写在响应前的最后一条 `turn/end` 事件与任何 subagent 通知,都会在 `Session.run()` 重建最终助手消息之前被收集。
+5. 用原始客户端输入与服务端输出覆盖替换 `packages/ui/jsonrpc/tests/transport.spec.ts` 中的对称传输对用例,并更新 `server.spec.ts`、`plugin-apply.spec.ts` 和 `built-scope-carrier.e2e.ts`,覆盖直接结果、顺序、重叠、关闭和收窄后的伪实现。更新 `python/sdk/tests/test_client.py`,覆盖基于响应的结束流程、意外请求帧处理、回调与并发行为,以及已删除的公开辅助方法。同步更新 JSON-RPC README、双语 Python SDK README、导出 JSDoc 与声明、`scripts/smoke-python-runtime.py` 和 Python 单可执行文件快照。
+
+## 备选方案
+
+**为未来方法保留通用的对称 JSON-RPC 对等端。** 服务端发起的请求将来可能用于交互式权限,但当前没有类型化方法或生产消费方。该功能完成设计后,预发布协议可以增加所需的最小方向,无需提前保留未使用的对等端能力。
+
+**为流式客户端保留 `session.finished`。** 轮次结束不是增量数据:请求响应已经标识同一个边界,并且在有序流中位于先前所有通知之后。第二条终止通知会产生两种结果表示,迫使客户端进行协调。
+
+## 验收标准
+
+- TypeScript 端点无法发起请求,也不消费通知。
+- Python 端点无法发起通知,也不消费服务端请求。
+- 轮次结束后,`session/prompt` 返回权威的 `ok`、`error` 或 `aborted` 状态及其原因。
+- 轮次中发出的会话事件与 subagent 生命周期通知都先于响应到达。
+- 同一会话的重叠拒绝、分帧、多字节输入、处理器错误、flush、关闭顺序与最终响应重建保持原有行为。
+- TypeScript 桥接测试、Python SDK 测试、构建后 JSON-RPC 覆盖、快照和生成的 API 文档全部通过。
+
+## 风险
+
+本提案会刻意收窄预发布协议格式。仅监听 `session.finished` 的原始客户端,以及使用未使用对称传输方法的嵌入方,都必须改为读取请求响应。未来若需要服务端发起请求,应新增类型化协议,而不是复用休眠的通用机制。
diff --git a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.md b/.agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md
similarity index 92%
rename from docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.md
rename to .agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md
index 2a30969ba4..c3b17c401a 100644
--- a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.md
+++ b/.agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md
@@ -1,4 +1,4 @@
-# RFC: Deterministic tests, the replay invariant fixture, and race stress
+# Agent Note: Deterministic tests, the replay invariant fixture, and race stress
Status: proposed
@@ -28,4 +28,4 @@ Land 1 and 2 together (they touch the same helpers); add the nightly job after t
Fake timers interact subtly with Promise scheduling in the loop — prefer event-driven waits; reserve fake timers for timer-service behavior itself.
-
+
diff --git a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.md b/.agents/notes/proposed/testing/2026-06-11-mutation-testing.md
similarity index 93%
rename from docs/rfc/proposed/testing/2026-06-11-mutation-testing.md
rename to .agents/notes/proposed/testing/2026-06-11-mutation-testing.md
index 8c68ccf09f..35df228b85 100644
--- a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.md
+++ b/.agents/notes/proposed/testing/2026-06-11-mutation-testing.md
@@ -1,4 +1,4 @@
-# RFC: Mutation testing as the coverage counterweight
+# Agent Note: Mutation testing as the coverage counterweight
Status: proposed
@@ -31,4 +31,4 @@ Stryker (`@stryker-mutator/vitest-runner`) over `packages/*/src`:
Runtime: mutation testing is expensive; per-file 100% coverage helps (every mutant is at least reached). If PR-scoped runs stay too slow, keep them nightly-only and rely on the score ratchet.
-
+
diff --git a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md b/.agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md
similarity index 95%
rename from docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md
rename to .agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md
index 3eedd92a2d..0c6652a94d 100644
--- a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md
+++ b/.agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md
@@ -1,4 +1,4 @@
-# RFC: Deep-readonly public surfaces
+# Agent Note: Deep-readonly public surfaces
Status: rejected — the pervasive `DeepReadonly` type flip is replaced by source-owned runtime immutability in `Session` plus relational development assertions. See [source-owned session immutability and dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md).
@@ -24,4 +24,4 @@ Introduce `DeepReadonly`, flip the session read paths, and fix the resulting com
`DeepReadonly` types can produce noisy errors at waterfall boundaries where mutation IS the API — keep the mutable/readonly boundary exactly at "logged vs in-flight" and document it in the session README.
-
+
diff --git a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md b/.agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md
similarity index 90%
rename from docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md
rename to .agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md
index 0fd4ca27f7..81dbe40ee4 100644
--- a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md
+++ b/.agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md
@@ -1,4 +1,4 @@
-# RFC: Make the shared example base providerless
+# Agent Note: Make the shared example base providerless
Status: rejected — superseded by [Extract example apps into packages](../../implemented/architecture/2026-06-20-extract-example-app-packages.md), which moves the spine into a `dsh-agent-spine-demo` bundle and deletes the `base*.yml` files, so there is no shared base YAML left to rename.
@@ -20,10 +20,10 @@ The shared base should contain only provider-neutral services and tools: `llm`,
- `examples/base-core.yml` is deleted.
- Real demo configs explicitly add the DeepSeek adapter.
- Snapshot replay config includes the same providerless base and its replay adapter.
-- The [examples README](../../../../examples/README.md), example-specific READMEs, and RFC references stop explaining "base = base-core plus adapter".
+- The [examples README](../../../../examples/README.md), example-specific READMEs, and Agent Note references stop explaining "base = base-core plus adapter".
## What we give up
Real demos lose one layer of convenience: each must opt into the adapter. That is the right default for examples, because adapter choice is the variable part and providerless wiring is the shared product core.
-
+
diff --git a/.agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md b/.agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md
new file mode 100644
index 0000000000..90c942411d
--- /dev/null
+++ b/.agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md
@@ -0,0 +1,36 @@
+# Agent Note: Generate the Agent Note index tables
+
+Status: rejected — a centralized generated list is merge-prone and adds little discovery value
+
+## Problem
+
+Per-lifecycle/per-class tables would list facts that are fully derivable: an Agent Note's path encodes lifecycle and class, its filename encodes the first-proposed date, and its H1 carries the title. A hand-maintained copy of those facts would also be a high-contention docs hotspot because concurrent Agent Note branches append rows to the same few lines. [The classification Agent Note](../../implemented/process/2026-06-20-agent-note-classification.md) makes the tree itself authoritative.
+
+## Proposal
+
+Keep the curated prose and generate the list as a fully generated `.agents/notes/INDEX.md`. A shared `scripts/agent-note-index.ts` module would own both the tree walker and the renderer. Two thin consumers would share it:
+
+- `scripts/gen-agent-note-index.ts` (`pnpm run gen-agent-note-index`) would rewrite INDEX.md in full from the tree.
+- `scripts/verify-agent-note-classification.ts` would check structure and assert that the committed INDEX.md byte-matches a fresh render.
+
+Adding, moving, or deleting an Agent Note would mean editing the Agent Note file and running the generator.
+
+## Alternatives considered
+
+### Why not marker-delimited regions inside README.md?
+
+Marker-delimited tables inside README.md would mix generated and curated text, requiring splice mechanics and protection for the surrounding contract. A dedicated generated file would at least keep those concerns separate.
+
+### Why not the verifier-only model?
+
+It catches mistakes but still makes every proposal edit a shared hotspot in a hand-maintained table. The author has already named and placed the file, so the index copy adds no information. This is the same hand-list-versus-derivation judgment the [package-inventory proposal](../../proposed/process/2026-06-20-discover-package-inventory.md) applies to tsconfig references and knip stanzas.
+
+## Consequences
+
+- The generated file would be explicit and contain no curated region.
+- A malformed or missing H1 would be a hard error because the H1 supplies each row title.
+- Concurrent branches would still modify the same committed artifact, even if conflicts could be resolved by rerunning the generator.
+
+## Related
+
+The implemented [no-index decision](../../implemented/process/2026-07-19-remove-generated-agent-note-index.md) keeps the tree and repository search as the discovery mechanisms.
diff --git a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md
similarity index 84%
rename from docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md
rename to .agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md
index 088fa8d25d..62dd7609e8 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md
@@ -1,10 +1,10 @@
-# RFC: Persist assembled assistant messages, not stream chunks
+# Agent Note: Persist assembled assistant messages, not stream chunks
Status: rejected — high-fidelity chunk replay, partial failed streams, and snapshot replay currently depend on persisted `assistant/chunk` events. Dropping chunks is only viable with a no-information-loss replay/artifact replacement.
## Problem
-The canonical session log currently persists every `assistant/chunk` exactly as streamed by the model. The [session persistence RFC](../../implemented/architecture/2026-06-14-session-persistence.md) chose this for token-level replay fidelity and contiguous `seq`, but the cost has grown: JSONL fixtures are dominated by tiny delta records, snapshot scenarios replay the model by grouping chunk events, ACP load reconstructs prior assistant output from chunks, and any future log reader must distinguish durable message history from token-level trace.
+The canonical session log currently persists every `assistant/chunk` exactly as streamed by the model. The [session persistence Agent Note](../../implemented/architecture/2026-06-14-session-persistence.md) chose this for token-level replay fidelity and contiguous `seq`, but the cost has grown: JSONL fixtures are dominated by tiny delta records, snapshot scenarios replay the model by grouping chunk events, ACP load reconstructs prior assistant output from chunks, and any future log reader must distinguish durable message history from token-level trace.
For successful steps that assemble completed content, the loop already appends an `assistant/message`. That is the event `deriveMessages()` uses for the next model request. In other words, the normal resumable conversation state is already present without the chunks; chunks are a live rendering and deterministic-test artifact, not required conversation history. Failed or aborted streams are different: partial assistant output may exist only as chunks, and empty max-token steps may produce no `assistant/message` at all.
@@ -31,4 +31,4 @@ The canonical user session no longer reconstructs the exact token stream of an o
This supersedes the chunk-persistence choice in [session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) and affects [ACP snapshot tests](../../implemented/testing/2026-06-19-acp-snapshot-tests.md), whose current replay plugin derives its script from `assistant/chunk` events.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.md b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md
similarity index 93%
rename from docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.md
rename to .agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md
index 18cd6e981d..b8b9b29987 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md
@@ -1,4 +1,4 @@
-# RFC: Drop ACP session/load until resume has a product shape
+# Agent Note: Drop ACP session/load until resume has a product shape
Status: rejected — Zed is the current target ACP client, advertises and exercises load-capable sessions, and keeps pending-load state for concurrent `session/load`. The bridge should keep `session/load` and make the resume contract solid.
@@ -24,4 +24,4 @@ For now, ACP starts fresh sessions only. `initialize` advertises `loadSession: f
An editor cannot reopen a prior persisted session through ACP. That is a real product feature, but the current implementation is ahead of the UX and ties the bridge to token-level log replay. Keeping persistence while dropping editor load narrows the bridge to the workflow it can currently present cleanly.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md
similarity index 68%
rename from docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md
rename to .agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md
index 2f1408dc4c..8ce4803f73 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md
@@ -1,10 +1,10 @@
-# RFC: Drop ACP terminal `_meta` rendering
+# Agent Note: Drop ACP terminal `_meta` rendering
Status: rejected — Zed is the current target client, and the terminal `_meta` convention is intentional Zed UX with a plain ACP fallback for other clients.
## Problem
-The ACP bridge implements a Zed-specific terminal-card convention through `_meta.terminal_info`, `_meta.terminal_output`, and `_meta.terminal_exit`. The implemented [rich ACP bash rendering RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) deliberately avoided ACP's client-side `terminal/create` because bash execution belongs in the harness, but still adopted the reference agents' display-only `_meta` convention. That gives a nicer Zed card at the cost of bridge state, capability negotiation, terminal ids, special update mapping, text fallback tests, and exit-pill parsing in `dsh-tool-bash`.
+The ACP bridge implements a Zed-specific terminal-card convention through `_meta.terminal_info`, `_meta.terminal_output`, and `_meta.terminal_exit`. The implemented [rich ACP bash rendering Agent Note](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) deliberately avoided ACP's client-side `terminal/create` because bash execution belongs in the harness, but still adopted the reference agents' display-only `_meta` convention. That gives a nicer Zed card at the cost of bridge state, capability negotiation, terminal ids, special update mapping, text fallback tests, and exit-pill parsing in `dsh-tool-bash`.
The fallback path already exists: render the tool call and completed output as normal ACP content blocks. Non-Zed clients rely on that path anyway, but the Zed terminal card is a current target-client feature rather than speculative decoration.
@@ -20,10 +20,10 @@ This proposal is narrower than [collapsing tool-owned UI presentation](2026-06-2
- `TerminalRendering`, terminal ids, terminal cwd resolution, and `_meta.terminal_*` update mapping disappear from `@deepseek-ai/dsh-acp`.
- `ToolTerminal` disappears from `@deepseek-ai/dsh-tools`, or is unused and deleted with the presentation cleanup.
- Bash result presentation no longer parses exit status for terminal pills.
-- The implemented [rich ACP bash rendering RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) stays in `implemented/` as shipped history and is cross-linked from this proposal if superseded.
+- The implemented [rich ACP bash rendering Agent Note](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) stays in `implemented/` as shipped history and is cross-linked from this proposal if superseded.
## What we give up
Zed users lose the dedicated terminal card: no cwd header, terminal display, or exit pill. They still see the command and output as plain content. That is a reasonable simplification while the ACP bridge is still unreleased and the `_meta` keys are a convention rather than a standard.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md b/.agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md
similarity index 88%
rename from docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md
rename to .agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md
index 859bd6f23d..939255f91e 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md
@@ -1,4 +1,4 @@
-# RFC: Drop bash full-output spill files
+# Agent Note: Drop bash full-output spill files
Status: rejected — full-output recovery is a real bash behavior. A future artifact/blob service may generalize it, but dropping spill files before that replacement would lose useful command output.
@@ -20,10 +20,10 @@ This proposal can land independently of [a generic long-running tool runtime](..
- `OutputCollector` keeps bounded buffers only and deletes the temp-file machinery.
- `renderResult()` reports truncation without a filesystem path.
- Tests cover tail truncation and no longer assert full-output file contents.
-- Security guidance in [docs/defensive-patterns.md](../../../defensive-patterns.md) stops treating private spill files as a model-visible interface.
+- Security guidance in [docs/defensive-patterns.md](../../../../docs/defensive-patterns.md) stops treating private spill files as a model-visible interface.
## What we give up
A model or user cannot recover the omitted prefix of a huge command output from a temp file. That is acceptable until there is a real artifact service. The current spill path is too much bespoke machinery for a feature whose lifecycle and permissions are not designed.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md b/.agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md
similarity index 88%
rename from docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md
rename to .agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md
index 94313fd0ad..b2bf42a348 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md
@@ -1,10 +1,10 @@
-# RFC: Drop durable step boundary events
+# Agent Note: Drop durable step boundary events
Status: rejected — `step/end` is the durable indication that a model step finished, and keeping the symmetric `step/start` / `step/end` pair makes crash repair, invariants, and transcript inspection clearer than inferring completion from adjacent step-scoped events.
## Problem
-The session log stores `step/start` and `step/end` events even though every step-scoped event already carries `{ turn, step }`: assistant chunks, assistant messages, tool calls, tool results, usage, and errors. `deriveMessages()` ignores step boundaries, ACP ignores them for UI, and the main consumers are invariants, tests, snapshot goldens, and crash repair.
+The session log stores `step/start` and `step/end` events even though every step-scoped event already carries `{ turn, step }`: assistant chunks, assistant messages, tool calls, tool results, usage, and errors. `deriveMessages()` ignores step boundaries, ACP ignores them for UI, and the main consumers are invariants, tests, snapshot expected outputs, and crash repair.
The rejected argument was that boundary events make the log more ceremonial than informative. In practice, `step/end` is concrete information: a reader can tell whether a model request finished, crashed, or is being repaired without deriving that state from the next event. A bare `step/start` is likewise useful for a model request that began but produced no chunks before failing.
@@ -20,11 +20,11 @@ The invariants plugin should enforce that step-scoped events have valid positive
- The loop has no `closeStep()` finalization path.
- ACP snapshots and persistence contract fixtures stop expecting step-boundary lines.
- `deriveMessages()` and replay derive the same message history from step-scoped events.
-- The [event taxonomy docs](../../../architecture.md) describe turns as the durable boundary and steps as a field on step-scoped records.
+- The [event taxonomy docs](../../../../docs/architecture.md) describe turns as the durable boundary and steps as a field on step-scoped records.
- The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy.
## What we give up
The log no longer records "a model request started but produced no event before the process died" as a durable fact, and no longer has an explicit "this step completed" marker. That loss is not acceptable while the session log is the durable replay and audit surface.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.md b/.agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md
similarity index 94%
rename from docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.md
rename to .agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md
index fc06cc76c9..c85e943476 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md
@@ -1,4 +1,4 @@
-# RFC: Drop unused session lineage metadata
+# Agent Note: Drop unused session lineage metadata
Status: rejected — `parentSession` is part of the documented fork/sub-agent seam and is already preserved by the agent/session resume path. The field is future-facing, but it is not accidental dead state.
@@ -26,4 +26,4 @@ If lineage returns, decide then whether it belongs in the immutable header, a se
The codebase loses a ready-made lineage hook for future fork/sub-agent UX. That is intentional. The field is easy to reintroduce when the feature exists, and the unreleased stance lets the format change without migrations.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.md b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md
similarity index 94%
rename from docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.md
rename to .agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md
index a59c992b0d..8e5d59172f 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md
@@ -1,4 +1,4 @@
-# RFC: Fold the persistence interface into dsh-session
+# Agent Note: Fold the persistence interface into dsh-session
Status: rejected — the separate persistence interface package is the intended modular capability seam for durable backends. Folding it into `dsh-session` would reduce package count at the cost of a cleaner backend boundary.
@@ -26,4 +26,4 @@ The implementing PR should update the [capability seams](../../implemented/archi
`dsh-session` becomes heavier: it owns both the in-memory log and the persistence interface. That is the trade. If third-party persistence backends were already a public ecosystem, the separate interface package would be a cleaner SDK boundary; pre-release, the extra package looks like abstraction before there is an external consumer.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.md b/.agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md
similarity index 91%
rename from docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.md
rename to .agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md
index 6125feaa8b..dbc6ffed44 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md
@@ -1,4 +1,4 @@
-# RFC: Collapse tool-owned UI presentation
+# Agent Note: Collapse tool-owned UI presentation
Status: rejected — tool-owned presentation should wait for more real tools before being generalized or deleted. Bash and ACP currently need the existing richer presentation path.
@@ -22,7 +22,7 @@ As a smaller alternative, replace the current optional-field bag with one explic
- `ToolCallPresentation`, `ToolResultPresentation`, `ToolTerminal`, and `ToolCallKind` disappear unless a minimal generic UI type still needs one.
- ACP no longer keeps presenter pending state or calls tool callbacks during live streaming/load replay.
- `dsh-tool-bash` no longer parses rendered text to recover exit status for a UI pill.
-- Snapshot goldens show generic tool cards and text results.
+- Snapshot expected outputs show generic tool cards and text results.
## What we give up
@@ -30,4 +30,4 @@ Bash loses its custom terminal-looking card and model-written description placem
## Related
-This is the broad version of [dropping ACP terminal metadata](2026-06-20-drop-acp-terminal-meta.md). If this RFC is accepted, that narrower RFC becomes unnecessary.
+This is the broad version of [dropping ACP terminal metadata](2026-06-20-drop-acp-terminal-meta.md). If this Agent Note is accepted, that narrower Agent Note becomes unnecessary.
diff --git a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.md b/.agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md
similarity index 96%
rename from docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.md
rename to .agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md
index fd1a4687b3..b26243f197 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md
@@ -1,4 +1,4 @@
-# RFC: Retire mid-turn steering
+# Agent Note: Retire mid-turn steering
Status: rejected — mid-turn steering is an intentional agent capability for between-step user/plugin input and future goal/loop workflows. It is complexity with a product direction, not an accidental duplicate of `send()`.
@@ -32,4 +32,4 @@ A user cannot add same-turn steering content while a model is between tool steps
This pairs naturally with [dropping durable step boundaries](2026-06-20-drop-durable-step-boundaries.md), because removing same-turn steering and `agent/turn-continuation` leaves tool calls as the only reason a turn contains multiple model steps.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.md b/.agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md
similarity index 88%
rename from docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.md
rename to .agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md
index 8f1d4e4531..83c6f598d4 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md
@@ -1,10 +1,10 @@
-# RFC: Return the ACP bridge to one live session per connection
+# Agent Note: Return the ACP bridge to one live session per connection
Status: rejected — Zed is the current target ACP client and its ACP implementation is explicitly multi-session: it stores live sessions in a `HashMap`, tracks `pending_sessions`, joins concurrent loads for the same id, and tests close-during-load behavior.
## Problem
-The ACP bridge now supports multiple live sessions on one JSON-RPC connection. That capability brings multi-entry session maps, reverse session/agent lookups, per-session prompt state, loading ids, demux for every event, cross-session teardown, and isolation concerns for future permission prompts and background tasks. The older [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) still tracks the unfinished permission-ownership piece; this RFC is the competing simplification path.
+The ACP bridge now supports multiple live sessions on one JSON-RPC connection. That capability brings multi-entry session maps, reverse session/agent lookups, per-session prompt state, loading ids, demux for every event, cross-session teardown, and isolation concerns for future permission prompts and background tasks. The older [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) still tracks the unfinished permission-ownership piece; this Agent Note is the competing simplification path.
The product target has proven it needs concurrent editor conversations over one harness process: Zed's ACP connection owns multiple sessions and load states. The snapshot replay tier still avoids concurrent model streams because its replay entries are positional; that is a test-fixture limitation, not a reason to remove bridge multiplexing.
@@ -20,10 +20,10 @@ Remove the multi-session maps and demux where a single `SessionRecord | undefine
- `session/new` and `session/load` reject while that record exists.
- Event handlers no longer demux across a `Map`.
- Multi-session tests are removed or moved under the proposal that continues to defend multiplexing.
-- The existing [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) is updated to link this RFC and remains the live direction.
+- The existing [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) is updated to link this Agent Note and remains the live direction.
## What we give up
An ACP client cannot host several concurrent conversations on one server process. That is a meaningful capability cut. The simpler model is still reasonable for an unreleased harness: one editor conversation maps to one agent process, and cross-session permission/background-task isolation stops being a live correctness burden.
-
+
diff --git a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.md b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md
similarity index 96%
rename from docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.md
rename to .agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md
index 1ed26c66f8..a685765197 100644
--- a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.md
+++ b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md
@@ -1,4 +1,4 @@
-# RFC: Truncate interrupted final turns on load
+# Agent Note: Truncate interrupted final turns on load
Status: rejected — a single turn can contain substantial real work, including many steps and large tool output. Preserving interrupted turns is preferable to silently dropping that tail on load.
@@ -31,4 +31,4 @@ A crash can lose real work from the final turn: assistant text, tool calls, and
This is a direct simplification of [session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) and [turn enclosure](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md). It also removes much of the motivation for durable step boundary events, making [drop durable step boundary events](2026-06-20-drop-durable-step-boundaries.md) smaller.
-
+
diff --git a/.agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md b/.agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md
new file mode 100644
index 0000000000..582673f185
--- /dev/null
+++ b/.agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md
@@ -0,0 +1,37 @@
+# Agent Note: Prune the unimplemented subagent seam vocabulary
+
+Status: rejected — the deferred capability vocabulary (`outputSchema`/`structured`, `toolFilter`, `sendMessage`/`resume`) is intentionally reserved surface: the seam advertises the full intended contract ahead of its implementations by design, so providers and consumers grow into a stable shape rather than re-negotiating it per capability. The consumer-evidence analysis below records the decision-time state.
+
+## Problem
+
+The [subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) shipped a two-tier capability design: start-time capability flags checked by the service, and optional runtime methods on `SubagentRun`. Three start-time features and both optional runtime methods have zero implementations and zero callers:
+
+- **`outputSchema`/`structured` and `toolFilter`** (`SubagentCapabilities`, `SubagentStartRequest`, `SubagentResult` in `packages/subagent/subagent/src/types.ts`): at the decision point, every real provider declared `outputSchema: false, toolFilter: false` (`packages/subagent/subagent-spawn/src/index.ts`, `packages/subagent/subagent-fork/src/index.ts`, `packages/subagent/subagent-acp/src/index.ts`); the sole production `ctx.subagents.start` caller (`packages/subagent/tool-subagent/src/index.ts`) built `{ prompt, parent, signal?, agentOptions? }` and structurally could not set either; `structured` appeared only in the scripted test fixture. The service's capability check carried two assert rows whose only exercisers were the rejection tests.
+- **`SubagentRun.sendMessage` / `SubagentRun.resume`** (same file): implemented by NO provider — not even the mock; the spawn spec asserts their *absence*.
+
+The only reason `dsh-subagent` depends on `dsh-tools` at all is `outputSchema`'s `SchemaSpec` type. Three subsequent subagent workstreams (per-session snapshot replay, the fork seed boundary, the ACP backend) landed around this surface without growing a single consumer.
+
+## Proposal
+
+Remove `outputSchema`/`structured`, `toolFilter`, `sendMessage`, and `resume` from the seam; shrink `SubagentCapabilities` to `{ depthLimit }`; drop the two capability-assert rows, the all-false flags on the three providers, the scripted fixture's structured branch and capability knobs, and the tests that exist to pin the removed surface. Drop the `dsh-tools` peer/dev dependency from `packages/subagent/subagent/package.json`. Update the [subagent.md](../../../../docs/core-data-structures/subagent.md) pastes and the type-equiv manifest, plus the affected provider READMEs. The implementing PR amends the seam Agent Note's capability catalog per [implemented/AGENTS.md](../../implemented/AGENTS.md).
+
+**Keep** `depthLimit`/`maxDepth` and capability checks. The in-process backend enforces the limit, although the shipping tool does not yet set it. Recursion is a known seam risk, so the appropriate follow-up is to supply a tool default rather than delete working enforcement.
+
+Adjacent surface examined and deliberately left alone: `SubagentService.getProvider()`/`list()` have test-harness consumers only, but the [prune-dead-seam-methods implementation note](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md) records precisely this shape being removed from the bash executor and reverted — a test harness IS a consumer for a one-line accessor over an already-tracked map. `SubagentRunEndInfo.lastAssistantMessage` is a recorded keep (the [subagent-observe-enrich Agent Note](../../implemented/feature/2026-06-30-subagent-observe-enrich.md)'s review dropped `agentType` and kept it deliberately, as the only final-message channel for out-of-process children); its currently-unwired bridge forwarding is a gap to close or a consumer to document, not surface for this Agent Note to cut.
+
+This is the seam-vocabulary echo of [prune dead methods from the persistence seam](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md): members every implementation must declare for nobody — weaker even, since here zero implementations exist.
+
+## Alternatives considered
+
+### Why not keep it?
+
+The two-kinds-of-capability design is the seam Agent Note's headline, and re-adding `outputSchema` later touches several files. But the design survives with `depthLimit` as its live example and the Agent Notes as its record, and the seam Agent Note itself concedes the shipped `toolFilter` shape is wrong (real enforcement needs a `tools/pre-execute` deny in the child's context, not schema filtering) — that deny primitive exists on the interception seams, so re-adding against a real implementing provider will pin a better contract than the current speculative one.
+
+## Acceptance criteria
+
+- The removed spellings appear only in this Agent Note and the amended seam Agent Notes; `SubagentCapabilities` is `{ depthLimit: boolean }`; the `dsh-tools` dependency edge is gone (`hygiene` green).
+- Depth-enforcement tests are unchanged and green.
+
+## Risks
+
+The subagent lifecycle events carry `lastAssistantMessage` on the end payload — that enrichment lives in the service module, not the seam vocabulary this Agent Note shrinks, and the observe-enrich Agent Note records dropping an `agentType` sibling for lacking a consumer: the judgment this Agent Note extends. The CC hooks bridge, the first outside consumer of those lifecycle events, reads only the event payloads and touches none of the surface removed here; the observe-enrich Agent Note's deferred control-flow redesign names implementing `resume` as its own future work — exactly the re-add trigger this Agent Note's pattern anticipates.
diff --git a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md b/.agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md
similarity index 91%
rename from docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md
rename to .agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md
index 7624629d41..dcfdfa13e6 100644
--- a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md
+++ b/.agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md
@@ -1,4 +1,4 @@
-# RFC: Collapse workflows to the exercised foreground core
+# Agent Note: Collapse workflows to the exercised foreground core
Status: rejected — Workflow progress is an intentional observation surface; make it useful through a consumer instead of deleting it.
@@ -18,7 +18,7 @@ Cancellation also has two public channels for one synchronous start. `WorkflowSt
Keep the exercised core: `agent(prompt, { schema, model })`, `parallel`, `pipeline`, `args`, concurrency/agent caps, cancellation, bounded disposal, structured results, worker isolation, and foreground tool collection. Remove all `workflow/*` events and their event-only info/outcome types; remove `phase()`, `log()`, agent `label`/`phase`, phase declarations, `whenToUse`, and their worker messages/host observers; collapse workflow metadata to the name the tool actually uses; remove event-only run ids/meta snapshots and the synthesized agent-end ledger. Shrink `WorkflowRun` to `result`, `cancel()`, and `dispose()`; the tool renders the request-owned name. Remove `WorkflowStartRequest.signal` and the worker host's input-signal listener/disarm state, retaining the caller-owned bridge from its abort signal to `run.cancel()`. Make `WorkflowError` one fatal error class without a boolean mode or `isFatalWorkflowError()` helper.
-Amend the implemented dynamic-workflow RFC and update the seam/tool/worker READMEs, tool schema, generated catalogs and package graph, worker type-equivalence records, unit tests, and workflow snapshot/header fixtures. Progress UI work, if commissioned, starts from a correlation contract that names the parent agent/session/tool call instead of reviving this protocol unchanged.
+Amend the implemented dynamic-workflow Agent Note and update the seam/tool/worker READMEs, tool schema, generated catalogs and package graph, worker type-equivalence records, unit tests, and workflow snapshot/header fixtures. Progress UI work, if commissioned, starts from a correlation contract that names the parent agent/session/tool call instead of reviving this protocol unchanged.
## Alternatives considered
diff --git a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md b/.agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md
similarity index 76%
rename from docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md
rename to .agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md
index cafd50e8a5..e96215a651 100644
--- a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md
+++ b/.agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md
@@ -1,4 +1,4 @@
-# RFC: Prune unused skill registry surface
+# Agent Note: Prune unused skill registry surface
Status: rejected — Direct runtime skill registration is an intentional extension path for third-party plugins.
@@ -10,11 +10,11 @@ The skill service's embedded-runtime subsystem has zero production caller of `ct
Remove `SkillService.register()`, `SkillRegistration`, the runtime pseudo-provider and reserved-name rules, runtime revisions/cache branches, and runtime-only source/rank normalization. Tests that need an embedded skill register a small real provider. Retain `providerRevision` as the in-flight discovery epoch, but key completed catalogs by cwd alone: every provider mutation synchronously clears the cache, and the post-await revision comparison already prevents inserting stale work. Remove `whenToUse`, `SkillCandidate.path`, and `SkillDefinition.path` from the skill contract and local-provider copies while retaining provider locator/root paths; retain `metadata`, `disableModelInvocation`, `source`, `provider`, `locator`, and `resourceBase` as either deliberate extension vocabulary or production-consumed fields.
-Amend the skill-system RFC, README, JSDoc, catalogs, and tests. Agent-scoped system-prompt sections, tool providers, and variables are explicitly outside this proposal: the [agent-scope contributor contract](../../implemented/architecture/2026-07-08-agent-scope-contexts.md) intentionally allows all three to be registered during `setup(agentCtx)` through the agent-owned context, so absence of a fixed in-repo scoped registration is not evidence of non-consumption.
+Amend the skill-system Agent Note, README, JSDoc, catalogs, and tests. Agent-scoped system-prompt sections, tool providers, and variables are explicitly outside this proposal: the [agent-scope contributor contract](../../implemented/architecture/2026-07-08-agent-scope-contexts.md) intentionally allows all three to be registered during `setup(agentCtx)` through the agent-owned context, so absence of a fixed in-repo scoped registration is not evidence of non-consumption.
## Alternatives considered
-**Keep runtime skill registration for embedders.** It is a deliberate synchronous direct-definition convenience in the implemented skill RFC. A small provider wrapper can expose the same embedded data under effect-owned lifetime, but it must implement async `list()`/`get()`, carry provider identity, and accept provider duplicate semantics. The proposal chooses that one regular path over preserving a second ranking, validation, cache-invalidation, and lookup path.
+**Keep runtime skill registration for embedders.** It is a deliberate synchronous direct-definition convenience in the implemented skill Agent Note. A small provider wrapper can expose the same embedded data under effect-owned lifetime, but it must implement async `list()`/`get()`, carry provider identity, and accept provider duplicate semantics. The proposal chooses that one regular path over preserving a second ranking, validation, cache-invalidation, and lookup path.
## Acceptance criteria
diff --git a/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml
new file mode 100644
index 0000000000..98acc791c3
--- /dev/null
+++ b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml
@@ -0,0 +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-19-fold-compaction-package-split.md: 47c9feb6bb0dd06fec0f002b7c1e930b288abe5e
+2026-07-19-fold-compaction-package-split.zh.md: 53717ff10d1210bd2072f322d1936ac6c389afcd
diff --git a/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.md b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.md
new file mode 100644
index 0000000000..47c9feb6bb
--- /dev/null
+++ b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.md
@@ -0,0 +1,37 @@
+# Agent Note: Fold the single compaction backend into its service package
+
+Status: rejected — More compaction backends are planned, so the interface and basic implementation packages remain separate.
+
+English | [中文](2026-07-19-fold-compaction-package-split.zh.md)
+
+## Problem
+
+Compaction is split between `@deepseek-ai/dsh-compact`, which owns an abstract two-method service and shared types, and `@deepseek-ai/dsh-compact-basic`, which owns the only complete implementation. Shipped configurations load only the basic package, and no production package independently consumes the interface package except that implementation.
+
+The split adds a package manifest, README, project boundary, dependency edge, abstract forwarding class, generated catalog entries, and composition wiring without demonstrating backend substitution. The [capability-seam decision](../../implemented/architecture/2026-06-13-capability-seams.md) requires a real interface, implementation, and consumer rather than a preemptive split; the [compaction decision](../../implemented/feature/2026-06-18-compaction-capability-seam.md) records that its independent consumer was deferred.
+
+## Proposal
+
+Move the basic implementation into `@deepseek-ai/dsh-compact` and remove `@deepseek-ai/dsh-compact-basic`. Keep `ctx.compact`, `CompactionResult`, the shared transcript and tool-pairing helpers, the existing configuration, and the concrete compaction algorithm in one package.
+
+Preserve `summarize()` as a protected customization hook. A deployment-specific summarizer can subclass or intercept the existing LLM call without requiring a second capability package. Reintroduce an interface package only when a second complete backend and an independent consumer need substitution.
+
+Amend the implemented compaction decision and the [recallable-compaction proposal](../../proposed/feature/2026-07-06-recallable-compaction.md) if this proposal is accepted so package ownership has one durable description.
+
+## Alternatives considered
+
+**Keep the split because a remote or recall backend may arrive.** A possible future implementation does not justify the current package boundary. Recall adds a consumer of compaction results, not necessarily another implementation, and a remote summarizer can use the protected hook.
+
+**Move the implementation package name onto the interface package.** Keeping `compact-basic` as the surviving name would make the product service appear to be one optional backend. `compact` is the stable service identity already used by `ctx.compact` and is the clearer single-package owner.
+
+## Acceptance criteria
+
+- `@deepseek-ai/dsh-compact-basic` and its workspace/package metadata are removed.
+- `@deepseek-ai/dsh-compact` owns the current configuration, plugin class, algorithm, types, events, and shared helpers.
+- Existing deployments can load the surviving package with equivalent configuration and model-visible behavior.
+- Automatic and manual compaction preserve cancellation, locking, token accounting, tool pairing, durable events, provenance, retry convergence, and transcript rendering.
+- Loader composition, unit, runaway-turn, cancellation, snapshot, and real-model compaction tests pass; generated catalogs and module graphs are current.
+
+## Risks
+
+This is an intentional pre-release package-name contraction. Embedders loading `@deepseek-ai/dsh-compact-basic` must switch packages, and future backend substitution would require extracting a boundary again. The cost is acceptable only while one complete implementation exists; acceptance should be revisited if a second backend lands first.
diff --git a/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md
new file mode 100644
index 0000000000..53717ff10d
--- /dev/null
+++ b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 将唯一的压缩后端并入服务包
+
+Status: rejected — 计划增加更多压缩后端,因此接口包与 basic 实现包继续分离。
+
+[English](2026-07-19-fold-compaction-package-split.md) | 中文
+
+## 问题
+
+压缩(compaction)目前拆分在两个包中:`@deepseek-ai/dsh-compact` 拥有一个含两个方法的抽象服务和共享类型,`@deepseek-ai/dsh-compact-basic` 拥有唯一的完整实现。交付配置只加载 basic 包,除了该实现外,没有生产包独立消费接口包。
+
+该拆分增加了一份包(package)manifest(元数据清单)、README、项目边界、依赖边、抽象转发类、生成目录项和组合接线,却没有体现后端替换需求。[能力服务边界决策](../../implemented/architecture/2026-06-13-capability-seams.md)要求接口、实现和消费方都必须真实存在,而不能预先拆分;[压缩决策](../../implemented/feature/2026-06-18-compaction-capability-seam.md)也记录了独立消费方仍被推迟。
+
+## 提案
+
+把 basic 实现移入 `@deepseek-ai/dsh-compact`,并删除 `@deepseek-ai/dsh-compact-basic`。`ctx.compact`、`CompactionResult`、共享 transcript(文本记录)和工具配对辅助方法、现有配置以及具体压缩算法都由一个包负责。
+
+保留 `summarize()` 作为受保护的自定义钩子。部署专用的摘要器可以通过继承或拦截现有 LLM(大语言模型)调用完成定制,无需第二个能力包。只有在第二个完整后端与独立消费方确实需要替换实现时,才重新提取接口包。
+
+如果本提案获准,应同步修订已实现的压缩决策与[可回忆压缩提案](../../proposed/feature/2026-07-06-recallable-compaction.md),使包所有权只有一处持久说明。
+
+## 备选方案
+
+**为可能出现的远程或回忆后端保留拆分。** 一种可能的未来实现不足以支撑当前包边界。回忆功能会增加压缩结果的消费方,但不一定增加另一种实现;远程摘要器也可以使用受保护钩子。
+
+**让接口包并入实现包名。** 如果保留 `compact-basic` 作为最终名称,产品服务会看起来像一个可选后端。`compact` 已经是 `ctx.compact` 使用的稳定服务标识,更适合作为单包所有者。
+
+## 验收标准
+
+- 删除 `@deepseek-ai/dsh-compact-basic` 及其工作区和包元数据。
+- `@deepseek-ai/dsh-compact` 拥有当前配置、插件类、算法、类型、事件和共享辅助方法。
+- 现有部署可以使用等效配置加载保留的包,模型可见行为不变。
+- 自动压缩和手动压缩保留取消、锁、token 用量、工具配对、持久事件、来源、重试收敛和 transcript 渲染行为。
+- Loader 组合、单元、失控轮次、取消、快照和真实模型压缩测试全部通过;生成目录与模块图保持最新。
+
+## 风险
+
+这是一项有意实施的预发布包名收缩。加载 `@deepseek-ai/dsh-compact-basic` 的嵌入方必须切换包,未来的后端替换也需要重新提取边界。只有在仍然只有一个完整实现时,这项代价才可接受;如果第二个后端先行落地,应重新评估是否接纳本提案。
diff --git a/.agents/skills/dsh-code-review/SKILL.md b/.agents/skills/dsh-code-review/SKILL.md
index 2b562cc321..814132d481 100644
--- a/.agents/skills/dsh-code-review/SKILL.md
+++ b/.agents/skills/dsh-code-review/SKILL.md
@@ -9,12 +9,12 @@ description: Use when reviewing a pull request in the deepseek-harness repo —
## Sources of truth
-- [AGENTS.md](../../../AGENTS.md) and [packages/AGENTS.md](../../../packages/AGENTS.md): repository and package rules.
+- [AGENTS.md](../../../AGENTS.md) and [packages/AGENTS.md](../../../packages/AGENTS.md): standing repository and package authoring contracts.
- [docs/defensive-patterns.md](../../../docs/defensive-patterns.md): subprocess, callback, async-state, and disposal bug classes.
- [docs/AGENTS.md](../../../docs/AGENTS.md): documentation placement and prose discipline.
- [dsh-prose-standard](../dsh-prose-standard/SKILL.md): required coverage and editorial judgment for comments, docs, prompts, and visible strings.
-- [docs/testing.md](../../../docs/testing.md) and the [quality-gates RFC](../../../docs/rfc/implemented/process/2026-06-11-quality-gates.md): required test tiers and gates.
-- [RFC index](../../../docs/rfc/README.md): design rationale. Treat disagreement with an RFC as a design discussion, not an automatic veto.
+- [docs/testing.md](../../../docs/testing.md) and the [quality-gates Agent Note](../../notes/implemented/process/2026-06-11-quality-gates.md): required test tiers and gates.
+- [Agent Notes](../../notes/README.md): design rationale. Treat disagreement with an Agent Note as a design discussion, not an automatic veto.
- For bilingual changes, read [translation-rules.md](../../../docs/i18n/translation-rules.md), [terminology.md](../../../docs/i18n/terminology.md), and [dsh-translate-docs](../dsh-translate-docs/SKILL.md).
## Blocking requirements
@@ -22,26 +22,27 @@ description: Use when reviewing a pull request in the deepseek-harness repo —
1. **New prose receives semantic review.** Use [dsh-prose-standard](../dsh-prose-standard/SKILL.md) to critically review every added or changed Markdown passage, JSDoc, comment, prompt, description, diagnostic, and visible string. Verify required coverage, accuracy, placement, and editorial quality against the owning code or behavior; automated checks do not establish those properties.
2. **Docs match the code.** Config, defaults, errors, wire fields, events, and public behavior update the package README and JSDoc in the same diff. Comments state non-obvious contracts; flag implementation narration, test walkthroughs, review history, and duplicated rationale for deletion or a link to their one home.
3. **Core type docs match.** Changes to spine or seam vocabulary update the appropriate [core-data-structures](../../../docs/core-data-structures/core.md) page and any `type-equiv` entry. Internal types need no catalog entry.
-4. **Registrations clean up.** A new registry contribution has a test that disposes its owner and observes removal.
+4. **Registrations clean up.** Verify each new registry contribution satisfies the disposal-test contract in [packages/AGENTS.md](../../../packages/AGENTS.md).
5. **Required gates pass.** Trust the [current readiness sequence](../../../AGENTS.md#run-the-ci-gates-locally-before-marking-a-pr-ready) and `pnpm run check:pre-push` for their enforced inventory; review the semantic gaps they cannot detect.
## Manual checks
-- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any RFC, including errors, cancellation, ownership, and disposal.
+- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any Agent Note, including errors, cancellation, ownership, and disposal.
- **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns.md](../../../docs/defensive-patterns.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, ownership before reentry, complete detach cleanup, and quiescent disposal.
-- **Capability shape:** a swappable capability follows the interface / implementation / consumer split. Consumers depend on the interface, not a backend.
-- **Scope, ownership, and necessity:** tie each abstraction, state machine, option, defensive copy, and compatibility path to a current contract or production consumer. Challenge unrelated features, speculative generality, and behavior placed outside its owning plugin or service.
-- **Configuration:** deployment-varying timeouts, caps, models, URLs, paths, and retry counts are validated `Config` fields, not literals or `DEFAULT_*` constants.
-- **Enforcement boundaries:** hidden schema fields, filtered prompts, facades, wrappers, and listener ordering are not authoritative enforcement when direct or alternate callers can bypass them. Exercise denial paths at the boundary that actually executes the operation.
-- **Borrowed and derived state:** determine whether retained caller-owned values are borrowed or snapshotted by contract; do not demand copies at typed same-process seams. Materialize mutable values that cross queues, model/tool JSON, durable logs or files, workers, processes, or wire boundaries. Commit notifications and derived state only at the documented success boundary, and trace caches, prompts, UI echoes, replay, and query views to one authoritative source.
-- **Bounds cover the final operation:** verify byte, token, item, and time limits at the boundary that owns the complete emitted or retained result, including wrappers and metadata. Probe tiny limits, exact thresholds, oversized single chunks, and multibyte text for byte limits.
+- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package contract](../../../packages/AGENTS.md).
+- **Scope, ownership, and necessity:** map each abstraction, state machine, option, defensive copy, and compatibility path to its current contract, production consumer, and owning plugin or service. Challenge unrelated features and speculative generality, then test the PR's coherence against [the root contract](../../../AGENTS.md#conventions).
+- **Configuration and public choices:** ask what current-consumer evidence or prior art supports each default, public operation set, format, or imported external concept. Require an explicit choice or deferral when that evidence is absent.
+- **Model perspective:** inspect the exact prompts, tool schemas, results, and diagnostics the model receives across affected modes. Flag concepts outside the model's task, then verify stable text verbatim and dynamic behavior through snapshots or end-to-end coverage.
+- **Enforcement boundaries:** follow every denial path to the operation that executes it; exercise direct and alternate callers that can bypass schemas, prompts, facades, wrappers, or listener ordering.
+- **Borrowed and derived state:** classify each retained value under the package boundary contract, then trace notifications and every cache, prompt, UI echo, replay, and query view to the documented success point and authoritative source.
+- **Bounds cover the final operation:** locate the owner of the complete emitted or retained result, including wrappers and metadata. Probe tiny and exact limits, oversized single chunks, and multibyte text for byte limits.
- **Real entry path:** tests exercise the shipped Loader, bin, worker, ACP bridge, or subprocess where relevant. A hand-mounted plugin does not catch Loader export-shape failures; a function plugin must named-export its namespace and have no default export.
- **Test strength:** assertions fail on the intended regression and verify external state, logs, events, or disposal rather than restating the implementation or trusting an agent's report. Coverage is necessary but not evidence that the scenario is correct.
-- **Changed checks have a negative control:** a new automated check, or a changed acceptance path in one, has a deliberately invalid case that reaches the real top-level runner and fails for the intended rule; a green happy path does not prove the check is wired.
-- **Implemented RFCs match shipped reality:** when a PR implements a proposed RFC, move and rewrite it as present-tense shipped state in the same diff, then verify paths, names, and mechanisms against the implementation.
-- **Transcript changes:** editor-visible or model-visible changes update snapshots or explain why no snapshot applies. Review golden diffs as behavior changes, not formatting noise.
+- **Mechanized invariants and negative controls:** trace each new or changed check through the executed top-level gate and its deliberately invalid case; confirm the real runner fails for the intended rule.
+- **Implemented Agent Notes match shipped reality:** when a PR implements a proposed Agent Note, move and rewrite it as present-tense shipped state in the same diff, then verify paths, names, and mechanisms against the implementation.
+- **Transcript changes:** editor-visible or model-visible changes update snapshots or explain why no snapshot applies. Review expected-output diffs as behavior changes, not formatting noise.
- **Bilingual changes:** compare meaning and terminology on both sides; a green pairing hash does not prove translation quality.
## Reporting findings
-State the defect, location, impact, and evidence. Separate blockers from suggestions and omit issues already enforced by a green gate. Use the existing GitHub review thread for replies. When receiving review, verify each claim and fix or rebut it on technical grounds without performative agreement.
+State the defect, location, impact, and evidence. Place a localized defect inline on the tightest relevant diff range; use a PR-level comment for cross-cutting architecture, scope, or review-wide synthesis. Separate blockers from suggestions and omit issues already enforced by a green gate. Use the existing GitHub review thread for replies. When receiving review, verify each claim and fix or rebut it on technical grounds without performative agreement.
diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md
new file mode 100644
index 0000000000..bee1b0dfe8
--- /dev/null
+++ b/.agents/skills/dsh-doc-site-sync/SKILL.md
@@ -0,0 +1,82 @@
+---
+name: dsh-doc-site-sync
+description: Use when publishing, updating, moving, or removing DeepSeek Harness documentation website pages; editing website/docs.ts mappings or navigation; diagnosing a page missing from the VitePress site; fixing projected documentation links; or running the docs:dev, docs:check, and doc-sync workflow after website-content changes.
+---
+
+# Synchronizing the DeepSeek Harness Documentation Site
+
+Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree.
+
+Repository translations follow the sibling pairing contract: English `foo.md`, Chinese `foo.zh.md`, and `foo.i18n.yaml` live together. Never create `zh-CN/` or other locale directories for website content. The site route trees are independent of that source layout: `foo.zh.md` projects to the root route and `foo.md` projects to the matching `/en/` route.
+
+## Read the owning contracts
+
+- Read [docs/AGENTS.md](../../../docs/AGENTS.md) and use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) when deciding where content belongs or changing product documentation prose.
+- Use [dsh-translate-docs](../dsh-translate-docs/SKILL.md) whenever an edited source has a bilingual counterpart.
+- Read the current `DocsPage` type and entries in [website/docs.ts](../../../website/docs.ts) before changing the manifest; do not rely on a remembered field set.
+- Read [website/.vitepress/config.ts](../../../website/.vitepress/config.ts) before adding a new section, sidebar collection, locale, or top-level navigation item.
+
+## Classify the change
+
+- **Edit an already published page:** change only its canonical Markdown source. Do not touch the manifest unless its route or navigation metadata changes.
+- **Publish a new page:** create it in its owning `docs/` tier, then add one manifest entry.
+- **Rename, move, or remove a page:** update the canonical file, manifest entry, and inbound repository links atomically. Remove stale manifest entries; `docs:check` rejects missing sources.
+- **Publish a generated catalog:** map the generated `docs/` file, but change its generator or source metadata rather than editing the catalog by hand.
+- **Change site structure:** update the manifest for ordinary pages; update VitePress configuration only when the existing sidebar, section, or locale model cannot express the change.
+
+Never edit or commit `website/.generated/`, `website/.cache/`, or `website/.dist/`. Never copy a maintained `docs/` page into `website/`.
+
+## Add or update a manifest entry
+
+Set every `DocsPage` field deliberately:
+
+- `source`: repository-relative canonical Markdown path. For a complete bilingual pair, add the English `.md` path through `pairedPages()`; it derives the sibling `.zh.md`, the content locales, and counterpart aliases.
+- `route`: public VitePress path including the `.md` suffix.
+- `label`: sidebar label, not necessarily the document H1.
+- `sidebar`: reuse `zh-guide`, `zh-develop`, or `en-docs` unless the information architecture genuinely needs another collection.
+- `section`: reuse an existing section when possible. If adding one, also place it in `sectionOrder` in the VitePress config.
+- `order`: stable order within the section.
+- `sourceAliases`: optional additional repository paths that should resolve to this page when links are projected. It does not create another public route.
+
+Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary.
+
+## Preserve link behavior
+
+Write normal repository-relative Markdown links in canonical docs. The projector applies these rules:
+
+- A target present in the manifest becomes a site-relative route.
+- An existing target outside the manifest becomes a GitHub source link, including supported line suffixes.
+- External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged.
+- A missing repository-relative target fails projection instead of silently producing a broken link.
+
+Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page.
+
+## Preview and validate
+
+Run local preview while editing:
+
+```sh
+pnpm docs:dev
+```
+
+The dev server watches mapped source files and reprojects them. Restart it after changing the manifest if the new source is not picked up automatically.
+
+Run the focused website gate before treating the mapping as valid:
+
+```sh
+pnpm docs:check
+```
+
+Before committing a documentation-site change, run:
+
+```sh
+pnpm run doc-sync
+pnpm run lint
+git diff --check
+```
+
+Use [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md) before pushing. Report the canonical files changed, manifest entries added or removed, public routes affected, and the exact checks run.
+
+## Keep deployment separate
+
+Synchronizing content into the VitePress build does not publish it to the internet. Do not add GitHub Pages permissions, deployment workflows, custom domains, or public hosting unless the user explicitly requests deployment and confirms the hosting policy.
diff --git a/.agents/skills/dsh-doc-site-sync/agents/openai.yaml b/.agents/skills/dsh-doc-site-sync/agents/openai.yaml
new file mode 100644
index 0000000000..9f4909f258
--- /dev/null
+++ b/.agents/skills/dsh-doc-site-sync/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "DSH Documentation Site Sync"
+ short_description: "Publish repository docs through the DSH website manifest"
+ default_prompt: "Use $dsh-doc-site-sync to publish or update a DeepSeek Harness documentation page on the website."
diff --git a/.agents/skills/dsh-doc-standards/SKILL.md b/.agents/skills/dsh-doc-standards/SKILL.md
index 6ad8902d66..2a5458db7f 100644
--- a/.agents/skills/dsh-doc-standards/SKILL.md
+++ b/.agents/skills/dsh-doc-standards/SKILL.md
@@ -10,7 +10,7 @@ The contract lives in [docs/AGENTS.md](../../../docs/AGENTS.md). This workflow c
## Sources of truth (read, don't re-summarize)
- [docs/AGENTS.md](../../../docs/AGENTS.md) — the taxonomy ("one home per fact"), budgets, slop checklist.
-- [docs/rfc/README.md](../../../docs/rfc/README.md) — when a decision earns an RFC, how to file it, and what goes inside one (the header block, per-lifecycle skeleton, and Alternatives-considered mandate, gated by `verify-rfc-format`); [docs/postmortem/README.md](../../../docs/postmortem/README.md) — when an incident earns a postmortem.
+- [.agents/notes/README.md](../../notes/README.md) — when a decision earns an Agent Note, how to file it, and what goes inside one (the header block, per-lifecycle skeleton, and Alternatives-considered mandate, gated by `verify-agent-note-format`); [docs/postmortem/README.md](../../../docs/postmortem/README.md) — when an incident earns a postmortem.
- [docs/i18n/README.md](../../../docs/i18n/README.md) — the bilingual pairing contract; editing either side of a pair obligates the counterpart in the same change.
- Root [AGENTS.md](../../../AGENTS.md) — the standing orders whose budget discipline this skill protects.
@@ -32,8 +32,8 @@ The audit is a hunt for the standard's slop checklist, cheapest probes first. Es
3. Inspect long comments for reasoning transcripts: control-flow narration, test walkthroughs, proof of obvious branches, review findings, rejected local alternatives, and the same rationale repeated beside sibling methods. Preserve only a non-obvious contract or durable rationale; otherwise delete the comment.
4. Hunt duplication by grepping distinctive phrases. Keep one home and replace other copies with links.
5. Replace hand-written catalogs, test/status inventories, and JSDoc restatements with the authoritative tree, script, or generated reference.
-6. In `implemented/` RFCs, remove migration plans, acceptance-task checklists, and future-tense spec language. Keep concise verification contracts that identify the behaviors and tiers pinning the shipped decision, plus named coverage gaps.
-7. If removing prose changes a promised behavior rather than its explanation, use a proposed RFC first (follow [dsh-find-simplifications](../dsh-find-simplifications/SKILL.md)).
+6. In `implemented/` Agent Notes, remove migration plans, acceptance-task checklists, and future-tense spec language. Keep concise verification contracts that identify the behaviors and tiers pinning the shipped decision, plus named coverage gaps.
+7. If removing prose changes a promised behavior rather than its explanation, use a proposed Agent Note first (follow [dsh-find-simplifications](../dsh-find-simplifications/SKILL.md)).
Keep every load-bearing rule, preferably as one to three lines plus a link to its rationale. Cut stories, duplicates, status notes, and the path used to derive the rule. Do not create a new explanation merely to relocate disposable reasoning.
diff --git a/.agents/skills/dsh-find-simplifications/SKILL.md b/.agents/skills/dsh-find-simplifications/SKILL.md
index 59dde3df0d..2ce80f3f74 100644
--- a/.agents/skills/dsh-find-simplifications/SKILL.md
+++ b/.agents/skills/dsh-find-simplifications/SKILL.md
@@ -1,17 +1,17 @@
---
name: dsh-find-simplifications
-description: 'Use when working in the deepseek-harness repo to find non-obvious simplification candidates and write proposed RFCs or inline TODO/FIXME/XXX notes for dead, duplicated, speculative, or over-built code surfaces; especially for requests like "find simplification RFCs", "look for unnecessary complexity", "audit for removal-style cleanups", or "fold worthwhile simplification ideas from another PR".'
+description: 'Use when working in the deepseek-harness repo to find non-obvious simplification candidates and write proposed Agent Notes or inline TODO/FIXME/XXX notes for dead, duplicated, speculative, or over-built code surfaces; especially for requests like "find simplification Agent Notes", "look for unnecessary complexity", "audit for removal-style cleanups", or "fold worthwhile simplification ideas from another PR".'
---
# Finding DeepSeek Harness Simplifications
-This skill helps turn a broad "find things to simplify" request into evidence-backed RFCs that remove or collapse existing harness surface area. It is guidance, not a checklist: follow the code, keep judgment active, and prefer a few well-proven candidates over a pile of thin guesses.
+This skill helps turn a broad "find things to simplify" request into evidence-backed Agent Notes that remove or collapse existing harness surface area. It is guidance, not a checklist: follow the code, keep judgment active, and prefer a few well-proven candidates over a pile of thin guesses.
## Start With Repo Context
-- Read `AGENTS.md`, especially the pre-release stance and the conventions (including the tests-are-not-golden-truth and RFCs-are-not-golden-truth doctrines), plus [docs/defensive-patterns.md](../../../docs/defensive-patterns.md) and [docs/testing.md](../../../docs/testing.md).
+- Read `AGENTS.md`, especially the pre-release stance and the conventions (including the tests-are-not-golden-truth and Agent Notes-are-not-golden-truth doctrines), plus [docs/defensive-patterns.md](../../../docs/defensive-patterns.md) and [docs/testing.md](../../../docs/testing.md).
- Skim [docs/architecture.md](../../../docs/architecture.md) before judging anything under `packages/`; simplifications that fight the service map or event taxonomy need extra evidence.
-- Use the RFC index ([docs/rfc/README.md](../../../docs/rfc/README.md)) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../../docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md), [shared persistence write coordinator](../../../docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), [capability seams](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md), and the twin adapter / dual persistence backend RFCs.
+- Use the Agent Note tree and its [contract](../../notes/README.md) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md), [shared persistence write coordinator](../../notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), [capability seams](../../notes/implemented/architecture/2026-06-13-capability-seams.md), and the twin adapter / dual persistence backend Agent Notes.
- Treat dual LLM adapters and dual persistence backends as intentional by default. Do not propose deleting either twin/backend as "low effort" unless the user explicitly overrides that constraint. Removing an unused method or hook inside a protected seam can still be valid if it does not collapse the protected design.
## What Counts As A Strong Candidate
@@ -24,10 +24,10 @@ A strong simplification removes, folds, or demotes something real and has clear
- A seam has methods every implementation must support but no consumer uses.
- A package boundary exists only for test/demo/support code and adds publish or dependency overhead.
- A feature implements speculative product generality: multi-session/session-load, background task rosters, live registry invalidation, mid-turn steering, tool-owned UI rendering, and similar shapes with no product owner.
-- An invariant, rollback path, goldens set, or special-case test exists only to protect an unused surface.
+- An invariant, rollback path, set of expected outputs, or special-case test exists only to protect an unused surface.
- The simplified behavior may differ slightly, but the new behavior is still reasonable and easier to explain.
-Thin candidates are usually not enough for an RFC: deleting one typo, running `knip` once, removing an intentionally documented backend/adapter, or flagging "this looks complex" without call-site proof.
+Thin candidates are usually not enough for an Agent Note: deleting one typo, running `knip` once, removing an intentionally documented backend/adapter, or flagging "this looks complex" without call-site proof.
## Survey Broadly
@@ -37,7 +37,7 @@ Use parallel subagents when the user asks for breadth or many candidates. Give e
- ACP and UI surfaces: `session/*` methods, terminal `_meta`, transcript rendering, single vs multi-session state.
- LLM/tools/system prompt: stream/generate surfaces, assemblers, registries, tool schema defaults, presentation hooks.
- Bash and tool execution: foreground/background split, task ownership, output spill files, executor methods.
-- Packages/examples/scripts/tests: package boundaries, static inventories, redundant snapshot goldens, support packages.
+- Packages/examples/scripts/tests: package boundaries, static inventories, redundant snapshot expected outputs, support packages.
If subagents are unavailable, simulate the same breadth yourself. Do not let the first good candidate stop the survey.
@@ -54,7 +54,7 @@ For complex asynchronous code, draw the ownership graph and map each sentinel, r
For every symbol or behavior, classify consumers before writing:
- Production corpus: `packages/*/src`, `examples/*/src`, `examples/**/*.yml`, runtime scripts, and loader/config paths.
-- Non-production corpus: tests, README/docs, RFCs, snapshots, generated goldens, and comments.
+- Non-production corpus: tests, README/docs, Agent Notes, snapshots, generated expected outputs, and comments.
- Ambiguous corpus: examples and scripts that may be product smoke paths. Inspect usage before classifying.
Use `rg` first. Good searches include the exact symbol, event name, package name, config key, method name with both `.name(` and `name(`, and any wire strings. Then read the call sites. `knip` can help, but it is not a substitute for understanding public interfaces, dynamic event names, tests, docs, and Cordis loader paths.
@@ -62,17 +62,17 @@ Use `rg` first. Good searches include the exact symbol, event name, package name
Reject or downgrade a candidate when:
- A production caller exists and the simplification would be a feature decision rather than a cleanup.
-- The surface is explicitly justified by an implemented RFC or a hard-won defensive pattern, and the new evidence does not beat that reason.
+- The surface is explicitly justified by an implemented Agent Note or a hard-won defensive pattern, and the new evidence does not beat that reason.
- The removal would force unrelated churn without actually making the contract smaller.
- The idea is correct but tiny. Add a targeted TODO/FIXME/XXX instead, using the urgency semantics in [docs/development.md](../../../docs/development.md).
-## Write The RFC
+## Write The Agent Note
-Create one file per durable proposal under `docs/rfc///yyyy-mm-dd-topic.md`, following the lifecycle/classification contract in `docs/rfc/README.md`. Regenerate `docs/rfc/INDEX.md`; never add a manual RFC table to the README. Keep prose paragraphs on one physical line and use relative Markdown links.
+Create one file per durable proposal under `.agents/notes///yyyy-mm-dd-topic.md`, following the lifecycle/classification contract in `.agents/notes/README.md`. Keep prose paragraphs on one physical line and use relative Markdown links.
Prefer this shape, adjusting when the idea needs it:
-- `# RFC: `
+- `# Agent Note: `
- `Status: proposed`
- `## Problem`: name the current surface, cite the relevant files, and state the consumer evidence. Separate production callers from tests/docs.
- `## Proposal`: say exactly what to remove, fold, demote, or rehome. Include tests, docs, READMEs, JSDoc, event-taxonomy, snapshot, and generated-file cleanup when relevant.
@@ -80,7 +80,7 @@ Prefer this shape, adjusting when the idea needs it:
- `## Acceptance criteria`: observable end state and gates.
- `## Risks`: public API changes, behavior changes, future product wants, and why the tradeoff is still reasonable.
-Be concrete enough that an implementing PR can follow the trail. Avoid vague "simplify this package" RFCs. When a proposal overlaps an existing RFC, consolidate the useful details into the existing one rather than creating a duplicate.
+Be concrete enough that an implementing PR can follow the trail. Avoid vague "simplify this package" Agent Notes. When a proposal overlaps an existing Agent Note, consolidate the useful details into the existing one rather than creating a duplicate.
## Inline TODO Notes
@@ -88,25 +88,25 @@ Use inline TODO/FIXME/XXX only for small, local cleanups that are clearly useful
- Name the smell with a stable tag, e.g. `TODO(double-default)` or `XXX(unused-default)`.
- Explain why it is safe to revisit and what action would simplify it.
-- Do not add TODOs for speculative complaints or for behavior that needs an RFC-level decision.
+- Do not add TODOs for speculative complaints or for behavior that needs an Agent Note-level decision.
## When Folding Another PR Or Branch
Diff the sibling branch against `origin/master`, not against the current PR branch, so you see its independent contribution. For each item:
-- Port non-overlapping RFCs or TODOs that meet the quality bar.
-- Consolidate overlapping material into the existing RFC that owns the topic.
+- Port non-overlapping Agent Notes or TODOs that meet the quality bar.
+- Consolidate overlapping material into the existing Agent Note that owns the topic.
- Do not port duplicate or lower-confidence proposals just to preserve the count.
- Update the PR body so reviewers see the true candidate count and scope.
- Close the duplicate PR only when the user asked you to, or when you clearly own that housekeeping.
## Validation And PR Hygiene
-For docs-only RFC work, run at least `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`. For code comments or skill changes, also run the relevant validator when one exists. Before pushing, expect the pre-push hook to run module graph freshness, unit tests, snapshots, doc-sync, and hygiene.
+For docs-only Agent Note work, run at least `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`. For code comments or skill changes, also run the relevant validator when one exists. Before pushing, expect the pre-push hook to run module graph freshness, unit tests, snapshots, doc-sync, and hygiene.
When opening or updating a PR, summarize:
-- How many RFCs and inline notes were added.
+- How many Agent Notes and inline notes were added.
- The main areas surveyed.
- What was intentionally excluded.
- Which checks passed.
diff --git a/.agents/skills/dsh-pre-push-checks/SKILL.md b/.agents/skills/dsh-pre-push-checks/SKILL.md
index 4ce005ee79..a138c5b4b4 100644
--- a/.agents/skills/dsh-pre-push-checks/SKILL.md
+++ b/.agents/skills/dsh-pre-push-checks/SKILL.md
@@ -5,7 +5,7 @@ description: Use before pushing, force-pushing, marking ready for review, claimi
# DSH Pre-Push Checks
-Use this skill to choose and run the smallest sufficient verification set before a `deepseek-harness` push. Do not treat the local pre-push hook as the full CI contract: CI also runs coverage, build, demo smoke, and built-bin smoke.
+Use this skill to choose and run the smallest sufficient verification set before a `deepseek-harness` push. Do not treat the local pre-push hook as the full CI contract: CI also runs coverage, build, and built-bin smoke.
## First Steps
@@ -41,7 +41,7 @@ Why `test:coverage`, not only `test`: CI enforces per-file 100% coverage. A bran
## Add Gates By Touched Surface
-Run `pnpm run doc-sync` and `pnpm run verify-module-graph` when the diff touches Markdown docs, package manifests, package imports/exports, generated catalogs, RFCs, architecture docs, translation pairs, Mermaid diagrams, or comments that cite docs/packages.
+Run `pnpm run doc-sync` and `pnpm run verify-module-graph` when the diff touches Markdown docs, package manifests, package imports/exports, generated catalogs, Agent Notes, architecture docs, translation pairs, Mermaid diagrams, or comments that cite docs/packages.
Run `pnpm run build` and `pnpm run hygiene` when the diff touches any package `package.json`, dependency graph, public exports, build config, declaration surface, bundled runtime path, or code that will be consumed from built `lib/`.
@@ -54,7 +54,7 @@ pnpm run test:snapshot
Run built-bin smoke tests after `pnpm run build` when app packages, app boot, package runtime imports, bin entries, loader behavior, or published artifact paths change.
```sh
-pnpm exec vitest run --config vitest.e2e.config.ts packages/examples/stdio-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/tests/built-bin.e2e.ts
+DSH_EXAMPLE_MODE=lib pnpm exec vitest run --config vitest.e2e.config.ts examples/headless-agent/tests/keyless-smoke.e2e.ts examples/tui-agent/tests/tui-keyless-smoke.e2e.ts packages/examples/cli-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/tests/built-bin.e2e.ts
```
Run real e2e when behavior depends on a real model/API, tool-use loop, ACP integration, prompt injection, or end-to-end agent UX. If `.env` is available, use it; do not print secrets.
diff --git a/.agents/skills/dsh-prose-standard/SKILL.md b/.agents/skills/dsh-prose-standard/SKILL.md
index b4cc300f14..faf234ae7c 100644
--- a/.agents/skills/dsh-prose-standard/SKILL.md
+++ b/.agents/skills/dsh-prose-standard/SKILL.md
@@ -45,7 +45,7 @@ This is not a one-way shortening pass. Add or restore prose when code, types, an
- **Tests:** explain only non-obvious test design—why a fixture, assertion, platform accommodation, real entry path, or indirect observation is necessary. Delete walkthroughs and inventories.
- **Cookbooks:** include prerequisites, required actions, the real entry path, observable verification, and concise warnings.
- **READMEs:** include the consumer contract: configuration, semantics, failures, limitations, extension points, and model-visible effects. Quote stable model-visible text owned by the package; link generated catalogs and cross-package owners. Keep durable gaps and maintainer traps, not ordinary cleanup inventories. Follow the [package README contract](../../../docs/cookbook/adding-a-package.md#4-write-the-package-readme).
-- **RFCs:** retain unique rationale, mechanisms, alternatives, consequences, shipped verification contracts, and named coverage gaps. Implemented RFCs state shipped reality in the present tense; remove planning checklists, not evidence of what pins the decision.
+- **Agent Notes:** retain unique rationale, mechanisms, alternatives, consequences, shipped verification contracts, and named coverage gaps. Implemented Agent Notes state shipped reality in the present tense; remove planning checklists, not evidence of what pins the decision.
- **Postmortems:** retain the incident sequence, evidence, causal chain, impact, and prevention. Remove repeated persuasion or implementation detail that does not establish causality.
- **Skills and agent instructions:** state behavioral guardrails and explicit scope limitations such as “guidance, not a script/checklist.” Keep the workflow concise and link its source of truth.
- **Examples and configuration comments:** explain boundaries, non-obvious wiring or load order, security stance, replay behavior, exceptions, and likely misuse. Do not narrate entries that the configuration already shows.
diff --git a/.agents/skills/dsh-prose-standard/references/examples.md b/.agents/skills/dsh-prose-standard/references/examples.md
index 7edb01af3d..c4270ecc98 100644
--- a/.agents/skills/dsh-prose-standard/references/examples.md
+++ b/.agents/skills/dsh-prose-standard/references/examples.md
@@ -56,7 +56,7 @@ Event order and its current-request consequence are caller-visible behavior, not
**Over-trimmed:** “Worker realm support.”
-**Balanced:** “Owns the worker realm and its host bridge. Realm initialization is single-shot; disposal terminates the worker and rejects later calls. See the worker-isolation RFC for the protocol rationale.”
+**Balanced:** “Owns the worker realm and its host bridge. Realm initialization is single-shot; disposal terminates the worker and rejects later calls. See the worker-isolation Agent Note for the protocol rationale.”
**Over-detailed:** A paragraph-by-paragraph preview of the classes and helper functions below.
@@ -84,17 +84,17 @@ Keep mapping details that explain an abstraction boundary or intentional informa
## Link rationale while keeping the local contract
-**Over-trimmed:** “Disposal is documented in the lifecycle RFC.”
+**Over-trimmed:** “Disposal is documented in the lifecycle Agent Note.”
-**Balanced:** “Disposal aborts the run and waits for provider quiescence. See the lifecycle RFC for ownership and race handling.”
+**Balanced:** “Disposal aborts the run and waits for provider quiescence. See the lifecycle Agent Note for ownership and race handling.”
-**Over-detailed:** Repeating the RFC's promise choreography and rejected ownership models beside every disposer.
+**Over-detailed:** Repeating the Agent Note's promise choreography and rejected ownership models beside every disposer.
Keep the behavior and completion guarantee where callers need them. Link aggressively for the algorithm and rationale; a link cannot replace the local contract.
-## Implemented RFCs retain verification contracts
+## Implemented Agent Notes retain verification contracts
-**Over-trimmed:** Deleting the entire Testing section because the RFC has already shipped.
+**Over-trimmed:** Deleting the entire Testing section because the Agent Note has already shipped.
**Balanced:** “Unit tests cover cancellation before and after publication, disposal quiescence, and provider reload. A built-entry smoke covers the real loader path; snapshot coverage is deferred because the transport is process-specific.”
@@ -162,6 +162,6 @@ Know what the generator extracts. That fragment must preserve the contract neede
**Over-detailed:** Listing private helper cleanup and unused test-only accessors with no caller or maintainer consequence.
-**Balanced:** “Provider selection is cached for the plugin lifetime; installing or repairing a provider requires reload.” Keep ordinary cleanup in its TODO or RFC.
+**Balanced:** “Provider selection is cached for the plugin lifetime; installing or repairing a provider requires reload.” Keep ordinary cleanup in its TODO or Agent Note.
Retain gaps and non-obvious constraints that affect use or safe maintenance. A package README is not a backlog dump.
diff --git a/.agents/skills/dsh-translate-docs/SKILL.md b/.agents/skills/dsh-translate-docs/SKILL.md
index d935c5c4b7..8786eb5e6b 100644
--- a/.agents/skills/dsh-translate-docs/SKILL.md
+++ b/.agents/skills/dsh-translate-docs/SKILL.md
@@ -1,10 +1,16 @@
---
name: dsh-translate-docs
-description: Use when creating or updating the bilingual counterpart of a doc in this repo (English ↔ Chinese pairs) — orients the translator to the pairing contract, the terminology source of truth, the translation rules, and the consistency gate that verifies the result
+description: Use when creating or updating the bilingual counterpart of a doc in this repo (English ↔ Chinese pairs) — tells the orchestrating agent when to delegate translation to a subagent, and orients the translator to the pairing contract, the terminology source of truth, the translation rules, and the consistency gate that verifies the result
---
# Translating DeepSeek-Harness docs
+## Delegate to a subagent
+
+When this skill fires and translations need to be written, do not translate yourself: spawn a subagent to do the translation work. If you are that delegated subagent, skip this section; the sections from here on address the agent actually writing the translation.
+
+## What this skill is
+
**This skill is guidance, not a translation memory.** It is the workflow map for keeping `foo.md ↔ foo.zh.md` pairs consistent and natural in both languages. Both languages carry equal authority — a change is authored in either one, and that side is the source for that update. You are the translator: the rules below say what must hold, not how to phrase any particular sentence — phrasing judgment is yours, terminology is not.
## Sources of truth (read, don't re-summarize)
diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml
index 198e7b4e09..48c96e4d20 100644
--- a/.github/workflows/build-exe-for-python-sdk.yml
+++ b/.github/workflows/build-exe-for-python-sdk.yml
@@ -1,7 +1,7 @@
name: Build single-exe
# Native builds for the release targets; see
-# docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
+# .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
# A full target run retains one SDK wheel and three runtime wheels; subset
# dispatch retains the SDK wheel and selected runtime wheels. Bare executables
# and source closures are test inputs. Run manually or label a PR
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index a2015696f4..3d1bba6c17 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -23,7 +23,7 @@ name: E2E (real DeepSeek API)
# in the BASE repo's context WITH secrets while still able to check out untrusted
# fork code — a textbook key-leak vector, especially once this repo is public.
# The fork/secret model and its public-repo implications are recorded in
-# docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md.
+# .agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md.
#
# Note: scheduled triggers are auto-disabled after 60 days of repo inactivity;
# push/pull_request/workflow_dispatch act as backstops.
@@ -110,9 +110,14 @@ jobs:
fi
echo "DEEPSEEK_API_KEY present."
+ # The e2e suites boot the example bins in `lib` mode (DSH_EXAMPLE_MODE=lib):
+ # the built artifact under plain Node, resolving plugins through real package
+ # exports — the shape a real consumer runs. That requires a prior build.
+ - name: Build (lib for the e2e example bins)
+ run: pnpm run build
+
# Real-API end-to-end tests only. The keyless gates (lint/typecheck/
- # coverage/snapshot/build/etc.) already run in ci.yml on every push/PR;
- # no need to repeat them or build first (tests run unbuilt via tsx).
+ # coverage/snapshot/etc.) already run in ci.yml on every push/PR.
# DEEPSEEK_BASE_URL is pinned to the external API; the secret is scoped to
# this step (and preflight) only — never exposed to checkout/setup/install.
- name: E2E tests (real DeepSeek API)
@@ -120,4 +125,5 @@ jobs:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
DEEPSEEK_BASE_URL: https://api.deepseek.com
DSH_E2E_MAX_WORKERS: 14
+ DSH_EXAMPLE_MODE: lib
run: pnpm run test:e2e
diff --git a/.github/workflows/expected-filenames.yml b/.github/workflows/expected-filenames.yml
new file mode 100644
index 0000000000..328da95529
--- /dev/null
+++ b/.github/workflows/expected-filenames.yml
@@ -0,0 +1,21 @@
+name: Expected filenames
+
+on:
+ pull_request:
+ paths:
+ - '*[gG][oO][lL][dD][eE][nN]*'
+ - '**/*[gG][oO][lL][dD][eE][nN]*'
+ - '!vendor/**'
+
+permissions:
+ contents: read
+
+jobs:
+ expected-filenames:
+ name: no golden filenames
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Check tracked filenames
+ run: scripts/check-expected-filenames.sh
diff --git a/.github/workflows/landlock-run.yml b/.github/workflows/landlock-run.yml
new file mode 100644
index 0000000000..8916f59a56
--- /dev/null
+++ b/.github/workflows/landlock-run.yml
@@ -0,0 +1,127 @@
+# Manually-dispatched CI for the landlock-run source of record
+# (native/landlock-run). A separate workflow from ci.yml on purpose: the
+# subtree is a self-contained pnpm workspace with its own gates, exercised on
+# demand — per-architecture native legs (build + behavioral tests + pack
+# rehearsal on real kernels) plus one darwin leg proving the documented
+# degradation on hosts without a platform package. Legs derive from the
+# subtree's checked-in package matrix (scripts/github-matrix.mjs). Packing
+# for npm happens in the release mirror (node-addon-landlock-run) after an
+# export — see native/README.md; this workflow never packs for release.
+name: Landlock Run
+
+on:
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+defaults:
+ run:
+ working-directory: native/landlock-run
+
+jobs:
+ matrix:
+ name: Matrix
+ runs-on: ubuntu-24.04
+ outputs:
+ ci: ${{ steps.matrix.outputs.ci }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - id: matrix
+ run: echo "ci=$(node ./scripts/github-matrix.mjs ci)" >> "$GITHUB_OUTPUT"
+
+ native:
+ name: ${{ matrix.platform }}
+ needs: matrix
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJson(needs.matrix.outputs.ci) }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ package_json_file: native/landlock-run/package.json
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: native/landlock-run/pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Install musl toolchain
+ run: |
+ sudo apt-get update -q
+ sudo apt-get install -yq musl-tools
+
+ - name: Build TypeScript
+ run: pnpm build:ts
+
+ - name: Typecheck
+ run: pnpm typecheck
+
+ - name: Build native binaries (this architecture is the builder of record)
+ run: pnpm build:native
+
+ - name: Entry tests (keyless)
+ run: node ./test/entry.test.js
+
+ # NALR_REQUIRE_LANDLOCK: a self-skip on the very platform that exists to
+ # prove enforcement would be a false green, so an unenforcing kernel
+ # fails the leg instead of skipping.
+ - name: Launcher tests (real kernel enforcement)
+ run: node ./test/launcher.test.js
+ env:
+ NALR_REQUIRE_LANDLOCK: 1
+
+ - name: Pack rehearsal (pack → install → confine, this platform only)
+ run: |
+ node ./scripts/pack-release.mjs .release/npm --current-platform-only
+ node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only
+ env:
+ NALR_REQUIRE_LANDLOCK: 1
+
+ darwin:
+ name: darwin (no platform package — degradation proof)
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ package_json_file: native/landlock-run/package.json
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: native/landlock-run/pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build TypeScript
+ run: pnpm build:ts
+
+ - name: Typecheck
+ run: pnpm typecheck
+
+ - name: Entry tests (keyless)
+ run: node ./test/entry.test.js
+
+ - name: Launcher tests (must self-skip cleanly)
+ run: node ./test/launcher.test.js
+
+ - name: Pack rehearsal (entry only — fallback resolution + unusable probe)
+ run: |
+ node ./scripts/pack-release.mjs .release/npm --current-platform-only
+ node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only
diff --git a/.github/workflows/pi-ai-provider-e2e.yml b/.github/workflows/pi-ai-provider-e2e.yml
new file mode 100644
index 0000000000..d198abf5b5
--- /dev/null
+++ b/.github/workflows/pi-ai-provider-e2e.yml
@@ -0,0 +1,78 @@
+name: E2E (pi-ai Azure OpenAI and Anthropic)
+
+# This suite spends tokens against two external providers and is intentionally
+# opt-in. It has no push, pull_request, schedule, or workflow_call trigger.
+on:
+ workflow_dispatch:
+ inputs:
+ azure_openai_model:
+ description: Azure OpenAI model from pi-ai's installed catalog
+ required: true
+ default: gpt-5.5
+ type: string
+ anthropic_model:
+ description: Anthropic model from pi-ai's installed catalog
+ required: true
+ default: claude-opus-4-8
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ e2e:
+ runs-on: ubuntu-latest
+ name: Azure OpenAI Responses + Anthropic Messages
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 24
+
+ - name: Enable corepack (pnpm)
+ run: corepack enable
+
+ - name: Resolve pnpm store path
+ id: pnpm-store
+ run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/cache@v4
+ with:
+ path: ${{ steps.pnpm-store.outputs.path }}
+ key: ${{ runner.os }}-node-24-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-node-24-pnpm-
+
+ - name: Install (immutable)
+ run: pnpm install --frozen-lockfile
+
+ # The tests self-skip locally when a credential is absent. A manually
+ # dispatched CI run must fail instead of reporting an all-skipped green.
+ - name: Preflight (require provider API keys)
+ env:
+ AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY_EXTERNAL }}
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }}
+ run: |
+ set -euo pipefail
+ missing=0
+ for name in AZURE_OPENAI_API_KEY ANTHROPIC_API_KEY; do
+ if [ -z "${!name:-}" ]; then
+ echo "::error::${name} is empty. Configure the corresponding *_EXTERNAL repository secret."
+ missing=1
+ fi
+ done
+ exit "$missing"
+
+ - name: E2E tests (real Azure OpenAI and Anthropic APIs)
+ env:
+ AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY_EXTERNAL }}
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_EXTERNAL }}
+ DSH_PI_AI_OPENAI_MODEL: ${{ inputs.azure_openai_model }}
+ DSH_PI_AI_OPENAI_BASE_URL: https://openai-routerhub-resource.services.ai.azure.com/api/projects/openai/openai/v1
+ DSH_PI_AI_ANTHROPIC_MODEL: ${{ inputs.anthropic_model }}
+ DSH_E2E_MAX_WORKERS: 2
+ run: >-
+ pnpm exec vitest run --config vitest.e2e.config.ts
+ packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts
diff --git a/.github/workflows/sandbox.yml b/.github/workflows/sandbox.yml
index 561d74b587..51dfe06f1c 100644
--- a/.github/workflows/sandbox.yml
+++ b/.github/workflows/sandbox.yml
@@ -19,7 +19,7 @@ permissions:
contents: read
jobs:
- # Keyless real-kernel sandbox proofs (sandbox RFC § Testing): each ladder
+ # Keyless real-kernel sandbox proofs (sandbox Agent Note § Testing): each ladder
# rung is only provable on a host where it enforces, so this job fans out
# an OS×runner matrix — bwrap and Landlock on Linux (separate legs: the
# Landlock files force the bwrap rung off, so each leg proves exactly one
diff --git a/AGENTS.md b/AGENTS.md
index cca6f73cdf..2479993ae4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -28,15 +28,17 @@ packages/ @deepseek-ai/dsh- workspaces at packages///
cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime
hooks/ Claude Code / Codex hook bridges + shared wire-protocol library
session-persistence/ persistence seam + JSONL/SQLite backends
- ui/ ACP/stdio/JSON-RPC bridges; boot, approval, interaction plugins
- examples/ demo bundles (agent-spine + stdio/ACP/JSON-RPC bins) leaves load
+ ui/ ACP/TUI/JSON-RPC bridges; boot, approval, interaction plugins
+ examples/ demo bundles (agent-spine + TUI/CLI/ACP/JSON-RPC bins) leaves load
support/ dev/test infrastructure packages
util/ zero-dependency utilities
python/ Python SDK and bundled runtime (see python/README.md)
+native/ node-addon-landlock-run source of record (see native/README.md)
examples/ Runnable cordis.yml leaves over packages/examples bundles (see examples/AGENTS.md)
-docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md)
+.agents/ Agent workflows and Agent Notes (`notes/`)
+docs/ architecture, generated catalogs, postmortems, cookbook (see docs/AGENTS.md)
scripts/ repo gates and generators
-website/ VitePress docs site (zh-CN); api/ pages generated from source
+website/ VitePress projection of selected bilingual docs/ sources
```
Package groups: [packages/README.md](packages/README.md).
@@ -48,8 +50,8 @@ pnpm install # pnpm workspaces, node ^22.19 || >=24
pnpm run test # vitest unit tests
pnpm run test:coverage # THE gating test run: per-file 100% coverage on packages/*/*/src
pnpm run test:e2e # real-API tests; self-skip without DEEPSEEK_API_KEY
-pnpm run test:snapshot # keyless ACP replay vs goldens; filter: -t
-pnpm run test:snapshot:record # re-record goldens (needs key)
+pnpm run test:snapshot # keyless ACP/headless/TUI replay vs expected outputs; filter: -t
+pnpm run test:snapshot:record # re-record expected outputs (needs key)
pnpm run typecheck
pnpm run lint
pnpm run duplication # cross-file TypeScript clone detection
@@ -57,8 +59,8 @@ pnpm run build # tsc emits lib/types, tsdown bundles runtime
pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check
pnpm run doc-sync # all documentation gates; see the doc-sync script in package.json
pnpm run website:build # VitePress build (doubles as the site's dead-link check)
-pnpm run demo:echo # mock-model REPL, no key needed
-pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY)
+pnpm run demo:headless "task" # one-shot agent (needs DEEPSEEK_API_KEY)
+pnpm run demo:tui # full-screen TUI coding agent (needs DEEPSEEK_API_KEY)
pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key)
pnpm run demo:acp # ACP server agent (needs DEEPSEEK_API_KEY)
```
@@ -83,20 +85,11 @@ pnpm run website:build
pnpm run verify-module-graph
pnpm run build
pnpm run hygiene
-out=$(printf 'echo ci smoke\n' | pnpm run demo:echo 2>&1)
-printf '%s\n' "$out" | grep -q '\[tool call\] echo({"text":"ci smoke"})'
-printf '%s\n' "$out" | grep -q '\[tool result\] ECHO: CI SMOKE'
-test -n "$(find .sessions -path '.sessions/cwd-*/main-session-*.jsonl' -type f -print -quit)"
-rm -rf .sessions
-pnpm exec vitest run --config vitest.e2e.config.ts packages/examples/stdio-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/tests/built-bin.e2e.ts packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts
+DSH_EXAMPLE_MODE=lib pnpm exec vitest run --config vitest.e2e.config.ts examples/headless-agent/tests/keyless-smoke.e2e.ts examples/tui-agent/tests/tui-keyless-smoke.e2e.ts packages/examples/cli-demo/tests/built-bin.e2e.ts packages/examples/acp-demo/tests/built-bin.e2e.ts packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts
```
`test:coverage`, not `test`, is the gate ([why](docs/testing.md)); report only commands actually run.
-## Agent efficiency
-
-- **One-round gate runs**: write full output to a temp file, print a summary, and include failure context (`grep -B3 -A15 -E 'FAIL|ERROR' "$out" | head -60`) in that invocation; never rerun to recover filtered detail.
-
## Secrets / .env
Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and root `.env`. cordis.yml allows `!!js` (never `!js`) only under plugin `config`; Loader metadata is static, so conditional composition uses overlays ([primer](docs/cordis-primer.md#loader-configuration)). Never commit credentials. CI e2e skips without a key; [testing.md](docs/testing.md) owns key policy.
@@ -104,7 +97,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`,
## Conventions
- Every npm package is `@deepseek-ai/dsh-`; vendored packages keep upstream names and are `private: true`. `cordis` is a peerDependency (+ dev) of every harness package.
-- ESM everywhere (`"type": "module"`). Cross-package imports use package names, never relative paths; in-package relative imports use explicit `.ts` extensions. Dev/test/demo run unbuilt via tsx + the root tsconfig `paths` map; builds are for outside consumers only.
+- ESM everywhere (`"type": "module"`). Cross-package imports use package names; in-package relative imports include `.ts`. CI subprocesses that boot examples or Cordis configs run built `lib/` under plain Node; only explicit source-path regressions use tsx ([testing policy](docs/testing.md#test-subprocess-launch-modes)).
- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer.
- **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`; scoped keys absent from payloads need `@dshScopeScan unsupported`. Public service methods document parameters and non-void returns.
- **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default.
@@ -113,17 +106,17 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`,
- **Plugins, not loop changes**: new behavior goes on the documented extension seams; changing `agent-loop` requires updating docs/architecture.md.
- **Capability seams are three packages** — interface / implementation / consumer; don't split preemptively.
- **Explicit > implicit at package seams**: defaulting is an explicit `resolve(request): Spec` step in the owning implementation, never a hidden `?? default` inside `run()` (the `dsh-bash` request/spec split is the template).
-- **No hardcoded tunables in plugins**: deployment choices are defaulted, validated `Config` fields changeable from cordis.yml; a `DEFAULT_*` constant or test seam is not configurability. Protocol constants, external specs, and security invariants stay fixed.
+- **No hardcoded tunables in plugins**: deployment-varying choices are validated `Config` fields changeable from cordis.yml; a `DEFAULT_*` constant or test seam is not configurability. Protocol constants, external specs, and security invariants stay fixed.
- **Misconfiguration fails loud** at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent.
- **Opaque cross-boundary ids are branded** (`Branded` from `dsh-brand`), never bare `string`.
- **An empty `catch` names what it swallows** and why nothing else can reach it; keep the `try` to one statement.
- **Prefer symmetry for parallel values**; unexplained asymmetry usually signals a missed extraction.
- **Tests describe behavior, not correctness.** Change obsolete behavior with its tests; explain why in the PR.
-- **Validate RFC premises against current code**; friction may expose overreach, so amend proposals before moving them to `implemented/`.
+- **Every non-trivial change MUST include at least one Agent Note in the same PR.** Update the owning note or add one, validate its premises against code, and exempt only mechanical/local edits ([scope](.agents/notes/README.md#when-to-write-one)).
- **Testing policy** — [docs/testing.md](docs/testing.md). Transcript changes need snapshots or a PR note. Fixtures must replay on macOS/Linux; fix fixtures, not normalizers.
- **A tool's ACP render intent is part of its design**, decided up front (`generic`/`terminal`/`diff`, `locations`); presentation methods are pure functions of `args` ([cookbook](docs/cookbook/adding-a-tool.md)).
- **Plan unit, e2e, and snapshot coverage** for new seams, lifecycle shapes, and transcript surfaces, and schedule any missing harness support before implementation.
-- **Merge PRs with merge commits**, never squash/rebase or rewrite pushed branches. Put a review fix on its introducing PR, then merge down the stack ([guide](docs/cookbook/responding-to-pr-review-on-a-stack.md)).
+- **Keep PRs coherent and merge with merge commits.** Split an independently meaningful feature or design decision into a separate or stacked PR when combining it obscures ownership, intent, or verification. Never squash/rebase or rewrite pushed branches; put a review fix on its introducing PR, then merge down the stack ([guide](docs/cookbook/responding-to-pr-review-on-a-stack.md)).
- TODO markers: `FIXME`/`TODO`/`XXX` by urgency ([semantics](docs/development.md)).
- Files end with exactly one trailing newline; `git diff --check` (pre-push) gates it.
@@ -135,9 +128,9 @@ Read [docs/defensive-patterns.md](docs/defensive-patterns.md) before lifecycle,
Everything compiles under `strict: true` with `noImplicitAny`; every remaining `any` explains why a narrower type is infeasible. Every module and export has concise JSDoc for its non-obvious contract; function-like exports include `@param`/`@returns`, as enforced by `verify-export-jsdoc`. Heritage-declared members, plugin-protocol slots, and constructors keep their docs at the declaring seam, protocol, or class.
-Comments and docs preserve complete contracts and non-obvious orientation, not reasoning transcripts. Do not narrate control flow or tests, preserve review history, or restate code. Keep factual clauses affecting behavior, failure, timing, ownership, or safe use; link aggressively to owning rationale. Use [dsh-prose-standard](.agents/skills/dsh-prose-standard/SKILL.md) for prose decisions. Encode enforceable invariants in checks, using narrow justified exceptions rather than disabling a rule globally.
+Comments and docs preserve complete contracts and non-obvious orientation, not reasoning transcripts. Do not narrate control flow or tests, preserve review history, or restate code. Keep factual clauses affecting behavior, failure, timing, ownership, or safe use; link aggressively to owning rationale. Use [dsh-prose-standard](.agents/skills/dsh-prose-standard/SKILL.md) for prose decisions. Wire mechanically checkable invariants into an executed top-level gate and prove each new or changed acceptance path rejects an invalid case. Use narrow justified exceptions instead of disabling a rule globally.
-Docs are part of every change: code changes update their README and JSDoc in the SAME change; a bilingual-pair edit updates the counterpart and re-records ([i18n contract](docs/i18n/README.md)). The writing rules — document the current state never the history, one physical line per paragraph, one home per fact — and the word-budget gate live in [docs/AGENTS.md](docs/AGENTS.md).
+Docs accompany every code change: update affected README/JSDoc contracts together; update both sides of a bilingual pair and re-record it ([i18n contract](docs/i18n/README.md)). Current-state prose, one physical line per paragraph, one home per fact, and word budgets live in [docs/AGENTS.md](docs/AGENTS.md).
## Editing these instructions
diff --git a/README.i18n.yaml b/README.i18n.yaml
index 790812344d..d78213a292 100644
--- a/README.i18n.yaml
+++ b/README.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-README.md: 53dd3896eb15800125673e7c44f7de02daca9376
-README.zh.md: ab826f62658248249ec18c57b35c0065c0f909d1
+README.md: 32958db0e74bd14d6d41e8d7886b8d3257fe0f59
+README.zh.md: b28b175a8296347a7bed05b4e53c0d75dc51efed
diff --git a/README.md b/README.md
index 53dd3896eb..32958db0e7 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,11 @@ This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) fra
```sh
pnpm install
pnpm run test # vitest
-pnpm run demo:repl # REPL agent demo (needs DEEPSEEK_API_KEY)
-pnpm run demo:acp # ACP server agent demo (needs DEEPSEEK_API_KEY)
+# Agent demos require DEEPSEEK_API_KEY.
+pnpm run demo:tui # full-screen TUI coding agent
+pnpm run demo:headless "task" # one-shot coding agent
+pnpm run demo:cordis # self-referential agent demo
+pnpm run demo:acp # ACP server agent demo
```
For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) and [documentation graph index](docs/graph-atlas.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/).
diff --git a/README.zh.md b/README.zh.md
index ab826f6265..b28b175a82 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -11,8 +11,11 @@
```sh
pnpm install
pnpm run test # vitest
-pnpm run demo:repl # REPL agent demo (needs DEEPSEEK_API_KEY)
-pnpm run demo:acp # ACP server agent demo (needs DEEPSEEK_API_KEY)
+# Agent demos require DEEPSEEK_API_KEY.
+pnpm run demo:tui # full-screen TUI coding agent
+pnpm run demo:headless "task" # one-shot coding agent
+pnpm run demo:cordis # self-referential agent demo
+pnpm run demo:acp # ACP server agent demo
```
面向开发者:先读[开发指南](docs/development.md),了解本地环境搭建、钩子、环境变量与质量门禁,动手改 package 之前再读[架构设计](docs/architecture.md)和[文档关系图索引](docs/graph-atlas.md)。局部上下文见 [packages/](packages/) 与 [vendor/](vendor/)。
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index 0c305e87c8..da0c03b088 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -1,6 +1,6 @@
# AGENTS.md — The documentation standard
-This file defines Markdown tiers, writing rules, and `verify-doc-budgets` ceilings. Use [dsh-doc-standards](../.agents/skills/dsh-doc-standards/SKILL.md) for placement and validation, and [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for required coverage and editorial judgment; the [doc-tiers RFC](rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md) owns rationale.
+This file defines Markdown tiers, writing rules, and `verify-doc-budgets` ceilings. Use [dsh-doc-standards](../.agents/skills/dsh-doc-standards/SKILL.md) for placement and validation, and [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for required coverage and editorial judgment; the [doc-tiers Agent Note](../.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md) owns rationale.
## The tier taxonomy: one home per fact
@@ -9,25 +9,26 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home;
| Tier | Job | Does NOT belong there |
|---|---|---|
| Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home |
-| Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`) | Orders specific to that subtree | Repo-wide rules the root file already carries |
-| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ RFCs), implementation-status annotations |
+| Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries |
+| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations |
| [core-data-structures/](core-data-structures/core.md) | The type catalog: literal shapes and semantics of the spine and seam vocabulary | Behavior narration (→ architecture.md) |
-| [rfc/](rfc/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` RFCs describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped |
+| [Agent Notes](../.agents/notes/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped |
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
-| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the RFC each guide links) |
+| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) |
+| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
-| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ RFCs), gate-by-gate enumerations that drift from `package.json` scripts |
-| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
+| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts |
+| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
-Placement: bugs → postmortems; rationale → RFCs; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
+Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
## Writing rules
-- **Document current state, not change history.** Avoid "previously/now/no longer", PRs, commits, and stack positions in durable prose; name the live mechanism. Put change stories in commits, PRs, RFCs, or postmortems.
-- **Write an RFC in the same PR for decisions a maintainer may reasonably revisit.** Mechanical or self-evident changes need none ([when to write one](rfc/README.md)).
+- **Document current state, not change history.** Avoid "previously/now/no longer", PRs, commits, and stack positions in durable prose; name the live mechanism. Put change stories in commits, PRs, Agent Notes, or postmortems.
+- **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)).
- **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit.
-- **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type definition is fenced ` ```ts type-equiv ` and registered in the manifest so it cannot drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)).
+- **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)).
- **The [core-data-structures catalog](core-data-structures/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](core-data-structures/core.md#what-counts-as-core)).
- **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)).
- **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, conditions, timing, modality, exceptions, consequences, and non-obvious orientation; delete implementation narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link to its owning rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for required coverage, decision rules, and examples.
@@ -43,15 +44,15 @@ When the gate goes red:
2. **Condense** content that belongs here but can be shorter.
3. **Raise** the ceiling only when the words truly need the space; justify the manifest diff in the PR. A too-low ceiling is a budget bug.
-Ceilings are guardrails, not reduction targets. Retain at least 5% headroom; lower a ceiling only when the document's durable contract still has room, and raise it when necessary content would otherwise be deleted. Targets: root `AGENTS.md` ≤ 1,500 words; `architecture.md` ≤ 1,800; each subtree `AGENTS.md` ≤ 600, except this file ≤ 1,250; `packages/README.md` ≤ 600. Review and the slop checklist govern unbudgeted tiers.
+Ceilings are guardrails, not reduction targets. Retain at least 5% headroom; lower a ceiling only when the document's durable contract still has room, and raise it when necessary content would otherwise be deleted. Targets: root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; each subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 650 and this file ≤ 1,250; `packages/README.md` ≤ 600. Review and the slop checklist govern unbudgeted tiers.
## The slop checklist
Hunt these in any doc; the [dsh-doc-standards](../.agents/skills/dsh-doc-standards/SKILL.md) skill runs this list as an audit:
- The same rule stated in more than one home. Grep a distinctive phrase; keep one home, convert the rest to links.
-- Narrated history: "previously", "now", "no longer", "used to", "renamed", "was moved", references to PRs or commits. State the current fact; the why belongs in an RFC, the story in a postmortem or git.
-- A war story told inline where a one-line rule plus a postmortem/RFC link would do.
+- Narrated history: "previously", "now", "no longer", "used to", "renamed", "was moved", references to PRs or commits. State the current fact; the why belongs in an Agent Note, the story in a postmortem or git.
+- A war story told inline where a one-line rule plus a postmortem/Agent Note link would do.
- Implementation-status annotations in prose or diagrams ("implemented!", "future: …"). Status rots; the repo layout and package manifests carry it.
- Hand-restating a generated catalog or JSDoc: event tables, tool arg tables, method signatures. Link instead.
- Hand-maintained inventories of tests, packages, or implementation status when the tree or a generator is authoritative.
@@ -59,10 +60,10 @@ Hunt these in any doc; the [dsh-doc-standards](../.agents/skills/dsh-doc-standar
- The same rationale repeated beside sibling methods. State it once at the owning seam or shared helper.
- Paragraph walls: one paragraph carrying several rules and parenthetical asides. Split it, or demote the detail to the linked home.
- Emphasis inflation: bold, CAPS, or "critically" everywhere means nothing stands out. Reserve emphasis for the clause that changes behavior.
-- Spec-speak in `implemented/` RFCs: "should", migration plans, acceptance checklists. An implemented RFC describes what is, per [rfc/implemented/AGENTS.md](rfc/implemented/AGENTS.md).
+- Spec-speak in `implemented/` Agent Notes: "should", migration plans, acceptance checklists. An implemented Agent Note describes what is, per the [implemented-note instructions](../.agents/notes/implemented/AGENTS.md).
## Cross-reference with machine-checkable links, never free prose
-Link repository references with relative Markdown paths, never bare filenames or RFC numbers. `verify-md-links` catches missing targets; the [cross-link RFC](rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md) owns the rationale.
+Link repository references with relative Markdown paths, never bare filenames or Agent Note numbers. `verify-md-links` catches missing targets; the [cross-link Agent Note](../.agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md) owns the rationale.
The gate checks file existence, not `#anchor` validity — verify anchors yourself when linking to one.
diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md
index fa2c4bd0fb..2134c8b355 100644
--- a/docs/agent-lifecycle.md
+++ b/docs/agent-lifecycle.md
@@ -32,19 +32,40 @@ sequenceDiagram
LLM-->>Driver: StreamChunk*
Driver->>Session: assistant/chunk*
Session-->>SDK: session/eventassistant/chunk*
+ alt final adapter or terminal in-band request failure
+ Driver->>Session: step/end
+ Driver->>Hooks: agent/request-error waterfall
+ Hooks-->>Driver: retry in a new step or preserve the original error
+ else model request succeeded
Driver->>Hooks: agent/step-result waterfall
Driver->>Session: assistant/message
- Driver->>Session: tool/call
- Driver->>Tools: execute through pre and post waterfalls
- Tools-->>Session: tool-owned events when applicable
- Driver->>Session: tool/result and step/end
+ Driver->>Tools: classify pending call by executionMode
+ loop barriers and bounded rolling pool, reclassify before start
+ opt call starts
+ Driver->>Session: tool/call
+ Driver->>Tools: ordered pre, concurrent execute
+ Tools-->>Session: tool-owned events when applicable
+ end
+ opt next model-order result ready
+ Driver->>Tools: ordered post
+ Driver->>Session: tool/result
+ end
+ end
+ Driver->>Session: post-tool context and steering
+ Driver->>Hooks: agent/post-step serial checkpoint
+ Driver->>Session: step/end
Driver->>Hooks: agent/turn-continuation waterfall
Driver->>Hooks: agent/turn-stop serial terminal checkpoint
+ end
Driver->>Session: turn/end
Driver->>Persistence: session/flush parallel checkpoint
Driver-->>SDK: agent/status idle
```
+The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.
+
+`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and a fresh retry step, and returns retry only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.
+
SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.
Maintenance mode: curated Mermaid sequence; exact event signatures live in the generated Cordis catalog.
diff --git a/docs/architecture.md b/docs/architecture.md
index e7fd2c4b42..886da4b88c 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,12 +1,12 @@
# DeepSeek Harness Architecture
-The **DeepSeek Harness SDK** builds agent harnesses on Cordis. The principle is simple: **everything is a plugin**. The shipped loop is one plugin, not a privileged kernel.
+The **DeepSeek Harness SDK** builds agent harnesses on Cordis. **Everything is a plugin**; the shipped loop is not a privileged kernel.
## Overview
-A harness is one [Cordis](cordis-primer.md) context. Packages contribute service keys, typed events, and disposable registrations: services expose stable calls (`ctx.llm`, `ctx.tools`, `ctx.sessions`), events provide interception and notifications (`agent/request`, `tools/pre-execute`, `session/event`), and registrations install prompt sections, tools, providers, adapters, or listeners.
+A harness is one [Cordis](cordis-primer.md) context. Packages add services (`ctx.llm`, `ctx.tools`, `ctx.sessions`), typed events (`agent/request`, `tools/pre-execute`, `session/event`), and disposable prompt, tool, provider, adapter, and listener registrations.
-`packages/core/` groups the default agent flow; surrounding capabilities are equally first-class Cordis plugins.
+`packages/core/` groups the default agent flow; other capabilities are Cordis plugins.
### Default Services
@@ -16,23 +16,25 @@ A harness is one [Cordis](cordis-primer.md) context. Packages contribute service
| `ctx.sessions` | `dsh-session` | in-memory event-sourced sessions |
| `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and prompt variables |
| `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) |
-| `ctx.agents` | `dsh-agent` | live agent registry, public `Agent` handle, `agent/*` events |
-| `ctx.agentLoop` | `dsh-agent-loop` | shipped `ReactLoopAgent` driver |
+| `ctx.agents` | `dsh-agent` | live agents, delegated creation, `agent/*` events, and process-local initiator scope |
+| `ctx.agentLoop` | `dsh-agent-loop` | concrete `Agent` driver |
### Capability Services
| ctx key | Package family | Role |
|---|---|---|
| `ctx.llm` | [`llm/`](../packages/llm/README.md) | adapter registry and streaming model calls |
+| `ctx.tokenMeter` | [`llm/token-meter`](../packages/llm/token-meter/README.md) | singleton replay-aware request/surface pressure |
| `ctx.bash` | [`bash/`](../packages/bash/README.md) | foreground/background command execution |
| `ctx.sandbox` | [`sandbox/`](../packages/sandbox/README.md) | same-world process confinement (argv wrapping, per-call policy) |
+| `ctx.sandboxPolicy` | [`sandbox/`](../packages/sandbox/README.md) | shared sandbox policy home |
| `ctx.codeRuntime` | [`code-runtime/`](../packages/code-runtime/README.md) | model-written program execution |
| `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives and policy events |
| `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill provider registry and progressive disclosure |
| `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries |
-| `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-log compaction |
+| `ctx.compact`, `ctx.toolResultPrune` | [`compact/`](../packages/compact/README.md)/[`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | summary compaction; optional model-free result pruning |
| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers |
-| `ctx.modes` | [`mode/`](../packages/mode/README.md) | logged session modes (plan mode) |
+| `ctx.modes` | [`mode/`](../packages/mode/README.md) | logged collaboration modes |
| `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry + generic `task_*` control tools |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration |
| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable storage for session logs |
@@ -54,55 +56,69 @@ Waterfall events behave like around-middleware: a listener delegates by calling
## Default Loop Lifecycle
-The shipped loop drains work from prompt through checkpoint. Every pause is a service call or event available to plugins.
+The shipped loop drains prompt-to-checkpoint work through plugin-visible services and events.
-A **session** is one agent's append-only event log. A **turn** drains one queued batch and runs until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points.
+A **session** is an append-only log. Each ordinary **turn** claims one queued `send()` item; injection claims none. A claimed `send()` successor awaits the preceding claimed ordinary turn's checkpoint but may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)). A turn ends when model and plugins stop it. A **step** is one model request plus tools. Below ([sequence companion](agent-lifecycle.md)), quotes mark durable events; other names are extension points.
+
+No id mints `-session-`; `sessionId` resumes/creates; `resumeSessionId` needs history. Resume restores lineage, seeds, and delegation depth pre-publication. Failures emit `agent-loop/config-start-failed(sessionId, error)`; front doors reject; teardown stays silent.
### Turn Flow
```text
-prepare private session + agent.ctx -> await unpublished setup
+choose declarative identity and fresh/resume path
+ -> prepare private session + agent.ctx -> await unpublished setup
-> enter session + agent -> session/created -> agent/created
-> enable driving -> agent/session-start(source) -> start driver
forever:
- wait for queued messages
+ wait for a queued message
emit agent/status(running)
TURN:
'turn/start'
- each queued message -> agent/prompt-submit
+ claimed message -> agent/prompt-submit
allowed prompt -> 'user/message' plus injected context
- every prompt blocked -> 'turn/end'(rejected)
+ blocked prompt -> 'prompt/blocked' -> 'turn/end'(rejected)
STEP loop:
drain steering
assemble system prompt and tool schemas
agent/session-prefix (first step)
agent/pre-step
- 'step/start'
snapshot the derived messages (the reconstruction boundary)
+ 'step/start'
agent/request (config only) -> log request/header -> llm/stream (frozen)
+ on final adapter-path or terminal in-band failure:
+ 'step/end'
+ agent/request-error(original error, failure facts, immutable prior failures, signal)
+ retry in the next numbered step or preserve the original error
+ otherwise:
'assistant/chunk'
- agent/step-result
- 'assistant/message'
- each tool call:
- 'tool/call'
- tools/pre-execute -> monotonic guards -> tools/execute -> tools/post-execute -> tools/result
- 'tool/result'
- append post-tool context and steering
- 'step/end'
- agent/turn-continuation
- agent/turn-stop (terminal policy)
- stop unless tools or continuation policy ask for another step
+ agent/step-result
+ 'assistant/message' (transformed content or empty success anchor after step-result rejection)
+ schedule tool calls by ctx.tools.executionMode:
+ exclusive -> one-call barrier
+ parallel -> rolling pool, <= maxParallelToolCalls in flight; reclassify before start
+ each start -> 'tool/call' -> ordered tools/pre-execute -> concurrent tools/execute
+ each model-order result -> ordered tools/post-execute -> 'tool/result'
+ append accepted tool-batch context after all recorded results, then steering
+ agent/post-step
+ 'step/end'
+ agent/turn-continuation
+ agent/turn-stop (terminal policy)
+ stop unless tools or continuation policy ask for another step
'turn/end'
checkpoint persistence and notify idle/running status
```
-Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variables; unknown or valueless references fail the turn. `dsh-system-prompt` owns the harness identity and default persona, which an agent scope may shadow. The loop supplies `model` and `cwd` ([prompt-ownership RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)).
+Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variables; unknown or valueless references fail the turn. `dsh-system-prompt` owns the harness identity and default persona, which an agent scope may shadow. The loop supplies `model` and `cwd` ([prompt ownership](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)).
-Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; ordinary leftover steering after a turn is re-queued as input. A terminal `agent/turn-stop` is the explicit exception: it runs after ordinary continuation and steering folding, then remains authoritative through turn close and flush so steering from those later listeners is discarded rather than becoming another step or turn; ordinary queued prompts are preserved.
+Tool-time context—including async `agent.inject()` notices and post-tool `additionalContexts`—settles, then follows recorded results. Steering drains before `agent/post-step`, which observes durable output, results, context, and steering before signal closure. Leftovers become queued input. Terminal `agent/turn-stop` runs after continuation and steering folding, stays authoritative through turn close and flush, and discards later steering but preserves queued prompts.
+
+Pruning precedes summaries; overflow retries require durable progress. Bounded transient retries compose on `agent/request-error`; cancellation wins ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)).
### Failure Boundaries
-The turn is the containment boundary. A throwing listener, adapter error finish, or failed step ends it with an error reason and reports `agent/error` without killing the driver. `cancel()` clears queued and steering work, aborts the active model/tool boundary when possible, and records the turn end. Disposal stops the loop, awaits quiescence, unregisters the agent, and drains service disposers.
+The turn is the containment boundary. Adapter failures close the step, entering `agent/request-error` with the exact `Error`, `LlmFailure`, and retry history. Retry opens a numbered step; success clears history; exhaustion stores the failure on `turn/end`. Failed chunks commit no message or tool.
+
+Other failures use `agent/error`. Cancellation and disposal beat recovery; undispatched model tool calls receive synthetic `tool/call` and `ABORTED` result pairs before `turn/end`. `cancel()` clears queues and aborts active work; disposal awaits quiescence before unregistering.
Every session event is turn-enclosed. Reloading preserves an interrupted tail and closes it with a synthetic `interrupted` turn end. Failures after durable turn close report only through `agent/error` because no safe in-turn position remains. Each turn has one `TurnEndReason`; [TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap) owns the variants.
@@ -112,7 +128,7 @@ Every session event is turn-enclosed. Reloading preserves an interrupted tail an
### Agent Scope
-Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. Typed resolvers derive carrier checks from merged `Events` signatures and `scopeTarget` ([semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition controls](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
+Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. Typed resolvers derive carrier checks from merged `Events` signatures and `scopeTarget` ([semantic gates](../.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See [agent scope](../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition controls](../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md). `AgentLoop` runs drivers inside `ctx.agents.withInitiator()`; private orchestration derives `agent.session`; other identities stay explicit ([decision](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)).
## State
@@ -120,15 +136,15 @@ Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, re
The session log is the source of truth. `deriveMessages()` projects session events into the `Message[]` sent to the model; raw `assistant/chunk` events stay in the log for replay and UI fidelity. Replay, fork, resume, transcript rendering, telemetry, and persistence all derive from the same event stream.
-**Model-visible ⟺ logged**: the log reconstructs every request — messages at `step/start` fronted by the header's session prefix, headers by folding `request/header` — and dev invariants assert this ([reconstructability RFC](rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)).
+**Model-visible ⟺ logged**: the log reconstructs every request — messages at `step/start` fronted by the header's session prefix, headers by folding `request/header` — and dev invariants assert this ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
-Durability is a plugin concern. Persistence backends buffer synchronous `session/event` notifications and the loop awaits a turn-end checkpoint before moving on. The `SessionPersistence` seam stores `SessionEvent` directly, with metadata in `SessionHeader`; JSONL and SQLite share one contract suite.
+Durability is a plugin concern. Backends buffer synchronous `session/event` notifications; the loop awaits a turn-end checkpoint. `SessionPersistence` stores `SessionEvent` directly and metadata in `SessionHeader`; JSONL defaults to checksummed Zstandard, with SQLite under one contract.
### Model Content
-Messages contain typed blocks (`text`, `reasoning`, `tool-call`, `tool-result`) derived from merge-extensible `ContentBlockMap`; the same pattern types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. New block types coordinate adapters, UI bridges, compaction pricing, and persistence as one repo-wide contract.
+Messages contain typed blocks (`text`, `reasoning`, `tool-call`, `tool-result`) derived from merge-extensible `ContentBlockMap`; the same pattern types `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`. New block types coordinate adapters, UI bridges, compaction pricing, token metering, and persistence as one repo-wide contract; replay measurement types live in [token-meter.md](core-data-structures/token-meter.md).
-Streaming is a raw chunk protocol (`block-start` through `finish`) with `BlockAssembler` as the shared chunk-to-block assembler. The loop logs raw chunks while assembling them for dispatch. `LlmAdapter` is the provider seam: subclass, implement `stream()`, and register with `ctx.llm.registerAdapter(models, adapter)`. StreamChunk conventions live in [llm-streaming.md](core-data-structures/llm-streaming.md).
+Streaming uses raw chunks and `BlockAssembler`. One `LlmAdapter.stream()` is one provider attempt; adapters report facts, while recovery policy lives on `agent/request-error`. The loop logs chunks and successful provenance/replay state. Remote adapters stop stalled transport with per-read idle watchdogs. Replay state reaches targets only when routes share an adapter instance ([contract](core-data-structures/llm-streaming.md)).
## Extension And Composition
@@ -138,11 +154,11 @@ A swappable capability usually splits into **interface / implementation / consum
Some seams bend the template deliberately: LLM combines interface and consumer because adapters implement it; filesystem wraps provider primitives with policy; web keeps search/fetch provider registries behind one service; skills and subagents use named providers. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children ([subagent.md](core-data-structures/subagent.md)).
-`dsh-workspace-context` composes baselines on `agent/session-prefix` and appends `ctx.fs`-discovered nested changes on `tools/post-execute`; its [RFC](rfc/implemented/feature/2026-06-24-workspace-context.md) records isolation. `dsh-paths` owns shared paths.
+`dsh-workspace-context` composes baselines on `agent/session-prefix` and appends `ctx.fs`-discovered nested changes on `tools/post-execute`; its [decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) records isolation. `dsh-paths` owns shared paths.
### Bundles And Apps
-`dsh-agent-spine-demo` bundles the default spine ([README](../packages/examples/agent-spine-demo/README.md)). `dsh-stdio-demo` adds a terminal front door; `dsh-acp-demo` adds stdout-pure ACP over JSON-RPC ([ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK supplies its default only without an explicit config channel and drives `dsh-jsonrpc` over line-delimited JSON-RPC ([Python SDK](../python/README.md)). Deployments remain thin leaves with swappable backends and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
+`dsh-agent-spine-demo` bundles the default spine ([README](../packages/examples/agent-spine-demo/README.md)). `dsh-tui-demo` owns the interactive full-screen terminal; `dsh-cli-demo` runs one persisted headless turn with format-pure stdout; `dsh-acp-demo` adds stdout-pure ACP over JSON-RPC ([ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK supplies its default only without an explicit config channel and drives `dsh-jsonrpc` over line-delimited JSON-RPC ([Python SDK](../python/README.md)). Deployments remain thin leaves with swappable backends and optional product tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
### Where New Behavior Goes
@@ -156,7 +172,7 @@ New behavior should attach to a documented extension point; changing the shipped
| Add a long-running/background capability | register the work on `ctx.tasks`; the generic `task_*` tools collect/stop it |
| Add filesystem access or policy | implement a `ctx.fs` provider or listen on `fs/*` policy events |
| Confine spawned processes | a `ctx.sandbox` backend; consumers wrap their argv before spawning |
-| Intercept prompts, requests, tool use, or continuation | listen on the relevant `agent/*` or `tools/*` waterfall; use serial `agent/turn-stop` for a monotonic terminal stop |
+| Intercept prompts, requests, model completion/failure, tool use, or continuation | listen on the relevant `agent/*` or `tools/*` event; use serial `agent/turn-stop` for a monotonic terminal stop |
| Add a session-stable request prefix outside history | compose it on `agent/session-prefix`, once per loop instance; logged on the request header |
| Add UI or editor integration | drive `ctx.agents` and render from `session/event` |
| Add durable session state | add a `SessionEventMap` member and render/replay from the log |
@@ -171,4 +187,4 @@ The [extension cookbook](cookbook/extension-cookbook.md) carries plugin skeleton
- Exact event and service signatures in [events](cordis-catalog/events.md)
- [services](cordis-catalog/services.md) catalogs
- package contracts in the [package map](../packages/README.md)
-- [RFCs](rfc/README.md)
+- [Agent Notes](../.agents/notes/README.md)
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index 1dfcb8ac63..f109abe5b1 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -14,9 +14,14 @@ flowchart LR
pkg_llm_replay["llm-replay"]
pkg_agent_loop["agent-loop"]
pkg_compact_basic["compact-basic"]
+ pkg_token_meter["token-meter"]
+ svc_tokenMeter["ctx.tokenMeter Replay token measurement"]
+ pkg_compact_tool_result_prune["compact-tool-result-prune"]
+ svc_toolResultPrune["ctx.toolResultPrune Model-free tool-result pruning"]
pkg_session["session"]
svc_sessions["ctx.sessions In-memory session store"]
pkg_agent["agent"]
+ pkg_cli_demo["cli-demo"]
pkg_session_persistence["session-persistence"]
pkg_session_query["session-query"]
pkg_subagent_inprocess["subagent-inprocess"]
@@ -42,14 +47,15 @@ flowchart LR
pkg_tool_todo["tool-todo"]
pkg_user_interaction["user-interaction"]
svc_userInteraction["ctx.userInteraction Human question/answer seam"]
- pkg_stdio_demo["stdio-demo"]
+ pkg_tui["tui"]
pkg_mode["mode"]
svc_modes["ctx.modes Session-mode policy state"]
pkg_stdio_agent["stdio-agent"]
pkg_skill["skill"]
svc_skills["ctx.skills Skill provider registry"]
pkg_skill_local["skill-local"]
- svc_agents["ctx.agents Agent registry"]
+ svc_agents["ctx.agents Agent service"]
+ pkg_tui_demo["tui-demo"]
svc_agentLoop["ctx.agentLoop Concrete loop driver"]
pkg_agent_spine_demo["agent-spine-demo"]
pkg_bash["bash"]
@@ -60,6 +66,9 @@ flowchart LR
pkg_sandbox["sandbox"]
svc_sandbox["ctx.sandbox Process-sandbox seam"]
pkg_sandbox_local["sandbox-local"]
+ pkg_sandbox_policy["sandbox-policy"]
+ svc_sandboxPolicy["ctx.sandboxPolicy Sandbox policy home"]
+ pkg_fs_sandbox["fs-sandbox"]
pkg_approval["approval"]
svc_approval["ctx.approval Approval seam"]
pkg_permission["permission"]
@@ -78,7 +87,6 @@ flowchart LR
pkg_subagent_spawn["subagent-spawn"]
pkg_subagent_fork["subagent-fork"]
pkg_subagent_acp["subagent-acp"]
- pkg_subagent_mock["subagent-mock"]
pkg_tasks["tasks"]
svc_tasks["ctx.tasks Background task registry"]
pkg_tool_tasks["tool-tasks"]
@@ -108,8 +116,10 @@ flowchart LR
pkg_code_runtime_worker --> svc_codeRuntime
pkg_compact --> svc_compact
pkg_compact_basic --> svc_compact
+ pkg_compact_tool_result_prune --> svc_toolResultPrune
pkg_fs --> svc_fs
pkg_fs_local --> svc_fs
+ pkg_fs_sandbox --> svc_fs
pkg_llm --> svc_llm
pkg_llm_deepseek --> svc_llm
pkg_llm_pi_ai --> svc_llm
@@ -118,6 +128,7 @@ flowchart LR
pkg_permission --> svc_permission
pkg_sandbox --> svc_sandbox
pkg_sandbox_local --> svc_sandbox
+ pkg_sandbox_policy --> svc_sandboxPolicy
pkg_session --> svc_sessions
pkg_session_persistence --> svc_sessionPersistence
pkg_session_persistence_jsonl --> svc_sessionPersistence
@@ -127,16 +138,16 @@ flowchart LR
pkg_skill_local --> svc_skills
pkg_spill --> svc_spillStore
pkg_spill_local --> svc_spillStore
- pkg_stdio_demo --> svc_userInteraction
pkg_subagent --> svc_subagents
pkg_subagent_acp --> svc_subagents
pkg_subagent_fork --> svc_subagents
- pkg_subagent_mock --> svc_subagents
pkg_subagent_spawn --> svc_subagents
pkg_system_prompt --> svc_systemPrompt
pkg_tasks --> svc_tasks
+ pkg_token_meter --> svc_tokenMeter
pkg_tool_bash --> svc_bashEnv
pkg_tools --> svc_tools
+ pkg_tui --> svc_userInteraction
pkg_user_interaction --> svc_userInteraction
pkg_web --> svc_web
pkg_web_fetch_local --> svc_web
@@ -148,9 +159,10 @@ flowchart LR
svc_agentLoop --> pkg_agent_spine_demo
svc_agents --> pkg_acp
svc_agents --> pkg_agent_loop
+ svc_agents --> pkg_cli_demo
svc_agents --> pkg_invariants
- svc_agents --> pkg_stdio_demo
svc_agents --> pkg_subagent_inprocess
+ svc_agents --> pkg_tui_demo
svc_approval --> pkg_tool_bash
svc_approval --> pkg_tools
svc_bash --> pkg_hooks_claude
@@ -165,6 +177,8 @@ flowchart LR
svc_modes --> pkg_stdio_agent
svc_permission --> pkg_acp
svc_sandbox --> pkg_bash_sandbox
+ svc_sandboxPolicy --> pkg_bash_sandbox
+ svc_sandboxPolicy --> pkg_fs_sandbox
svc_sessionPersistence --> pkg_acp
svc_sessionPersistence --> pkg_agent_loop
svc_sessionPersistence --> pkg_hooks_claude
@@ -173,6 +187,7 @@ flowchart LR
svc_sessionPersistence --> pkg_tool_bash
svc_sessions --> pkg_agent
svc_sessions --> pkg_agent_loop
+ svc_sessions --> pkg_cli_demo
svc_sessions --> pkg_invariants
svc_sessions --> pkg_session_persistence
svc_sessions --> pkg_session_query
@@ -187,6 +202,8 @@ flowchart LR
svc_tasks --> pkg_tool_bash
svc_tasks --> pkg_tool_subagent
svc_tasks --> pkg_tool_tasks
+ svc_tokenMeter --> pkg_compact_basic
+ svc_toolResultPrune --> pkg_compact_basic
svc_tools --> pkg_acp
svc_tools --> pkg_agent_loop
svc_tools --> pkg_tool_ask_user
@@ -198,8 +215,8 @@ flowchart LR
svc_tools --> pkg_tool_todo
svc_tools --> pkg_tool_web
svc_userInteraction --> pkg_acp
- svc_userInteraction --> pkg_stdio_demo
svc_userInteraction --> pkg_tool_ask_user
+ svc_userInteraction --> pkg_tui
svc_web --> pkg_tool_web
svc_workflows --> pkg_tool_workflow
svc_fs -. event gate .-> pkg_fs_policy
@@ -208,25 +225,28 @@ flowchart LR
| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
| --- | --- | --- | --- | --- | --- | --- |
| `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
-| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
+| `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. |
+| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
+| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`cli-demo`](../packages/examples/cli-demo), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`acp`](../packages/ui/acp), [`session-query`](../packages/session-query/session-query) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | - | - | - | Resolves live and optional persisted logs into one logical corpus for exact reads and relationship traces. |
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
-| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
+| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`tui`](../packages/ui/tui), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`tui`](../packages/ui/tui), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.modes` | `core` | [`mode`](../packages/mode/mode) | - | `stdio-agent`, [`acp`](../packages/ui/acp) | - | Folds the logged per-agent mode (mode/set), flushes user flips at turn boundaries, and enforces the mode through the assemble filter and the tools/pre-execute gate. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
-| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
+| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tui-demo`](../packages/examples/tui-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. |
| `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. |
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
+| `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/bash/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. |
| `ctx.approval` | `seam` | `approval` | [`acp`](../packages/ui/acp) | [`tools`](../packages/core/tools), [`tool-bash`](../packages/bash/tool-bash) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
| `ctx.permission` | `core` | [`permission`](../packages/ui/permission) | - | [`acp`](../packages/ui/acp) | - | User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events. |
| `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). |
-| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate. |
-| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred. |
-| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-mock`](../packages/support/subagent-mock) | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. |
+| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. |
+| `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred. |
+| `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp) | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. |
| `ctx.tasks` | `core` | [`tasks`](../packages/tasks/tasks) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (tool-bash background commands, tool-subagent background delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it. |
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index b3a08d3066..0dfa62e77c 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -11,27 +11,29 @@ A `Requires:` line lists the service keys the plugin `inject`s: its `cordis.yml`
## `@deepseek-ai/dsh-acp`
-Requires: `agents` · `sessions` · `sessionPersistence` · `tools` · `userInteraction`
+Requires: `agents` · `sessionPersistence` · `tools` · `userInteraction` · `llm` · `systemPrompt`
```ts config-catalog
/** Plugin config: the agent template ACP sessions are created from. */
export interface AcpConfig {
+ /** Provider route for created agents. */
+ provider?: string
/** Model name for created agents (must have a registered adapter). */
model?: string
- /** Runtime-only transport override for tests; production uses stdio. */
+ /** Runtime-only transport override; production uses stdio. */
stream?: Stream
}
```
Depends on: `Stream` (`@agentclientprotocol/sdk`)
-Source: [`packages/ui/acp/src/index.ts:206`](../packages/ui/acp/src/index.ts)
+Source: [`packages/ui/acp/src/index.ts:213`](../packages/ui/acp/src/index.ts)
## `@deepseek-ai/dsh-acp-demo`
```ts config-catalog
/**
- * App config: the swappable per-deployment values. `model` configures the
+ * App config: the swappable per-deployment values. `provider` and `model` configure the
* agent template the ACP bridge creates each session's agent from (NOT a
* pre-created agent — ACP creates agents at `session/new`); `persona` is the
* deployment persona (forwarded to the system-prompt plugin); `toolOrder` is
@@ -40,8 +42,12 @@ Source: [`packages/ui/acp/src/index.ts:206`](../packages/ui/acp/src/index.ts)
* through agent-spine-demo); `persistenceRoot` is the JSONL backend's directory.
*/
export interface Config {
+ /** Provider route for ACP-created agents. */
+ provider: string
/** Model name for ACP-created agents (must have a registered adapter). */
model: string
+ /** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
+ maxParallelToolCalls?: number
/** Deployment persona (the system-prompt plugin's `persona` config). */
persona?: string
/** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
@@ -52,32 +58,43 @@ export interface Config {
dshHome?: string
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
persistenceRoot?: string
+ /** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
+ persistenceCompression?: JsonlCompression
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
/** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
skills?: agentCore.SkillConfig
/** Model-facing bash tool config forwarded through agent-core. */
toolBash?: NonNullable
- /** Generic background-task control-tool config forwarded through agent-core. */
+ /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
toolTasks?: NonNullable
+ /** Bounded transient model-request retry policy forwarded through agent-core. */
+ llmRetry?: NonNullable
}
```
-Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
+Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
-Source: [`packages/examples/acp-demo/src/index.ts:32`](../packages/examples/acp-demo/src/index.ts)
+Source: [`packages/examples/acp-demo/src/index.ts:36`](../packages/examples/acp-demo/src/index.ts)
## `@deepseek-ai/dsh-agent-loop`
Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt`
```ts config-catalog
-/** Plugin configuration for declarative startup agents. */
+/** Agent-loop plugin configuration. */
export interface Config {
+ /**
+ * Maximum parallel-safe calls in flight per agent step. `1` is serial;
+ * omission defaults to {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS}.
+ */
+ maxParallelToolCalls?: number
/** Agents created or resumed at plugin startup. */
agents: (AgentOptions & {
- /** Registry identity for the live agent. */
- id: AgentId
+ /** Stable config label used in logs and as the fresh combined-id prefix. */
+ id: string
+ /** Optional stable identity; remounts resume its materialized history, while first use creates it fresh. */
+ sessionId?: SessionId
/** Optional workspace for a fresh session. */
cwd?: string
/** Persisted session to resume instead of creating a fresh session. */
@@ -86,9 +103,9 @@ export interface Config {
}
```
-Depends on: [`AgentId`](../packages/core/agent/src/index.ts) · [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts)
+Depends on: [`AgentOptions`](core-data-structures/core.md) · [`SessionId`](core-data-structures/core.md)
-Source: [`packages/core/agent-loop/src/index.ts:322`](../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:360`](../packages/core/agent-loop/src/index.ts)
## `@deepseek-ai/dsh-agent-spine-demo`
@@ -111,6 +128,8 @@ Source: [`packages/core/agent-loop/src/index.ts:322`](../packages/core/agent-loo
export interface Config {
/** The agent-loop `agents` list (see dsh-agent-loop's `Config`). */
agents?: AgentLoopConfig['agents']
+ /** Agent-loop concurrency cap; `1` is serial. */
+ maxParallelToolCalls?: AgentLoopConfig['maxParallelToolCalls']
/** The deployment persona (see dsh-system-prompt's `Config`). */
persona?: SystemPromptConfig['persona']
/** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */
@@ -125,12 +144,16 @@ export interface Config {
skills?: SkillConfig
/** Model-facing bash tool config, including this producer's background opt-in. */
toolBash?: toolBash.Config
- /** Generic background-task control-tool wait bounds. */
- toolTasks?: toolTasks.Config
+ /** Generic background-task controls; set false to keep the task service without model-facing task tools. */
+ toolTasks?: toolTasks.Config | false
+ /** Bounded transient model-request retry policy. */
+ llmRetry?: llmRetry.Config
}
/** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */
export interface SkillConfig {
+ /** Mount the bundled local skill provider and model-facing skill tool (default true). */
+ enabled?: boolean
/** Registry-level discovery cache settings. */
registry?: SkillRegistryConfig
/** Local filesystem skill provider settings. */
@@ -140,9 +163,9 @@ export interface SkillConfig {
}
```
-Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts)
+Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`llmRetry`](../packages/llm/llm-retry/src/index.ts) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts)
-Source: [`packages/examples/agent-spine-demo/src/index.ts:57`](../packages/examples/agent-spine-demo/src/index.ts)
+Source: [`packages/examples/agent-spine-demo/src/index.ts:60`](../packages/examples/agent-spine-demo/src/index.ts)
## `@deepseek-ai/dsh-bash-local`
@@ -157,7 +180,9 @@ export interface Config {
maxTimeoutMs?: number
/** Per-stream in-memory output cap; overflow spills to a temp file. */
maxOutputBytes?: number
- /** Grace period between the SIGTERM and the SIGKILL escalation on a kill. */
+ /** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
+ maxSpillBytes?: number
+ /** Grace period for kill escalation and for inherited pipes after shell exit. */
graceMs?: number
}
```
@@ -166,30 +191,63 @@ Source: [`packages/bash/bash-local/src/index.ts:17`](../packages/bash/bash-local
## `@deepseek-ai/dsh-bash-sandbox`
-Requires: `sandbox`
+Requires: `sandbox` · `sandboxPolicy`
```ts config-catalog
/**
- * Plugin config: the local executor's knobs plus the sandbox policy. All
- * optional — `static Config` supplies the defaults (`mode: 'read-only'` is the
- * fail-safe default; an example that wants a workspace-writable agent opts in
- * explicitly). The runner choice is not configured here: which platform
- * backend confines the command is the `ctx.sandbox` provider's config.
+ * Plugin config: the local executor's knobs, verbatim. The sandbox policy —
+ * the default mode and the `workspace-write` boundary root — is NOT here: it
+ * lives on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), the one
+ * home both enforcing families read, so bash and fs can never confine to
+ * different roots. The runner choice is likewise the `ctx.sandbox` provider's
+ * config, not this executor's.
*/
-export interface Config extends LocalConfig {
- /** File-sandbox mode commands run under (default: `read-only`). */
- mode?: SandboxMode
- /**
- * Root directory `workspace-write` mode may write under (default: the
- * executor's default working directory — `cwd`, else `process.cwd()`).
- */
- workspaceRoot?: string
+export type Config = LocalConfig
+```
+
+Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
+
+Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts)
+
+## `@deepseek-ai/dsh-cli-demo`
+
+```ts config-catalog
+/** App config forwarded to the spine, configured agent, and JSONL backend. */
+export interface Config {
+ /** Provider route for the configured agent. */
+ provider: string
+ /** Model name for the configured agent; a matching adapter must be registered. */
+ model: string
+ /** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
+ maxParallelToolCalls?: number
+ /** Deployment persona forwarded to the system-prompt plugin. */
+ persona?: string
+ /** Explicit model-facing tool order forwarded to the system-prompt plugin. */
+ toolOrder?: string[]
+ /** Tool-registry presentation config forwarded through agent-spine-demo. */
+ tools?: ToolsConfig
+ /** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
+ dshHome?: string
+ /** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
+ persistenceRoot?: string
+ /** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
+ persistenceCompression?: JsonlCompression
+ /** Skill registry, local-provider, and model-facing consumer config. */
+ skills?: agentCore.SkillConfig
+ /** Model-facing bash tool config forwarded through agent-spine-demo. */
+ toolBash?: NonNullable
+ /** Generic background-task control-tool config forwarded through agent-spine-demo. */
+ toolTasks?: NonNullable
+ /** Bounded transient model-request retry policy forwarded through agent-spine-demo. */
+ llmRetry?: NonNullable
+ /** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
+ workspaceContext: agentCore.Config['workspaceContext']
}
```
-Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](core-data-structures/sandbox.md)
+Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
-Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts)
+Source: [`packages/examples/cli-demo/src/index.ts:25`](../packages/examples/cli-demo/src/index.ts)
## `@deepseek-ai/dsh-code-runtime-worker`
@@ -229,44 +287,47 @@ Source: [`packages/code-runtime/code-runtime-worker/src/index.ts:21`](../package
## `@deepseek-ai/dsh-compact-basic`
-Requires: `llm`
+Requires: `llm` · `tokenMeter`
```ts config-catalog
-/**
- * Backend configuration. Every knob is REQUIRED except `auto` and
- * `charsPerToken`: there is no concrete data yet to justify default
- * thresholds/budgets, so a consumer must state each value explicitly rather
- * than inherit a guessed default. `auto` alone defaults to `true`
- * (auto-compaction is the intended posture), and `charsPerToken` defaults to
- * the English-text heuristic its estimator was calibrated on.
- */
+/** Basic compaction configuration; every common field has a deployment default. */
export interface BasicCompactConfig {
- /** Context window size in tokens. */
- contextWindow: number
- /** Compact when estimated token usage exceeds this fraction of context window. */
- thresholdRatio: number
- /** Number of tokens of recent context to retain during compaction. */
- retainTokens: number
- /** Model to use for summarization (`''` — uses the agent's model). */
- summarizationModel: string
- /** Provider generation cap for the summarization call. */
- maxTokens: number
- /** Extra compaction attempts when the first compacted surface is still over threshold. */
- compactionRetries: number
- /** Enable automatic compaction on the `agent/pre-step` seam (default true). */
+ /** Compact at this fraction of the token meter's context window. Defaults to `0.8`. */
+ thresholdRatio?: number
+ /** Recent surface tokens retained verbatim. Defaults to `floor(contextWindow * 0.16)`. */
+ retainTokens?: number
+ /** Summary provider; `''` resolves the latest routed pair, then the agent pair. Defaults to `''`. */
+ summarizationProvider?: string
+ /** Summary model; `''` resolves the latest routed pair, then the agent pair. Defaults to `''`. */
+ summarizationModel?: string
+ /** Provider generation cap for summarization. Defaults to `8192`. */
+ maxTokens?: number
+ /** Extra attempts after the first compaction when pressure remains above threshold. Defaults to `1`. */
+ compactionRetries?: number
+ /** Maximum retries after canonical context overflow; `0` disables recovery. Defaults to `1`. */
+ maxOverflowRetries?: number
+ /** Enable automatic post-step pressure and overflow-recovery listeners. Defaults to `true`. */
auto?: boolean
- /**
- * Text density for the token estimator: estimated tokens = chars /
- * `charsPerToken`. Defaults to 4 (typical English text). A CJK-heavy
- * deployment should set ~1-2 — CJK runs at roughly 1-2 chars per token, so
- * the default UNDERestimates several-fold and compaction fires far too late.
- * May be fractional.
- */
- charsPerToken?: number
}
```
-Source: [`packages/compact/compact-basic/src/types.ts:20`](../packages/compact/compact-basic/src/types.ts)
+Source: [`packages/compact/compact-basic/src/types.ts:8`](../packages/compact/compact-basic/src/types.ts)
+
+## `@deepseek-ai/dsh-compact-tool-result-prune`
+
+```ts config-catalog
+/** Character-budget policy for deterministic tool-result pruning. */
+export interface ToolResultPruneConfig {
+ /** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */
+ thresholdChars?: number
+ /** Maximum leading Unicode code points retained. Defaults to `4096`. */
+ headChars?: number
+ /** Maximum trailing Unicode code points retained. Defaults to `1024`. */
+ tailChars?: number
+}
+```
+
+Source: [`packages/compact/compact-tool-result-prune/src/types.ts:4`](../packages/compact/compact-tool-result-prune/src/types.ts)
## `@deepseek-ai/dsh-fs-local`
@@ -280,6 +341,24 @@ export interface Config {
Source: [`packages/fs/fs-local/src/index.ts:38`](../packages/fs/fs-local/src/index.ts)
+## `@deepseek-ai/dsh-fs-sandbox`
+
+Requires: `sandboxPolicy`
+
+```ts config-catalog
+/**
+ * Plugin config: the local backend's knobs, verbatim (only `cwd`, the resolve
+ * base for relative paths). The sandbox default (mode + `workspace-write`
+ * boundary root) is NOT here — it lives on `ctx.sandboxPolicy`, the one home
+ * both enforcing families share.
+ */
+export type Config = LocalConfig
+```
+
+Depends on: [`LocalConfig`](#deepseek-aidsh-fs-local)
+
+Source: [`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src/index.ts)
+
## `@deepseek-ai/dsh-hooks-claude`
Requires: `bash`
@@ -346,8 +425,10 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:42`](../packages/hooks/hooks-c
Requires: `agents`
```ts config-catalog
-/** Runtime-only test seams; no field is configurable from `cordis.yml`. */
+/** JSON-RPC deployment config plus runtime-only test seams. */
export interface JsonRpcConfig {
+ /** Report max-token turn/subagent termination as a successful SDK result. */
+ maxTokensAsSuccess?: boolean
/** Transport input override; production uses `process.stdin`. */
input?: Readable
/** Transport output override; production uses `process.stdout`. */
@@ -377,47 +458,70 @@ export interface Config {
apiKey?: string
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL, then the public API. */
baseURL?: string
- /** Model names to register (sent verbatim on the wire). */
- models?: string[]
/** Thinking-mode default for every request (provider default: enabled). */
thinking?: 'enabled' | 'disabled'
/** Thinking effort (only meaningful with thinking enabled). */
reasoningEffort?: 'high' | 'max'
+ /** Advisory models shown by discovery consumers; defaults to V4 Flash and V4 Pro. */
+ models?: DeepSeekCatalogModel[]
+ /** Maximum provider idle time while one stream read is outstanding (default five minutes). */
+ streamIdleTimeoutMs?: number
+}
+
+/** One optional model entry advertised by the hand-written adapter. */
+export interface DeepSeekCatalogModel {
+ /** Wire model id accepted by the configured endpoint. */
+ id: string
+ /** Selector label; defaults to {@link id}. */
+ name?: string
+ /** Optional selector detail for deployments with similar model variants. */
+ description?: string
}
```
-Source: [`packages/llm/llm-deepseek/src/index.ts:30`](../packages/llm/llm-deepseek/src/index.ts)
+Source: [`packages/llm/llm-deepseek/src/index.ts:34`](../packages/llm/llm-deepseek/src/index.ts)
## `@deepseek-ai/dsh-llm-pi-ai`
Requires: `llm`
```ts config-catalog
-/**
- * Plugin config, validated by the same-named schemastery schema. Every field
- * is optional in yml: credentials/endpoint fall back to the environment (a
- * missing API key fails plugin load, not the first call).
- */
+/** Plugin configuration: the non-empty provider profiles this instance owns. */
export interface Config {
- /** API key; falls back to $DEEPSEEK_API_KEY. Required one way or the other. */
- apiKey?: string
- /** Endpoint base; falls back to $DEEPSEEK_BASE_URL, then the public API. */
- baseURL?: string
- /** Model names to register (sent verbatim on the wire). */
- models?: string[]
- /**
- * Thinking level for every request: 'off' disables thinking mode; 'high'
- * and 'xhigh' (wire 'max') set the effort. Omitted = provider default
- * (thinking enabled), matching llm-deepseek's omission semantics.
- */
- reasoning?: PiAiReasoning
+ /** Non-empty set of pi-ai provider routes this adapter instance owns. */
+ providers: PiAiProviderProfile[]
}
-/** Reasoning levels surfaced by this adapter (DeepSeek wire: high|max). */
-export type PiAiReasoning = 'off' | 'high' | 'xhigh'
+/** Configuration for one pi-ai provider route. */
+export interface PiAiProviderProfile {
+ /** pi-ai provider catalog name and Harness route key. */
+ provider: string
+ /** Provider credential; when absent pi-ai uses its provider-native ambient discovery. */
+ apiKey?: string
+ /** Override the selected catalog model's endpoint without changing its protocol metadata. */
+ baseURL?: string
+ /** Provider request headers; Harness attribution wins reserved names. */
+ headers?: Record
+ /** Provider-neutral pi-ai reasoning level. */
+ reasoning?: ThinkingLevel
+ /** Token budgets used by reasoning providers that support them. */
+ thinkingBudgets?: ThinkingBudgets
+ /** Prompt-cache retention preference. */
+ cacheRetention?: CacheRetention
+ /** Streaming transport preference. */
+ transport?: Transport
+ /** HTTP/provider SDK timeout in milliseconds. */
+ timeoutMs?: number
+ /** WebSocket connection timeout in milliseconds. */
+ websocketConnectTimeoutMs?: number
+ /** Maximum provider idle time while one stream read is outstanding. */
+ streamIdleTimeoutMs?: number
+}
```
-Source: [`packages/llm/llm-pi-ai/src/index.ts:37`](../packages/llm/llm-pi-ai/src/index.ts)
+Depends on: `CacheRetention` (`@earendil-works/pi-ai`) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `ThinkingLevel` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`)
+
+Source: [`packages/llm/llm-pi-ai/src/config.ts:48`](../packages/llm/llm-pi-ai/src/config.ts)
## `@deepseek-ai/dsh-llm-replay`
@@ -436,10 +540,54 @@ export interface Config {
* a nested-agent scenario; absent/empty for a single-session scenario.
*/
childFiles?: string[]
+ /** Optional replay-only provider catalog; absent or empty selects catch-all waterfall replay. */
+ providers?: ReplayProviderConfig[]
+}
+
+/** One provider route exposed by the replay adapter. */
+export interface ReplayProviderConfig {
+ /** Provider route used for replay requests. */
+ id: string
+ /** Selector label; defaults to {@link id}. */
+ name?: string
+ /** Advisory models exposed to clients such as ACP editors. */
+ models?: ReplayModelConfig[]
+}
+
+/** One model exposed by a replay-only provider catalog. */
+export interface ReplayModelConfig {
+ /** Model id used for replay requests. */
+ id: string
+ /** Selector label; defaults to {@link id}. */
+ name?: string
+ /** Optional selector description. */
+ description?: string
}
```
-Source: [`packages/support/llm-replay/src/index.ts:306`](../packages/support/llm-replay/src/index.ts)
+Source: [`packages/support/llm-replay/src/index.ts:375`](../packages/support/llm-replay/src/index.ts)
+
+## `@deepseek-ai/dsh-llm-retry`
+
+Requires: `agents`
+
+```ts config-catalog
+/** Deployment-owned limits and classification for transient request recovery. */
+export interface Config {
+ /** Maximum transient retries after the first request (default 2). */
+ maxTransientRetries?: number
+ /** Initial local exponential-backoff delay in milliseconds (default 500). */
+ initialDelayMs?: number
+ /** Maximum accepted or locally scheduled delay in milliseconds (default 10000). */
+ maxDelayMs?: number
+ /** Symmetric random multiplier range around one (default 0.1). */
+ jitterRatio?: number
+ /** Stable failure codes eligible for this policy. */
+ retryableCodes?: string[]
+}
+```
+
+Source: [`packages/llm/llm-retry/src/index.ts:39`](../packages/llm/llm-retry/src/index.ts)
## `@deepseek-ai/dsh-mcp-client`
@@ -538,7 +686,7 @@ export interface Config {
/** One preset's sandbox/approval bundle and optional client presentation. */
export interface PresetSpec {
- /** The `bash/sandbox-mode` value the preset writes through. */
+ /** The `sandbox/mode` value the preset writes through. */
sandbox: SandboxMode
/** The `approval/policy` value the preset writes through. */
approval: ApprovalPolicy
@@ -551,7 +699,7 @@ export interface PresetSpec {
Depends on: [`ApprovalPolicy`](core-data-structures/approval.md) · [`SandboxMode`](core-data-structures/sandbox.md)
-Source: [`packages/ui/permission/src/index.ts:80`](../packages/ui/permission/src/index.ts)
+Source: [`packages/ui/permission/src/index.ts:83`](../packages/ui/permission/src/index.ts)
## `@deepseek-ai/dsh-repeat-tool-guard`
@@ -583,7 +731,7 @@ export interface Config {
}
```
-Source: [`packages/guard/repeat-tool-guard/src/index.ts:27`](../packages/guard/repeat-tool-guard/src/index.ts)
+Source: [`packages/guard/repeat-tool-guard/src/index.ts:26`](../packages/guard/repeat-tool-guard/src/index.ts)
## `@deepseek-ai/dsh-sandbox-local`
@@ -613,6 +761,31 @@ export interface Config {
Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts)
+## `@deepseek-ai/dsh-sandbox-policy`
+
+```ts config-catalog
+/**
+ * Plugin config: the deployment's sandbox default. All optional — `Config`
+ * supplies the defaults (`mode: 'read-only'` is the fail-safe default; a
+ * deployment that wants a workspace-writable agent opts in explicitly). The
+ * runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor
+ * is any per-family knob: this is the one shared policy home.
+ */
+export interface Config {
+ /** File-sandbox mode a session starts from (default: `read-only`). */
+ mode?: SandboxMode
+ /**
+ * Absolute root directory `workspace-write` may write under (default:
+ * `process.cwd()`). Both enforcing families fence against this SAME root.
+ */
+ workspaceRoot?: string
+}
+```
+
+Depends on: [`SandboxMode`](core-data-structures/sandbox.md)
+
+Source: [`packages/sandbox/sandbox-policy/src/index.ts:44`](../packages/sandbox/sandbox-policy/src/index.ts)
+
## `@deepseek-ai/dsh-session-persistence-jsonl`
Requires: `sessions`
@@ -626,10 +799,15 @@ export interface Config {
* (bash calls, subprocesses). Sessions group under per-cwd subdirectories.
*/
root: string
+ /** Physical encoding; defaults to checksummed Zstandard frames. */
+ compression?: JsonlCompression
}
+
+/** Physical encoding selected for JSONL session artifacts. */
+export type JsonlCompression = 'zstd' | 'none'
```
-Source: [`packages/session-persistence/session-persistence-jsonl/src/index.ts:24`](../packages/session-persistence/session-persistence-jsonl/src/index.ts)
+Source: [`packages/session-persistence/session-persistence-jsonl/src/index.ts:36`](../packages/session-persistence/session-persistence-jsonl/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-sqlite`
@@ -640,8 +818,12 @@ Requires: `sessions`
export interface Config {
/**
* Filesystem path to the SQLite database file. The special value `:memory:`
- * opens an in-process database (tests); a file path is created (with parent
- * dirs) on construction.
+ * opens an in-process database (tests). On filesystems with POSIX modes,
+ * missing directories and databases are created owner-only; existing path
+ * modes are preserved. Filesystem setup errors other than an existing database
+ * fail initialization. The backend does not protect confidentiality or
+ * integrity when another principal can replace the database entry in its
+ * parent directory.
*/
path: string
/**
@@ -664,7 +846,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:39`](../packages/session-persistence/session-persistence-sqlite/src/index.ts)
+Source: [`packages/session-persistence/session-persistence-sqlite/src/index.ts:55`](../packages/session-persistence/session-persistence-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-query`
@@ -744,71 +926,6 @@ export interface Config {
Source: [`packages/spill/spill-policy/src/index.ts:45`](../packages/spill/spill-policy/src/index.ts)
-## `@deepseek-ai/dsh-stdio`
-
-Requires: `agents` · `userInteraction`
-
-```ts config-catalog
-/** Serializable plugin configuration (cordis-native, schemastery). */
-export interface Config {
- /** Banner printed once on start, before the first `> ` prompt. */
- welcome?: string
- /** Id of the agent stdin drives (`send`/`steer`) and whose status gates the EOF exit; rendering is global. Defaults to `'main'`. */
- agent?: string
-}
-```
-
-Source: [`packages/ui/stdio/src/index.ts:33`](../packages/ui/stdio/src/index.ts)
-
-## `@deepseek-ai/dsh-stdio-demo`
-
-```ts config-catalog
-/**
- * App config: the swappable per-demo values, each routed to where the app wires
- * it. `model`/`resumeSessionId` configure the pre-created `main` agent (through
- * {@link @deepseek-ai/dsh-agent-spine-demo}'s forwarded `agents` list); `persona` is
- * the deployment persona (forwarded to the system-prompt plugin); `toolOrder`
- * is the explicit model-facing tool order (forwarded to the system-prompt plugin);
- * fresh sessions use `process.cwd()` as their workspace cwd; resumed sessions
- * keep their persisted cwd. `persistenceRoot` is the JSONL backend's directory;
- * `welcome` is the UI banner.
- */
-export interface Config {
- /** Model name for the `main` agent (must have a registered adapter). */
- model: string
- /** Deployment persona (the system-prompt plugin's `persona` config). */
- persona?: string
- /** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
- toolOrder?: string[]
- /** Tool-registry config — its presentation `mode` (forwarded through agent-spine-demo; see dsh-tools). */
- tools?: ToolsConfig
- /** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
- dshHome?: string
- /** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
- persistenceRoot?: string
- /** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
- welcome?: string
- /** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
- skills?: agentCore.SkillConfig
- /** Model-facing bash tool config forwarded through agent-core. */
- toolBash?: NonNullable
- /** Generic background-task control-tool config forwarded through agent-core. */
- toolTasks?: NonNullable
- /**
- * If set, the `main` agent RESUMES this persisted session id instead of
- * starting fresh. Sourced from an env var in the leaf `cordis.yml`
- * (`resumeSessionId: !!js process.env.RESUME_SESSION_ID`).
- */
- resumeSessionId?: string
- /** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
- workspaceContext: agentCore.Config['workspaceContext']
-}
-```
-
-Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools)
-
-Source: [`packages/examples/stdio-demo/src/index.ts:37`](../packages/examples/stdio-demo/src/index.ts)
-
## `@deepseek-ai/dsh-subagent-acp`
Requires: `subagents`
@@ -870,41 +987,6 @@ export interface Config {
Source: [`packages/subagent/subagent-fork/src/index.ts:25`](../packages/subagent/subagent-fork/src/index.ts)
-## `@deepseek-ai/dsh-subagent-mock`
-
-Requires: `subagents`
-
-```ts config-catalog
-/** Config for the mock provider; all optional with test-friendly defaults. */
-export interface Config {
- /** Registry name to register under. */
- name: string
- /** The text the scripted child "returns" as its final answer. */
- reply?: string
- /** The stop reason the run settles with. */
- stopReason?: SubagentStopReason
- /** Which start-time capabilities to advertise (default: all `true`). */
- capabilities?: Partial
- /**
- * The conversation-history descriptor to declare
- * ({@link SubagentProvider.inheritsParentContext}); default `false` (fresh
- * conversation). Set `true` to exercise seeded/fork wording in consumer
- * tests. This flag says nothing about tool, service, scope, or authority
- * inheritance.
- */
- inheritsParentContext?: boolean
- /**
- * Structured value surfaced when a request carries an `outputSchema` and the
- * `outputSchema` capability is on (default: `{ reply }`).
- */
- structured?: unknown
-}
-```
-
-Depends on: [`SubagentCapabilities`](../packages/subagent/subagent/src/index.ts) · [`SubagentStopReason`](../packages/subagent/subagent/src/index.ts)
-
-Source: [`packages/support/subagent-mock/src/index.ts:86`](../packages/support/subagent-mock/src/index.ts)
-
## `@deepseek-ai/dsh-subagent-spawn`
Requires: `subagents`
@@ -954,7 +1036,19 @@ export interface Config {
}
```
-Source: [`packages/context/time-context/src/index.ts:20`](../packages/context/time-context/src/index.ts)
+Source: [`packages/context/time-context/src/index.ts:19`](../packages/context/time-context/src/index.ts)
+
+## `@deepseek-ai/dsh-token-meter`
+
+```ts config-catalog
+/** Token-meter plugin configuration. */
+export interface TokenMeterConfig {
+ /** Service-wide context-window capacity in tokens. Defaults to `128000`. */
+ contextWindow?: number
+}
+```
+
+Source: [`packages/llm/token-meter/src/types.ts:10`](../packages/llm/token-meter/src/types.ts)
## `@deepseek-ai/dsh-tool-bash`
@@ -970,7 +1064,7 @@ export interface Config {
}
```
-Source: [`packages/bash/tool-bash/src/index.ts:39`](../packages/bash/tool-bash/src/index.ts)
+Source: [`packages/bash/tool-bash/src/index.ts:40`](../packages/bash/tool-bash/src/index.ts)
## `@deepseek-ai/dsh-tool-cordis`
@@ -982,7 +1076,7 @@ export interface Config {
/**
* Milliseconds the SYNCHRONOUS portion of mount code may run in the vm
* before evaluation is aborted (default 5000). An async body escapes this
- * bound — see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md for the trust stance.
+ * bound — see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md for the trust stance.
*/
vmTimeoutMs?: number
}
@@ -1008,7 +1102,7 @@ export interface Config {
}
```
-Source: [`packages/fs/tool-fs/src/index.ts:22`](../packages/fs/tool-fs/src/index.ts)
+Source: [`packages/fs/tool-fs/src/index.ts:24`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-fs-search`
@@ -1030,7 +1124,7 @@ export interface Config {
}
```
-Source: [`packages/fs/tool-fs-search/src/index.ts:59`](../packages/fs/tool-fs-search/src/index.ts)
+Source: [`packages/fs/tool-fs-search/src/index.ts:62`](../packages/fs/tool-fs-search/src/index.ts)
## `@deepseek-ai/dsh-tool-skill`
@@ -1077,8 +1171,7 @@ export interface Config {
/**
* Tool filter applied to every child. Filtered tools disappear from its
* prompt and reject execution. Requires the provider's `toolFilter`
- * capability; unknown names fail startup. Children otherwise see this tool,
- * so deny it or set `maxDepth` to bound recursion.
+ * capability; unknown names fail startup.
*/
toolFilter?: {
/** Global tool names the child keeps; everything else is removed. */
@@ -1087,14 +1180,19 @@ export interface Config {
deny?: string[]
}
/**
- * Maximum child depth. Requires the provider's `depthLimit` capability and a
- * non-negative safe integer. Omission is unbounded.
+ * Maximum child depth: a non-negative safe integer (default `3`; `0` forbids
+ * delegation entirely), or `'provider-managed'` to send no cap. A numeric cap
+ * requires the provider's `depthLimit` capability (mount fails loud
+ * otherwise). The provider checks the calling agent's current depth at every
+ * start; the tool remains model-visible so runtime policy owns rejection.
+ * `'provider-managed'` is for an out-of-process provider (ACP) whose
+ * recursion budget belongs to the child harness's own deployment.
*/
- maxDepth?: number
+ maxDepth?: number | 'provider-managed'
}
```
-Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts)
+Depends on: [`AgentOptions`](core-data-structures/core.md)
Source: [`packages/subagent/tool-subagent/src/index.ts:23`](../packages/subagent/tool-subagent/src/index.ts)
@@ -1172,7 +1270,87 @@ export interface Config {
export type ToolPresentationMode = 'native' | 'code' | 'both'
```
-Source: [`packages/core/tools/src/index.ts:322`](../packages/core/tools/src/index.ts)
+Source: [`packages/core/tools/src/index.ts:382`](../packages/core/tools/src/index.ts)
+
+## `@deepseek-ai/dsh-tui`
+
+Requires: `agents` · `userInteraction` · `tools`
+
+```ts config-catalog
+/** Serializable plugin configuration. */
+export interface Config extends TuiConfig {
+ /** Header subtitle. Defaults to `ready.`. */
+ welcome?: string
+ /** Exact shared agent/session identity driven by this terminal. Defaults to `main`. */
+ sessionId?: string
+}
+
+/** Presentation settings for the pi-tui terminal mode. */
+export interface TuiConfig {
+ /** Render model reasoning blocks. */
+ showReasoning?: boolean
+ /** Maximum tool-output lines shown before the card is collapsed. */
+ maxToolOutputLines?: number
+ /** Maximum options visible at once in a user-question dialog. */
+ maxQuestionOptions?: number
+ /** User-question dialog width in terminal columns. */
+ questionDialogWidth?: number
+ /** User-question dialog maximum height in terminal rows. */
+ questionDialogMaxHeight?: number
+ /** Show the terminal's hardware cursor at the pi editor's IME marker. */
+ showHardwareCursor?: boolean
+ /** Apply the built-in ANSI color palette. */
+ color?: boolean
+ /** Terminal window title while the UI is mounted. */
+ title?: string
+}
+```
+
+Source: [`packages/ui/tui/src/index.ts:102`](../packages/ui/tui/src/index.ts)
+
+## `@deepseek-ai/dsh-tui-demo`
+
+```ts config-catalog
+/** App config routed to the spine, TUI, configured agent, and JSONL backend. */
+export interface Config {
+ /** Provider route for the `main` agent. */
+ provider: string
+ /** Model name for the `main` agent; a matching adapter must be registered. */
+ model: string
+ /** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
+ maxParallelToolCalls?: number
+ /** Deployment persona forwarded to the system-prompt plugin. */
+ persona?: string
+ /** Explicit model-facing tool order forwarded to the system-prompt plugin. */
+ toolOrder?: string[]
+ /** Tool-registry presentation config forwarded through agent-spine-demo. */
+ tools?: ToolsConfig
+ /** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
+ dshHome?: string
+ /** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
+ persistenceRoot?: string
+ /** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
+ persistenceCompression?: JsonlCompression
+ /** TUI subtitle rendered on start. Defaults to `ready.`. */
+ welcome?: string
+ /** Full-screen TUI presentation settings. */
+ ui?: uiTui.TuiConfig
+ /** Skill registry, local-provider, and model-facing consumer config. */
+ skills?: agentCore.SkillConfig
+ /** Model-facing bash tool config forwarded through agent-spine-demo. */
+ toolBash?: NonNullable
+ /** Generic background-task controls forwarded through agent-spine-demo; set false to omit them. */
+ toolTasks?: NonNullable
+ /** Persisted session id to resume instead of creating a fresh session. */
+ resumeSessionId?: string
+ /** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
+ workspaceContext: agentCore.Config['workspaceContext']
+}
+```
+
+Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · [`JsonlCompression`](../packages/session-persistence/session-persistence-jsonl/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`uiTui`](../packages/ui/tui/src/index.ts)
+
+Source: [`packages/examples/tui-demo/src/index.ts:31`](../packages/examples/tui-demo/src/index.ts)
## `@deepseek-ai/dsh-user-approval`
@@ -1382,7 +1560,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
## Seam packages (not directly loadable)
-Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](rfc/implemented/architecture/2026-06-13-capability-seams.md)).
+Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).
- `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts))
- `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts))
@@ -1413,4 +1591,5 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
- `@deepseek-ai/dsh-scripts` ([`packages/sdk/scripts/src/index.ts`](../packages/sdk/scripts/src/index.ts))
- `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts))
- `@deepseek-ai/dsh-subagent-subprocess` ([`packages/subagent/subagent-subprocess/src/index.ts`](../packages/subagent/subagent-subprocess/src/index.ts))
+- `@deepseek-ai/dsh-telemetry` ([`packages/sdk/telemetry/src/index.ts`](../packages/sdk/telemetry/src/index.ts))
- `@deepseek-ai/dsh-timeout` ([`packages/util/timeout/src/index.ts`](../packages/util/timeout/src/index.ts))
diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml
index 27c31ba1ef..6fd3feebbd 100644
--- a/docs/cookbook/adding-a-package.i18n.yaml
+++ b/docs/cookbook/adding-a-package.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-adding-a-package.md: 2930cee9ab64b382f6211335ae639bce45629d1d
-adding-a-package.zh.md: 10e906c320203c3658c103fc8936540f72697d65
+adding-a-package.md: 556a48493af4452c178634c0abb4e23e2419dd8e
+adding-a-package.zh.md: 5f7e4692233448c746d25e4808c078c390cf39e6
diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md
index 2930cee9ab..556a48493a 100644
--- a/docs/cookbook/adding-a-package.md
+++ b/docs/cookbook/adding-a-package.md
@@ -16,7 +16,7 @@ packages///
src/index.ts # service default export or plugin (name/inject/apply/Config)
tests/.spec.ts
README.md # service API, events, extension points, design notes,
- # + gated Model Experience context blocks or short sentence
+ # + gated Model Experience context blocks or short form
# + the gated "Known Limitations and Deferred Work" section
# (or a whitelist entry in scripts/verify-package-readme-limitations.ts)
```
@@ -44,31 +44,39 @@ For a swappable capability, split interface / implementation / consumer into sep
## 4. Write the package README
-Keep package-specific service API, config, events, extension points, and design notes first. The limitations section records durable consumer gaps and non-obvious maintainer constraints owned by this package; ordinary cleanup stays in its source TODO or RFC. An indirect Model Experience sentence may name the consumer that surfaces this package's contribution, but it does not restate that consumer's implementation. End a package README with this canonical sequence:
+Keep package-specific service API, config, events, extension points, and design notes first. The limitations section records durable consumer gaps and non-obvious maintainer constraints owned by this package; ordinary cleanup stays in its source TODO or Agent Note. An indirect Model Experience sentence may name the consumer that surfaces this package's contribution, but it does not restate that consumer's implementation. End a package README with this canonical sequence:
````markdown
## Model Experience
### Request surface and condition
-**What the model sees**: An exact data-dependent shape, an anchored generated-catalog link, or an introduction to the verbatim literal below.
+#### What the model sees
-**Token effect**: Fixed, conditional, retained, replaced, capped, or zero-direct token effect.
+An exact data-dependent shape, an anchored generated-catalog link, or an introduction to the verbatim literal below.
-#### Verbatim text for this context surface, when needed
+##### Verbatim text for this field, when needed
```markdown
Stable system-prompt prose of any length, or another long non-generated literal, copied exactly from source.
```
+#### Token effect
+
+Fixed, conditional, retained, replaced, capped, or zero-direct token effect.
+
+#### KV Cache effect
+
+Append-only, prefix-stable, replacing, or independent behavior, including the exact conditions that may invalidate reuse.
+
## Known Limitations and Deferred Work
- **Consumer-visible gap** — exact boundary, consequence, or maintainer constraint.
````
-Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary-model surface, with the two fields shown above. Quote stable text owned by the package: system-prompt prose goes in a titled H4 plus `markdown` fence, other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema surface links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema surfaces separate when scoping can hide one without the other. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the mechanical shape.
+Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary-model surface, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema surface links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema surfaces separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the mechanical shape.
-A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts); a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) is independent. The [Model Experience RFC](../rfc/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale.
+A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts), followed by a `KV Cache effect` H4 and one non-empty paragraph; a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) is independent. The [Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale.
## 5. Verify
diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md
index 10e906c320..5f7e469223 100644
--- a/docs/cookbook/adding-a-package.zh.md
+++ b/docs/cookbook/adding-a-package.zh.md
@@ -16,7 +16,7 @@ packages///
src/index.ts # service default export or plugin (name/inject/apply/Config)
tests/.spec.ts
README.md # service API, events, extension points, design notes,
- # + gated Model Experience context blocks or short sentence
+ # + gated Model Experience context blocks or short form
# + the gated "Known Limitations and Deferred Work" section
# (or a whitelist entry in scripts/verify-package-readme-limitations.ts)
```
@@ -44,31 +44,39 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
## 4. 编写包 README
-将包特有的服务 API、配置、事件、扩展点和设计说明放在前面。limitations 部分记录持久的消费方缺口和本包拥有的非显而易见的维护者约束;日常清理事项留在源码 TODO 或 RFC 中。间接的 Model Experience 语句可以点名暴露本包贡献的消费方,但不重述该消费方的实现。包 README 以如下规范序列结尾:
+将包特有的服务 API、配置、事件、扩展点和设计说明放在前面。limitations 部分记录持久的消费方缺口和本包拥有的非显而易见的维护者约束;日常清理事项留在源码 TODO 或 Agent Note 中。间接的 Model Experience 语句可以点名暴露本包贡献的消费方,但不重述该消费方的实现。包 README 以如下规范序列结尾:
````markdown
## Model Experience
### Request surface and condition
-**What the model sees**: An exact data-dependent shape, an anchored generated-catalog link, or an introduction to the verbatim literal below.
+#### What the model sees
-**Token effect**: Fixed, conditional, retained, replaced, capped, or zero-direct token effect.
+An exact data-dependent shape, an anchored generated-catalog link, or an introduction to the verbatim literal below.
-#### Verbatim text for this context surface, when needed
+##### Verbatim text for this field, when needed
```markdown
Stable system-prompt prose of any length, or another long non-generated literal, copied exactly from source.
```
+#### Token effect
+
+Fixed, conditional, retained, replaced, capped, or zero-direct token effect.
+
+#### KV Cache effect
+
+Append-only, prefix-stable, replacing, or independent behavior, including the exact conditions that may invalidate reuse.
+
## Known Limitations and Deferred Work
- **Consumer-visible gap** — exact boundary, consequence, or maintainer constraint.
````
-根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助模型的 surface 使用一个 H3,包含上述两个字段。引用包拥有的稳定文本:系统提示词放在带标题的 H4 加 `markdown` 围栏中,其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。tool-schema surface 链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行机械形状。
+根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助模型的 surface 使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。tool-schema surface 链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包契约。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行机械形状。
-没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience RFC](../rfc/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。
+没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句,随后添加 `KV Cache effect` H4 和一个非空正文段落;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。
## 5. 验证
diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml
index 6ec964c335..070cc45f93 100644
--- a/docs/cookbook/adding-a-tool.i18n.yaml
+++ b/docs/cookbook/adding-a-tool.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-adding-a-tool.md: da214702939e01fedf3d0d69be7560bbafe0696a
-adding-a-tool.zh.md: b216d18b1593cd7e6074685bd39684f1b9694eac
+adding-a-tool.md: a45315dc0ec92ab28963c2aca32dffcf5f778dcd
+adding-a-tool.zh.md: f574957ddd0e42cedc93ddc0f3270110a8f110c5
diff --git a/docs/cookbook/adding-a-tool.md b/docs/cookbook/adding-a-tool.md
index da21470293..a45315dc0e 100644
--- a/docs/cookbook/adding-a-tool.md
+++ b/docs/cookbook/adding-a-tool.md
@@ -2,7 +2,7 @@
English | [中文](adding-a-tool.zh.md)
-How to give the model a new capability. Reference implementations: `examples/echo-agent/src/echo-tool.ts` (minimal) and `packages/bash/tool-bash` (production-grade, three-package seam).
+How to give the model a new capability. The minimal shape below shows the contract; `packages/bash/tool-bash` is the production-grade three-package seam.
## The minimal shape
@@ -35,7 +35,7 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool (w
## Rules of the execute() contract
-- **Args are validated for you.** `defineTool` validates the model-generated `arguments` against the `SchemaSpec` before `execute` runs (type, required keys, enum membership, nested objects/arrays — [runtime arg validation](../rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md)), so inside `execute` the args already match `InferArgs`. You still hand-check value constraints the DSL can't express (non-empty strings, positive numbers, cross-field rules); throw a descriptive Error for those. Raw JSON-Schema tools registered directly (MCP) are NOT validated by the harness — they validate their own input.
+- **Args are validated for you.** `defineTool` validates the model-generated `arguments` against the `SchemaSpec` before `execute` runs (type, required keys, enum membership, nested objects/arrays — [runtime arg validation](../../.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md)), so inside `execute` the args already match `InferArgs`. You still hand-check value constraints the DSL can't express (non-empty strings, positive numbers, cross-field rules); throw a descriptive Error for those. Raw JSON-Schema tools registered directly (MCP) are NOT validated by the harness — they validate their own input.
- **Registration borrows your readonly definition.** A typed same-process contribution is not a serialization boundary; do not mutate its schema or replace callbacks after registration. `schemas()` materializes only the explicit model-facing projection. To hot-swap a tool, dispose its owning effect and register the replacement; mutable state inside the callback's closure remains ordinary plugin state.
- **Execution identity is protected.** The registry materializes `arguments` as detached lossless JSON in one recursive pass, freezes that value before policy starts, and assigns an opaque `exec.token`; `callId`, `name`, `arguments`, `agent`, `token`, and an optional enclosing-transport `parent` token stay immutable through dispatch. `parent` is identity-only and exposes no live outer execution. Treat `args` as readonly input. An around-dispatch wrapper may add, replace, or remove only `exec.signal` to impose cancellation or a deadline.
- **Throwing or returning non-JSON data means `isError`.** The registry catches throws and materializes the final result before observers run. A malformed or non-JSON result becomes `{ isError: true }`, preventing a live success that cannot be logged. Throw for infrastructure failures; report domain failures in result text when the model must interpret them.
@@ -47,11 +47,11 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool (w
Gate `run_in_background` with producer config, reject a pre-aborted call, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The runtime validates ownership and control-surface availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup.
-The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. Once the id is returned, use a task-owned cancellation signal rather than `exec.signal`. See the [background task runtime RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer.
+The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. Once the id is returned, use a task-owned cancellation signal rather than `exec.signal`. See the [background task runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer.
## Execution policy and observation
-Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](./extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap core dispatch with a deadline/retry/metrics scope, `tools/post-execute` to transform or attach model-facing context, and `tools/result` to observe the immutable normalized outcome without changing it. A sandboxing implementation can also sit behind the tool's executor capability seam; the exact contracts are in the [`dsh-tools` README](../../packages/core/tools/README.md#extension-points).
+Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap core dispatch with a deadline/retry/metrics scope, `tools/post-execute` to transform or attach model-facing context, and `tools/result` to observe the immutable normalized outcome without changing it. A sandboxing implementation can also sit behind the tool's executor capability seam; the exact contracts are in the [`dsh-tools` README](../../packages/core/tools/README.md#extension-points).
## Code Mode reaches your tool for free
@@ -78,8 +78,8 @@ Hard rules (they bite if broken):
- **UI-only formatting stays out of the model result.** A fenced ` ```console ` block, a diff, a relativized path — none of these may appear in what `execute` returns to the model; they live only in the presentation. (A `terminal` result view carries RAW `output`; the bridge adds the fences.)
- **`defineTool` soft-validates the display path.** A malformed/older logged arg shape makes the wrapper return `undefined` (a generic fallback) rather than throw — display must never crash a replay.
-The neutral vocabulary lives in `dsh-tools` (never import an ACP type into a tool); the ACP bridge maps each `card` to the wire. The design and the why are in [the render-intent-union RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md); `dsh-tool-fs` (generic/diff) and `dsh-tool-bash` (terminal) are the reference implementations.
+The neutral vocabulary lives in `dsh-tools` (never import an ACP type into a tool); the ACP bridge maps each `card` to the wire. The design and the why are in [the render-intent-union Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md); `dsh-tool-fs` (generic/diff) and `dsh-tool-bash` (terminal) are the reference implementations.
## Tests every tool needs
-Cover argument rejection, every result shape, and HMR disposal. For a side-effecting tool, drive the real tool through the agent loop with a scripted `MockAdapter` and assert its `tool/call` and `tool/result` session events. For an editor card, assert the exact `presentCall` and `presentResult` views and add an [ACP snapshot](../rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md) through the real bridge; a terminal card's scenario sets `terminalOutput: true` to exercise the capable-client path.
+Cover argument rejection, every result shape, and HMR disposal. For a side-effecting tool, drive the real tool through the agent loop with a scripted `MockAdapter` and assert its `tool/call` and `tool/result` session events. For an editor card, assert the exact `presentCall` and `presentResult` views and add an [ACP snapshot](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) through the real bridge; a terminal card's scenario sets `terminalOutput: true` to exercise the capable-client path.
diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md
index b216d18b15..f574957ddd 100644
--- a/docs/cookbook/adding-a-tool.zh.md
+++ b/docs/cookbook/adding-a-tool.zh.md
@@ -2,7 +2,7 @@
[English](adding-a-tool.md) | 中文
-如何为模型赋予一项新能力。参考实现:`examples/echo-agent/src/echo-tool.ts`(最小化)和 `packages/bash/tool-bash`(生产级,由三个包(package)构成的 seam)。
+如何为模型赋予一项新能力。下文的最小形态展示这项契约;`packages/bash/tool-bash` 是生产级、由三个包(package)构成的 seam。
## 最小形态
@@ -35,7 +35,7 @@ export function apply(ctx: Context) {
## execute() 契约的规则
-- **参数已为你校验。** `defineTool` 在 `execute` 运行前,会根据 `SchemaSpec` 校验模型生成的 `arguments`(类型、必填键、枚举成员、嵌套对象/数组——见[运行时参数校验](../rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md)),因此 `execute` 内部的 args 已匹配 `InferArgs`。你仍需手动检查 DSL 无法表达的值约束(非空字符串、正数、跨字段规则),对这些情况抛出描述性 Error。直接注册的原始 JSON-Schema 工具(MCP)不由 harness 校验,它们自行校验输入。
+- **参数已为你校验。** `defineTool` 在 `execute` 运行前,会根据 `SchemaSpec` 校验模型生成的 `arguments`(类型、必填键、枚举成员、嵌套对象/数组——见[运行时参数校验](../../.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md)),因此 `execute` 内部的 args 已匹配 `InferArgs`。你仍需手动检查 DSL 无法表达的值约束(非空字符串、正数、跨字段规则),对这些情况抛出描述性 Error。直接注册的原始 JSON-Schema 工具(MCP)不由 harness 校验,它们自行校验输入。
- **注册借用你的只读定义。** 类型化的同进程贡献不是序列化边界;注册后不要修改其 schema 或替换回调。`schemas()` 只物化显式的模型可见投影。如需热替换工具,请 dispose 其所属副作用并注册替代品;回调闭包内的可变状态仍是普通的插件状态。
- **执行身份受保护。** 注册表在一次递归遍历中将 `arguments` 物化为分离的无损 JSON,在策略开始前冻结该值,并分配一个不透明的 `exec.token`;`callId`、`name`、`arguments`、`agent`、`token` 以及可选的外层传输 `parent` token 在整个分发过程中保持不可变。`parent` 仅用于身份标识,不暴露活跃的外层执行。请将 `args` 视为只读输入。around-dispatch 包装器只能添加、替换或移除 `exec.signal`,以施加取消或截止时间。
- **抛出异常或返回非 JSON 数据意味着 `isError`。** 注册表捕获异常,并在观察者运行前物化最终结果。格式错误或非 JSON 的结果变为 `{ isError: true }`,防止出现无法记录的活跃成功。基础设施故障请抛异常;当模型需要解读领域失败时,请在结果文本中报告。
@@ -47,11 +47,11 @@ export function apply(ctx: Context) {
通过 producer 配置控制 `run_in_background`,拒绝已预先中止的调用,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。运行时会在 `run()` 启动工作前校验 owner 和控制面是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。
-producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。返回 id 后,应使用 task 自有的取消信号,而不是 `exec.signal`。流式 producer 的示例和完整契约见[后台 task 运行时 RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。
+producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。返回 id 后,应使用 task 自有的取消信号,而不是 `exec.signal`。流式 producer 的示例和完整契约见[后台 task 运行时 Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。
## 执行策略与观测
-尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](./extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝(后续监听器无法撤销);使用 `tools/execute` 为核心分发包装截止时间/重试/指标作用域;使用 `tools/post-execute` 转换或附加模型可见的上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。沙箱实现也可以位于工具执行器的能力 seam 之后;确切契约见 [`dsh-tools` README](../../packages/core/tools/README.md#extension-points)。
+尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝(后续监听器无法撤销);使用 `tools/execute` 为核心分发包装截止时间/重试/指标作用域;使用 `tools/post-execute` 转换或附加模型可见的上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。沙箱实现也可以位于工具执行器的能力 seam 之后;确切契约见 [`dsh-tools` README](../../packages/core/tools/README.md#extension-points)。
## Code Mode 自动触达你的工具
@@ -78,8 +78,8 @@ producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的
- **UI 格式不进入模型结果。** 围栏 ` ```console ` 块、diff、相对化路径——这些都不得出现在 `execute` 返回给模型的内容中;它们只存在于展示层。(`terminal` 结果视图携带原始 `output`;桥接层添加围栏。)
- **`defineTool` 对展示路径做软校验。** 格式错误或旧版日志中的 arg 形态会使包装器返回 `undefined`(通用回退)而非抛异常——展示绝不能导致回放崩溃。
-中性词汇定义在 `dsh-tools` 中(绝不在工具中导入 ACP 类型);ACP 桥接层将每个 `card` 映射到协议格式(wire format)。设计与原因见[渲染意图联合体 RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。
+中性词汇定义在 `dsh-tools` 中(绝不在工具中导入 ACP 类型);ACP 桥接层将每个 `card` 映射到协议格式(wire format)。设计与原因见[渲染意图联合体 Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。
## 每个工具必须的测试
-覆盖参数拒绝、每种结果形态和 HMR dispose。对于有副作用的工具,使用脚本化的 `MockAdapter` 驱动真实工具通过 agent loop(智能体循环),并断言其 `tool/call` 和 `tool/result` 会话事件。对于编辑器卡片,断言 `presentCall` 和 `presentResult` 的精确视图,并通过真实桥接层添加一个 [ACP 快照](../rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md);终端卡片的场景设置 `terminalOutput: true` 以覆盖 capable-client 路径。
+覆盖参数拒绝、每种结果形态和 HMR dispose。对于有副作用的工具,使用脚本化的 `MockAdapter` 驱动真实工具通过 agent loop(智能体循环),并断言其 `tool/call` 和 `tool/result` 会话事件。对于编辑器卡片,断言 `presentCall` 和 `presentResult` 的精确视图,并通过真实桥接层添加一个 [ACP 快照](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md);终端卡片的场景设置 `terminalOutput: true` 以覆盖 capable-client 路径。
diff --git a/docs/cookbook/adding-a-vendored-package.i18n.yaml b/docs/cookbook/adding-a-vendored-package.i18n.yaml
index 0a359ead84..b3ca7ca791 100644
--- a/docs/cookbook/adding-a-vendored-package.i18n.yaml
+++ b/docs/cookbook/adding-a-vendored-package.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-adding-a-vendored-package.md: d7b5b93b59fb39d8369be6eb42fb0a8b977c68b4
-adding-a-vendored-package.zh.md: 86b1e6c959180ba15b6fcb56b6dfe5a3be791b47
+adding-a-vendored-package.md: 1b82f2e582ca5cd040a7f3237505848dbb304fae
+adding-a-vendored-package.zh.md: 7245682ef8b7d85ace2c626f8d47aa36f739506b
diff --git a/docs/cookbook/adding-a-vendored-package.md b/docs/cookbook/adding-a-vendored-package.md
index d7b5b93b59..1b82f2e582 100644
--- a/docs/cookbook/adding-a-vendored-package.md
+++ b/docs/cookbook/adding-a-vendored-package.md
@@ -2,7 +2,7 @@
English | [中文](adding-a-vendored-package.zh.md)
-When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-http`), it is **vendored** as pinned source under `vendor/`, not added as an npm dependency — see [the vendoring decision](../rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md) for why. [vendor/README.md](../../vendor/README.md) covers *updating* an already-vendored package; this guide is the file-by-file checklist for adding a **new** one. (Verified against the existing vendored set; if it drifts, fix it here.)
+When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-http`), it is **vendored** as pinned source under `vendor/`, not added as an npm dependency — see [the vendoring decision](../../.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md) for why. [vendor/README.md](../../vendor/README.md) covers *updating* an already-vendored package; this guide is the file-by-file checklist for adding a **new** one. (Verified against the existing vendored set; if it drifts, fix it here.)
## 1. Copy the source in
diff --git a/docs/cookbook/adding-a-vendored-package.zh.md b/docs/cookbook/adding-a-vendored-package.zh.md
index 86b1e6c959..7245682ef8 100644
--- a/docs/cookbook/adding-a-vendored-package.zh.md
+++ b/docs/cookbook/adding-a-vendored-package.zh.md
@@ -2,7 +2,7 @@
[English](adding-a-vendored-package.md) | 中文
-当 harness 需要引入另一个上游 Cordis 包(如 `@cordisjs/plugin-http`)时,应将其作为固定版本的源码 **vendor** 到 `vendor/` 下,而非作为 npm 依赖添加——原因见[vendoring 决策](../rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md)。[vendor/README.md](../../vendor/README.md) 介绍如何*更新*已有的 vendored 包;本指南是添加**新** vendored 包的逐文件清单。(已对照现有 vendored 集合验证;如有偏差,请在此修正。)
+当 harness 需要引入另一个上游 Cordis 包(如 `@cordisjs/plugin-http`)时,应将其作为固定版本的源码 **vendor** 到 `vendor/` 下,而非作为 npm 依赖添加——原因见[vendoring 决策](../../.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md)。[vendor/README.md](../../vendor/README.md) 介绍如何*更新*已有的 vendored 包;本指南是添加**新** vendored 包的逐文件清单。(已对照现有 vendored 集合验证;如有偏差,请在此修正。)
## 1. 复制源码
diff --git a/docs/cookbook/adding-an-llm-adapter.i18n.yaml b/docs/cookbook/adding-an-llm-adapter.i18n.yaml
index 37f934f3e6..497ae08c32 100644
--- a/docs/cookbook/adding-an-llm-adapter.i18n.yaml
+++ b/docs/cookbook/adding-an-llm-adapter.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-adding-an-llm-adapter.md: 70306ccf119f523dd812a859eef1e8628383bb48
-adding-an-llm-adapter.zh.md: e6d151adfc793a829a876031d5d7280ba078c8e9
+adding-an-llm-adapter.md: f20442b8c2ce823452a3ea13409f202185d12d04
+adding-an-llm-adapter.zh.md: 2864dd1e18742c7449e24f22504a5a38976ab450
diff --git a/docs/cookbook/adding-an-llm-adapter.md b/docs/cookbook/adding-an-llm-adapter.md
index 70306ccf11..f20442b8c2 100644
--- a/docs/cookbook/adding-an-llm-adapter.md
+++ b/docs/cookbook/adding-an-llm-adapter.md
@@ -16,11 +16,11 @@ export const inject = ['llm']
export const Config: z = z.object({ apiKey: z.string(), … })
export function apply(ctx: Context, config: Config) {
- ctx.llm.registerAdapter(['model-a', 'model-b'], new MyAdapter(…))
+ ctx.llm.registerAdapter(['my-provider'], new MyAdapter(…))
}
```
-Registration is effect-based (HMR-safe); one adapter per model name — duplicates throw. Secrets are cordis-native: schemastery Config with env fallbacks, fed from cordis.yml via `!!js process.env.MY_KEY`. Never read ad-hoc key files in code.
+Registration is effect-based (HMR-safe); one adapter per provider route — duplicates throw, and multi-route registration is all-or-nothing. `options.provider` selects the adapter and `options.model` is the provider model id, so a dynamic catalog adapter can serve new models without lifecycle reconfiguration. Secrets are cordis-native: schemastery Config with env fallbacks, fed from cordis.yml via `!!js process.env.MY_KEY`. Never read ad-hoc key files in code.
## Protocol obligations (the contract two implementations verified)
@@ -30,6 +30,7 @@ Registration is effect-based (HMR-safe); one adapter per model name — duplicat
- Errors have exactly two sanctioned paths: THROW from `stream()` (transport and protocol failures — use `LlmError` with a stable code), or end the stream with `finish {kind: 'error' | 'aborted'}` (provider in-band failures). Consumers handle both; pick per failure class and document it.
- Honor `options.signal` (pass it to fetch / your SDK).
- A `GenerateOptions` field your provider cannot honor (e.g. a `stop` list on a provider without stop sequences): throw `LlmError(..., 'UNSUPPORTED')` rather than silently dropping it.
+- If the provider requires response ids, signatures, or other native metadata on follow-up calls, emit the minimal lossless-JSON projection as `finish.replayState`. Validate it when rebuilding history. `LlmService` passes it only when the historical provider route and target provider route are currently owned by the exact same adapter instance; your adapter decides whether same-model, cross-model, or cross-provider restoration is legal. Never infer native replay from provider/model names alone when state is absent.
Provider-specific request knobs (thinking modes, effort levels) belong in the ADAPTER's Config, not in `GenerateOptions` — the core vocabulary stays provider-neutral.
@@ -41,5 +42,5 @@ Split the adapter into testable stages (llm-deepseek's layout): wire types (`typ
- **Unit: mock the provider, not the harness.** A scripted `node:http` server speaking the provider's wire format covers happy paths, every error status, malformed payloads, premature closes, and aborts — no network, and it drives the 100% per-file coverage gate. Works for SDK-backed adapters too (point the SDK's baseURL at the mock).
- **Hostile framing tests.** Split stream payloads at arbitrary byte positions (including mid-UTF-8) — real networks do.
-- **E2E: `tests/*.e2e.ts`** under `pnpm run test:e2e`, gated with `describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green. Cover each model × each provider mode you map (thinking on/off, effort levels), a tool-call round trip INCLUDING the follow-up turn with results in history, and loose assertions only (substring/structure, bounded maxTokens — real models are nondeterministic).
+- **E2E: `tests/*.e2e.ts`** under `pnpm run test:e2e`, gated with `describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green. Cover representative model/provider/API families and every provider mode you map, a tool-call round trip INCLUDING the follow-up turn with results in history, and loose assertions only (substring/structure, bounded maxTokens — real models are nondeterministic).
- Register the e2e file pattern in `knip.json` (per-workspace `entry` override) or knip flags it unused.
diff --git a/docs/cookbook/adding-an-llm-adapter.zh.md b/docs/cookbook/adding-an-llm-adapter.zh.md
index e6d151adfc..2864dd1e18 100644
--- a/docs/cookbook/adding-an-llm-adapter.zh.md
+++ b/docs/cookbook/adding-an-llm-adapter.zh.md
@@ -16,11 +16,11 @@ export const inject = ['llm']
export const Config: z = z.object({ apiKey: z.string(), … })
export function apply(ctx: Context, config: Config) {
- ctx.llm.registerAdapter(['model-a', 'model-b'], new MyAdapter(…))
+ ctx.llm.registerAdapter(['my-provider'], new MyAdapter(…))
}
```
-注册基于副作用(HMR 安全);每个模型名称对应一个适配器,重复注册会抛出异常。密钥采用 Cordis 原生方式管理:schemastery Config 带环境变量回退,通过 cordis.yml 的 `!!js process.env.MY_KEY` 注入。代码中禁止临时读取密钥文件。
+注册基于副作用(HMR 安全);每个提供方路由仅对应一个适配器,重复注册会抛出异常,多路由注册要么全部成功,要么全部失败。`options.provider` 用于选择适配器,`options.model` 是提供方模型 ID,因此动态模型目录适配器无需重新配置生命周期即可提供新模型。密钥采用 Cordis 原生方式管理:schemastery Config 带环境变量回退,通过 cordis.yml 的 `!!js process.env.MY_KEY` 注入。代码中禁止临时读取密钥文件。
## 协议义务(两个实现共同验证的契约)
@@ -30,6 +30,7 @@ export function apply(ctx: Context, config: Config) {
- 错误有且仅有两条合法路径:从 `stream()` **抛出**(传输与协议故障——使用带稳定 code 的 `LlmError`),或以 `finish {kind: 'error' | 'aborted'}` 结束流(提供方带内故障)。消费方两者都处理;按故障类别选择路径并加以文档化。
- 遵守 `options.signal`(将其传递给 fetch 或你的 SDK)。
- 如果 `GenerateOptions` 中某个字段你的提供方无法支持(例如提供方不支持 stop sequences 时收到 `stop` 列表):抛出 `LlmError(..., 'UNSUPPORTED')`,而非静默丢弃。
+- 如果提供方在后续调用中需要响应 ID、签名或其他原生元数据,请将其最小无损 JSON 投影作为 `finish.replayState` 发出。重建历史时验证该状态。只有历史提供方路由和目标提供方路由当前由完全相同的适配器实例拥有时,`LlmService` 才会传递该状态;由适配器决定同模型、跨模型或跨提供方恢复是否合法。状态缺失时,切勿仅根据提供方/模型名称推断原生回放。
提供方特有的请求旋钮(thinking 模式、effort 级别)放在**适配器**的 Config 中,而非 `GenerateOptions` 中——核心词汇保持提供方无关。
@@ -41,5 +42,5 @@ export function apply(ctx: Context, config: Config) {
- **单元测试:mock 提供方,而非 harness。** 用脚本化的 `node:http` 服务器模拟提供方的协议格式,覆盖正常路径、所有错误状态码、畸形载荷、连接提前关闭和中止——无需网络,且能满足 100% 逐文件覆盖率门禁。对基于 SDK 的适配器同样适用(将 SDK 的 baseURL 指向 mock 服务器)。
- **恶意分帧测试。** 在任意字节位置(包括 UTF-8 字符中间)切割流载荷——真实网络环境正是如此。
-- **E2E:`tests/*.e2e.ts`**,通过 `pnpm run test:e2e` 运行,以 `describe.skipIf(!process.env.MY_KEY)` 守卫,确保无密钥的 CI 保持绿色。覆盖你映射的每个模型 × 每种提供方模式(thinking 开/关、effort 级别)、一次包含后续轮次(历史中带工具结果)的工具调用往返,以及仅做宽松断言(子串/结构匹配、有界的 maxTokens——真实模型是非确定性的)。
+- **E2E:`tests/*.e2e.ts`**,通过 `pnpm run test:e2e` 运行,以 `describe.skipIf(!process.env.MY_KEY)` 守卫,确保无密钥的 CI 保持绿色。覆盖具有代表性的模型/提供方/API 系列以及你映射的每种提供方模式、一次包含后续轮次(历史中带工具结果)的工具调用往返,以及仅做宽松断言(子串/结构匹配、有界的 maxTokens——真实模型是非确定性的)。
- 在 `knip.json` 中注册 e2e 文件模式(per-workspace `entry` 覆盖),否则 knip 会将其标记为未使用。
diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml
index 23dcd83aee..a061eec1f6 100644
--- a/docs/cookbook/extension-cookbook.i18n.yaml
+++ b/docs/cookbook/extension-cookbook.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
-extension-cookbook.md: ea027710e80c6afc83f1a074b60fb83bb5a86642
-extension-cookbook.zh.md: d8b69e16fd53d89e6f5a8d855043fb39b8b667bd
+extension-cookbook.md: caa90f5d8dca940fef5986fbc14dc383a0bf8f08
+extension-cookbook.zh.md: b37687585b0be2a01c20f9b68d579259012880ab
diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md
index ea027710e8..caa90f5d8d 100644
--- a/docs/cookbook/extension-cookbook.md
+++ b/docs/cookbook/extension-cookbook.md
@@ -4,11 +4,11 @@ English | [中文](extension-cookbook.zh.md)
> FIXME: This important guide has not received sufficient human design review; complete that review before the first release.
-The three plugin shapes you write against the harness extension surface, as illustrative snippets (elided imports and helper stubs — not copy-paste-complete). For the full step-by-step guides see [adding a package](./adding-a-package.md), [adding a tool](./adding-a-tool.md), and [adding an LLM adapter](./adding-an-llm-adapter.md); for the seams these hook into see [docs/architecture.md](../architecture.md).
+The three plugin shapes you write against the harness extension surface, as illustrative snippets (elided imports and helper stubs — not copy-paste-complete). For the full step-by-step guides see [adding a package](adding-a-package.md), [adding a tool](adding-a-tool.md), and [adding an LLM adapter](adding-an-llm-adapter.md); for the seams these hook into see [docs/architecture.md](../architecture.md).
## A tool plugin
-A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `execute` args, result shaping, the `run_in_background` pattern) lives in [adding-a-tool.md](./adding-a-tool.md) — that guide is the source of truth for the tool shape. Raw JSON-Schema `ToolDefinition`s are also accepted by `ctx.tools.register()` directly (that is how MCP-sourced tools arrive); `defineTool` is the typed sugar for first-party tools.
+A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `execute` args, result shaping, the `run_in_background` pattern) lives in [adding-a-tool.md](adding-a-tool.md) — that guide is the source of truth for the tool shape. Raw JSON-Schema `ToolDefinition`s are also accepted by `ctx.tools.register()` directly (that is how MCP-sourced tools arrive); `defineTool` is the typed sugar for first-party tools.
## A hook plugin (permission-gate example)
@@ -32,7 +32,7 @@ export function apply(ctx: Context) {
}
```
-This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an invariant needs a monotonic final denial, `tools/execute` when a plugin must wrap the actual dispatch lifetime (timeouts/retries/metrics; only `exec.signal` is replaceable), `tools/post-execute` for explicit result transformation, and `tools/result` for contained observation of the immutable final outcome. The [adding-a-tool guide](./adding-a-tool.md#execution-policy-and-observation) gives the selection rule.
+This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an invariant needs a monotonic final denial, `tools/execute` when a plugin must wrap the actual dispatch lifetime (timeouts/retries/metrics; only `exec.signal` is replaceable), `tools/post-execute` for explicit result transformation, and `tools/result` for contained observation of the immutable final outcome. The [adding-a-tool guide](adding-a-tool.md#execution-policy-and-observation) gives the selection rule.
## A UI plugin
@@ -40,7 +40,7 @@ A UI plugin renders from the `session/event` feed (the assistant token stream as
```ts
import type { Context } from 'cordis'
-import { AgentId } from '@deepseek-ai/dsh-agent'
+import { SessionId } from '@deepseek-ai/dsh-session'
declare function render(text: string): void
declare function onUserInput(handler: (text: string) => void): void
@@ -54,7 +54,7 @@ export function apply(ctx: Context) {
render(event.data.chunk.text)
}
})
- onUserInput(text => ctx.agents.get(AgentId('main'))?.send([{ type: 'text', text }]))
+ onUserInput(text => ctx.agents.get(SessionId('client-session'))?.send([{ type: 'text', text }]))
}
```
@@ -87,11 +87,11 @@ export function apply(ctx: Context) {
## Runnable wirings
-Three complete examples load their plugin trees from `cordis.yml`: [`examples/echo-agent`](../../examples/echo-agent) (mock model + echo tool — the all-mock skeleton check, `pnpm run demo:echo`), [`examples/coding-agent`](../../examples/coding-agent) (DeepSeek V4 + the bash tool suite behind a terminal REPL UI, `pnpm run demo:repl`), and [`examples/acp-agent`](../../examples/acp-agent) (an agent exposed as an ACP server over JSON-RPC stdio — the client-driver shape, `pnpm run demo:acp`). Each leaf is just its swappable backends plus an app-package entry: the stdio demos load [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo), the ACP demo loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and both app packages share the spine via the [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle.
+Four runnable leaves load their plugin trees from `cordis.yml`: [`examples/tui-agent`](../../examples/tui-agent) (DeepSeek coding tools through the full-screen TUI, `pnpm run demo:tui`), [`examples/headless-agent`](../../examples/headless-agent) (the coding capabilities behind a one-shot task and DSH-native output, `pnpm run demo:headless "task"`), [`examples/cordis-agent`](../../examples/cordis-agent) (self-inspection and dynamic plugin mounting through the TUI, `pnpm run demo:cordis`), and [`examples/acp-agent`](../../examples/acp-agent) (an ACP server over JSON-RPC stdio, `pnpm run demo:acp`). Interactive leaves load [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo), non-interactive leaves load [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), the ACP leaf loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and all three app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo).
## The feature → mechanism map
-Every product feature maps to a listener on a documented extension seam — the microkernel claim made checkable ([microkernel RFC](../rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)). No row modifies the loop.
+Every product feature maps to a listener on a documented extension seam — the microkernel claim made checkable ([microkernel Agent Note](../../.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)). No row modifies the loop.
`system-prompt/assemble` is an expert cooperative whole-assembly transform: its returned assembly is authoritative, so listener authors own preserving active Code Mode and structured-output protocol contributions. Prefer `ctx.tools.restrict()` for tool filtering that must stay aligned across presentation, lookup, and execution.
@@ -102,7 +102,7 @@ Every product feature maps to a listener on a documented extension seam — the
| `/loop` | on the `turn/end` session event, `send()` the next iteration; or force-continue |
| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and terminal `agent/turn-stop` |
| Queued + steering messages | core `Agent.send()` / `Agent.steer()` |
-| Context compaction (auto + manual) | the `ctx.compact` seam + a backend (`dsh-compact-basic`) on the serial `agent/pre-step` seam; auto = token-pressure check before each step; a manual trigger invokes the same `ctx.compact` routine ([compaction RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) |
+| Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/post-step`, canonical overflow recovery runs on `agent/request-error`, and manual callers use the same compact service ([compaction Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) |
| System prompt configurability | `ctx.systemPrompt.section()` with ordering and scope-local shadowing |
| AGENTS.md (root) | a section provider reading the file |
| AGENTS.md (subdir, on-touch) + file-change notices | `agent.inject()` from a watcher / tool-result listener |
diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md
index d8b69e16fd..b37687585b 100644
--- a/docs/cookbook/extension-cookbook.zh.md
+++ b/docs/cookbook/extension-cookbook.zh.md
@@ -4,11 +4,11 @@
> FIXME:这篇重要指南尚未经过充分的人工设计审查;请在首次发布前完成审查。
-针对 harness 扩展表面编写的三种插件形态,以示意性代码片段呈现(省略了 import 和辅助桩——不可直接复制运行)。完整的分步指南见[添加包(package)](./adding-a-package.md)、[添加工具](./adding-a-tool.md)和[添加 LLM(大语言模型)适配器](./adding-an-llm-adapter.md);这些插件所挂接的 seam 见 [docs/architecture.md](../architecture.md)。
+针对 harness 扩展表面编写的三种插件形态,以示意性代码片段呈现(省略了 import 和辅助桩——不可直接复制运行)。完整的分步指南见[添加包(package)](adding-a-package.md)、[添加工具](adding-a-tool.md)和[添加 LLM(大语言模型)适配器](adding-an-llm-adapter.md);这些插件所挂接的 seam 见 [docs/architecture.md](../architecture.md)。
## 工具插件
-工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果塑形、`run_in_background` 模式)见 [adding-a-tool.md](./adding-a-tool.md)——该指南是工具形态的真源。`ctx.tools.register()` 也直接接受原始 JSON-Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是为第一方工具提供的类型化语法糖。
+工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果塑形、`run_in_background` 模式)见 [adding-a-tool.md](adding-a-tool.md)——该指南是工具形态的真源。`ctx.tools.register()` 也直接接受原始 JSON-Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是为第一方工具提供的类型化语法糖。
## 钩子插件(以权限门禁为例)
@@ -32,7 +32,7 @@ export function apply(ctx: Context) {
}
```
-这个 waterfall(瀑布式事件)是可重排的策略层。当不变式需要单调的最终拒绝时使用 `ctx.tools.guard()`;当插件需要包裹实际分发生命周期时(超时/重试/指标;仅 `exec.signal` 可替换)使用 `tools/execute`;显式结果变换使用 `tools/post-execute`;对不可变最终结果的受限观察使用 `tools/result`。选择规则见[添加工具指南](./adding-a-tool.md#execution-policy-and-observation)。
+这个 waterfall(瀑布式事件)是可重排的策略层。当不变式需要单调的最终拒绝时使用 `ctx.tools.guard()`;当插件需要包裹实际分发生命周期时(超时/重试/指标;仅 `exec.signal` 可替换)使用 `tools/execute`;显式结果变换使用 `tools/post-execute`;对不可变最终结果的受限观察使用 `tools/result`。选择规则见[添加工具指南](adding-a-tool.md#execution-policy-and-observation)。
## UI 插件
@@ -40,7 +40,7 @@ UI 插件从 `session/event` 事件流渲染(助手 token 流以 `assistant/ch
```ts
import type { Context } from 'cordis'
-import { AgentId } from '@deepseek-ai/dsh-agent'
+import { SessionId } from '@deepseek-ai/dsh-session'
declare function render(text: string): void
declare function onUserInput(handler: (text: string) => void): void
@@ -54,7 +54,7 @@ export function apply(ctx: Context) {
render(event.data.chunk.text)
}
})
- onUserInput(text => ctx.agents.get(AgentId('main'))?.send([{ type: 'text', text }]))
+ onUserInput(text => ctx.agents.get(SessionId('client-session'))?.send([{ type: 'text', text }]))
}
```
@@ -87,11 +87,11 @@ export function apply(ctx: Context) {
## 可运行的组装示例
-三个完整示例从 `cordis.yml` 加载各自的插件树:[`examples/echo-agent`](../../examples/echo-agent)(mock 模型 + echo 工具——全 mock 骨架检查,`pnpm run demo:echo`)、[`examples/coding-agent`](../../examples/coding-agent)(DeepSeek V4 + bash 工具套件,配合终端 REPL UI,`pnpm run demo:repl`)、[`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露为 ACP 服务器的 agent——客户端驱动形态,`pnpm run demo:acp`)。每个叶子只是其可替换后端加一个 app 包入口:stdio 演示加载 [`@deepseek-ai/dsh-stdio-demo`](../../packages/examples/stdio-demo),ACP 演示加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),两个 app 包通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) bundle 共享主干。
+四个可运行叶子从 `cordis.yml` 加载各自的插件树:[`examples/tui-agent`](../../examples/tui-agent)(通过全屏 TUI 运行的 DeepSeek coding 工具,`pnpm run demo:tui`)、[`examples/headless-agent`](../../examples/headless-agent)(通过单次任务和 DSH 原生输出运行的 coding 能力,`pnpm run demo:headless "task"`)、[`examples/cordis-agent`](../../examples/cordis-agent)(通过 TUI 进行自我检查和动态插件挂载,`pnpm run demo:cordis`)与 [`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露的 ACP 服务器,`pnpm run demo:acp`)。交互式叶子加载 [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo),非交互式叶子加载 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo),ACP 叶子加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),三个 app 包都通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) 共享主干。
## 功能→机制映射
-每个产品功能都映射到一个文档化扩展 seam 上的监听器——微内核声明由此可验证([微内核 RFC](../rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md))。没有任何一行修改循环本身。
+每个产品功能都映射到一个文档化扩展 seam 上的监听器——微内核声明由此可验证([微内核 Agent Note](../../.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md))。没有任何一行修改循环本身。
`system-prompt/assemble` 是一个专家协作式的整体装配变换:其返回的装配结果具有权威性,因此监听器作者有责任保留活跃的 Code Mode 和结构化输出协议的贡献。对于需要在展示、查找和执行之间保持对齐的工具过滤,优先使用 `ctx.tools.restrict()`。
@@ -102,7 +102,7 @@ export function apply(ctx: Context) {
| `/loop` | 在 `turn/end` 会话事件上 `send()` 下一次迭代;或强制继续 |
| 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和终端 `agent/turn-stop` 来强制输出 |
| 排队消息 + steering(中途引导) | 核心 `Agent.send()` / `Agent.steer()` |
-| 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + 串行 `agent/pre-step` seam 上的后端(`dsh-compact-basic`);自动 = 每步之前的 token 压力检查;手动触发调用同一个 `ctx.compact` 例程([压缩 RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) |
+| 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/post-step`,规范化溢出恢复运行在 `agent/request-error`,手动调用方使用同一个压缩服务([压缩 Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) |
| 系统提示词可配置性 | `ctx.systemPrompt.section()`,支持排序与作用域局部覆盖 |
| AGENTS.md(根目录) | 一个读取该文件的 section provider |
| AGENTS.md(子目录,按需触发)+ 文件变更通知 | 从 watcher / tool-result 监听器调用 `agent.inject()` |
diff --git a/docs/cookbook/maintaining-dsh-code-review.md b/docs/cookbook/maintaining-dsh-code-review.md
index 5a6c2df120..8af449b749 100644
--- a/docs/cookbook/maintaining-dsh-code-review.md
+++ b/docs/cookbook/maintaining-dsh-code-review.md
@@ -1,6 +1,6 @@
# Maintaining the dsh-code-review skill
-The [`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill is kept current by a single designated operator running a private periodic maintenance tool. This cookbook is the entry point for that operator — and for anyone taking over the role — and for repo contributors who want to understand why skill updates arrive as small periodic PRs rather than one-off audits. The workflow itself is specified in the [human-review skill-maintenance RFC](../rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md).
+The [`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill is kept current by a single designated operator running a private periodic maintenance tool. This cookbook is the entry point for that operator — and for anyone taking over the role — and for repo contributors who want to understand why skill updates arrive as small periodic PRs rather than one-off audits. The workflow itself is specified in the [human-review skill-maintenance Agent Note](../../.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md).
## What the maintainer receives
@@ -55,8 +55,8 @@ The mechanism lives on one machine. Interruptions the operator handles as they a
- **Daily run missed.** The two-day overlap window catches one skipped day automatically; longer gaps recover by running the wrapper manually with `DSH_CODE_REVIEW_SINCE=`. Overlapping windows are idempotent: guidance already in the current skill is classified `covered` and does not re-enter as a candidate.
- **Adapter provider outage.** The tool refuses to run when the two reviewer commands resolve to byte-identical executables. A single batch whose adapter response fails schema or id validation is failed closed at the batch level (every item in the batch marked unclear) and the run continues; the raw output is preserved for debugging. If either adapter produces no valid result for any nonempty batch in an operation, the run fails, writes a failure record, and notifies the operator; it never collapses a total-provider outage into "no candidate."
-- **Handoff to another maintainer.** Open a follow-up RFC that supersedes the current one: either move the mechanism into the repository or record the new operator's private setup. Do not silently transfer the tool — the "single-maintainer bus factor" in the RFC's Risks section is the reason the handoff needs a documented decision.
+- **Handoff to another maintainer.** Open a follow-up Agent Note that supersedes the current one: either move the mechanism into the repository or record the new operator's private setup. Do not silently transfer the tool — the "single-maintainer bus factor" in the Agent Note's Risks section is the reason the handoff needs a documented decision.
## Where the operator's private setup lives
-The tool source, reviewer adapters, provider credentials, and scheduler are the operator's private infrastructure and are outside this repository by design (see the RFC's "Where the mechanism lives" section). This cookbook and the RFC describe **what the workflow guarantees**; **how** those guarantees are implemented is a private-infrastructure concern. If you are the new operator, the RFC's `## Proposal` sections are the specification you build against.
+The tool source, reviewer adapters, provider credentials, and scheduler are the operator's private infrastructure and are outside this repository by design (see the Agent Note's "Where the mechanism lives" section). This cookbook and the Agent Note describe **what the workflow guarantees**; **how** those guarantees are implemented is a private-infrastructure concern. If you are the new operator, the Agent Note's `## Proposal` sections are the specification you build against.
diff --git a/docs/cordis-catalog/core/context.md b/docs/cordis-catalog/core/context.md
new file mode 100644
index 0000000000..f6b249c738
--- /dev/null
+++ b/docs/cordis-catalog/core/context.md
@@ -0,0 +1,364 @@
+
+
+# Context
+
+The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md).
+
+Root and child dependency containers for Cordis plugins.
+
+A context is a proxy: normal property reads go through the service resolver, while `extend()`, `isolate()`, and `intercept()` create scoped child contexts without mutating their parent.
+
+[Source](../../../vendor/cordis/src/context.ts#L42)
+
+### ctx.extend(meta?)
+
+```ts cordis-catalog
+/**
+ * Create a child context with extra metadata on top of the current scope.
+ *
+ * The child prototypally inherits every property of this context; own
+ * properties of `meta` shadow the inherited ones. The parent is not mutated.
+ *
+ * @param meta — own properties (including symbol keys) to define on the child.
+ * @returns a child context inheriting from this one.
+ */
+extend(meta = {}): this
+```
+
+Create a child context with extra metadata on top of the current scope.
+
+The child prototypally inherits every property of this context; own properties of `meta` shadow the inherited ones. The parent is not mutated.
+
+- `meta` — own properties (including symbol keys) to define on the child.
+
+**Returns** a child context inheriting from this one.
+
+[Source](../../../vendor/cordis/src/context.ts#L99)
+
+### ctx.isolate(name, label?)
+
+```ts cordis-catalog
+/**
+ * Create a child context with an independent service scope for `name`.
+ *
+ * Below the returned context, reads and writes of the service `name`
+ * resolve against the new label instead of the parent's, so a different
+ * implementation can be provided without affecting the parent scope.
+ * Passing the same `label` to two `isolate()` calls joins their scopes.
+ *
+ * @param name — the service name to isolate.
+ * @param label — scope label to join; defaults to a fresh unique symbol.
+ * @returns a child context whose `name` service resolves in the new scope.
+ */
+isolate(name: string, label?: symbol)
+```
+
+Create a child context with an independent service scope for `name`.
+
+Below the returned context, reads and writes of the service `name` resolve against the new label instead of the parent's, so a different implementation can be provided without affecting the parent scope. Passing the same `label` to two `isolate()` calls joins their scopes.
+
+- `name` — the service name to isolate.
+- `label` — scope label to join; defaults to a fresh unique symbol.
+
+**Returns** a child context whose `name` service resolves in the new scope.
+
+[Source](../../../vendor/cordis/src/context.ts#L121)
+
+### ctx.intercept(name, config)
+
+```ts cordis-catalog
+/**
+ * Add service-specific intercept config for plugins started below this
+ * context.
+ *
+ * Plugins loaded under the returned context see `config` merged into the
+ * service's resolved config (ancestor entries first; see
+ * `Service[symbols.resolveConfig]`). The parent context is not affected.
+ *
+ * @param name — the service name whose config to intercept.
+ * @param config — the intercept config to merge for that service.
+ * @returns a child context carrying the additional intercept entry.
+ */
+intercept(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this
+intercept(name: string, config: any): this
+```
+
+Add service-specific intercept config for plugins started below this context.
+
+Plugins loaded under the returned context see `config` merged into the service's resolved config (ancestor entries first; see `Service[symbols.resolveConfig]`). The parent context is not affected.
+
+- `name` — the service name whose config to intercept.
+- `config` — the intercept config to merge for that service.
+
+**Returns** a child context carrying the additional intercept entry.
+
+[Source](../../../vendor/cordis/src/context.ts#L139)
+
+### ctx.root
+
+```ts cordis-catalog
+/** The root context of the application (every child context shares it). @experimental */
+root: this
+```
+
+The root context of the application (every child context shares it). @experimental
+
+[Source](../../../vendor/cordis/src/context.ts#L22)
+
+### ctx.baseUrl
+
+```ts cordis-catalog
+/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */
+baseUrl?: string
+```
+
+Base URL used to resolve relative plugin/module specifiers, if the runtime sets one.
+
+[Source](../../../vendor/cordis/src/context.ts#L24)
+
+### ctx.events
+
+```ts cordis-catalog
+/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */
+events: EventsService
+```
+
+The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...).
+
+[Source](../../../vendor/cordis/src/context.ts#L26)
+
+### ctx.logger
+
+```ts cordis-catalog
+/** The logging service. Call `ctx.logger(name)` for a named logger. */
+logger: LoggerService
+```
+
+The logging service. Call `ctx.logger(name)` for a named logger.
+
+[Source](../../../vendor/cordis/src/context.ts#L28)
+
+### ctx.reflect
+
+```ts cordis-catalog
+/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */
+reflect: ReflectService
+```
+
+The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...).
+
+[Source](../../../vendor/cordis/src/context.ts#L30)
+
+### ctx.registry
+
+```ts cordis-catalog
+/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */
+registry: RegistryService
+```
+
+The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`).
+
+[Source](../../../vendor/cordis/src/context.ts#L32)
+
+## Static members
+
+### Context.effect
+
+```ts cordis-catalog
+/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */
+static readonly effect: unique symbol
+```
+
+Symbol key under which a disposer exposes its EffectMeta diagnostics tree.
+
+[Source](../../../vendor/cordis/src/context.ts#L44)
+
+### Context.filter
+
+```ts cordis-catalog
+/** Symbol key for a context's listener filter, consulted on every event dispatch. */
+static readonly filter: unique symbol
+```
+
+Symbol key for a context's listener filter, consulted on every event dispatch.
+
+[Source](../../../vendor/cordis/src/context.ts#L46)
+
+### Context.isolate
+
+```ts cordis-catalog
+/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */
+static readonly isolate: unique symbol
+```
+
+Symbol key of the isolation map (see the `Context[symbols.isolate]` property).
+
+[Source](../../../vendor/cordis/src/context.ts#L48)
+
+### Context.intercept
+
+```ts cordis-catalog
+/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */
+static readonly intercept: unique symbol
+```
+
+Symbol key of the intercept map (see the `Context[symbols.intercept]` property).
+
+[Source](../../../vendor/cordis/src/context.ts#L50)
+
+### Context.is(value)
+
+```ts cordis-catalog
+/**
+ * Returns true for Cordis context proxies and context prototypes.
+ *
+ * Works across realms and across multiple copies of cordis, because the
+ * brand is keyed by a global symbol rather than by `instanceof`.
+ *
+ * @param value — the value to test.
+ * @returns `true` if `value` is a Cordis context, narrowing its type.
+ */
+static is(value: any): value is Context
+```
+
+Returns true for Cordis context proxies and context prototypes.
+
+Works across realms and across multiple copies of cordis, because the brand is keyed by a global symbol rather than by `instanceof`.
+
+- `value` — the value to test.
+
+**Returns** `true` if `value` is a Cordis context, narrowing its type.
+
+[Source](../../../vendor/cordis/src/context.ts#L61)
+
+## Service store and mixins
+
+### ctx.get(name, strict?)
+
+```ts cordis-catalog
+/**
+ * Read a service from the store without the inject requirement.
+ *
+ * @param name — the service name.
+ * @param strict — when `true` (default), only return implementations
+ * whose providing fiber is currently active.
+ * @returns the service value, or `undefined` when not (yet) provided.
+ */
+get(name: K, strict?: boolean): undefined | this[K]
+get(name: string, strict?: boolean): any
+```
+
+Read a service from the store without the inject requirement.
+
+- `name` — the service name.
+- `strict` — when `true` (default), only return implementations whose providing fiber is currently active.
+
+**Returns** the service value, or `undefined` when not (yet) provided.
+
+[Source](../../../vendor/cordis/src/reflect.ts#L16)
+
+### ctx.set(name, value)
+
+```ts cordis-catalog
+/**
+ * Overwrite a provided service's value.
+ *
+ * Only the fiber that provided the service may set it; setting an
+ * unprovided name throws.
+ *
+ * @param name — the service name.
+ * @param value — the new service value.
+ */
+set(name: K, value: undefined | this[K]): void
+set(name: string, value: any): void
+```
+
+Overwrite a provided service's value.
+
+Only the fiber that provided the service may set it; setting an unprovided name throws.
+
+- `name` — the service name.
+- `value` — the new service value.
+
+[Source](../../../vendor/cordis/src/reflect.ts#L28)
+
+### ctx.provide(name, value)
+
+```ts cordis-catalog
+/**
+ * Register a service implementation owned by the current fiber.
+ *
+ * The service becomes visible to dependents in the same isolation scope
+ * once the fiber is active; it is unregistered (waking dependents) when
+ * the returned disposer runs or the fiber unloads. Throws if the name is
+ * already provided in this scope or declared as an accessor.
+ *
+ * @param name — the service name.
+ * @param value — the service value.
+ * @returns a disposer that unregisters the service.
+ */
+provide(name: K, value: undefined | this[K]): () => void
+provide(name: string, value?: any): () => void
+```
+
+Register a service implementation owned by the current fiber.
+
+The service becomes visible to dependents in the same isolation scope once the fiber is active; it is unregistered (waking dependents) when the returned disposer runs or the fiber unloads. Throws if the name is already provided in this scope or declared as an accessor.
+
+- `name` — the service name.
+- `value` — the service value.
+
+**Returns** a disposer that unregisters the service.
+
+[Source](../../../vendor/cordis/src/reflect.ts#L43)
+
+### ctx.accessor(name, options)
+
+```ts cordis-catalog
+/**
+ * Define a computed context property backed by get/set hooks.
+ *
+ * The accessor is removed when the current fiber unloads. Throws if the
+ * name is already declared.
+ *
+ * @param name — the context property name.
+ * @param options — the `get` hook and optional `set` hook.
+ */
+accessor(name: string, options: Omit): void
+```
+
+Define a computed context property backed by get/set hooks.
+
+The accessor is removed when the current fiber unloads. Throws if the name is already declared.
+
+- `name` — the context property name.
+- `options` — the `get` hook and optional `set` hook.
+
+[Source](../../../vendor/cordis/src/reflect.ts#L55)
+
+### ctx.mixin(name, mixins)
+
+```ts cordis-catalog
+/**
+ * Expose selected members of a service directly on `ctx`.
+ *
+ * Each mixed-in key becomes an accessor that forwards to the service
+ * (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`.
+ * Mixins are removed when the current fiber unloads.
+ *
+ * @param name — the context property holding the source service.
+ * @param mixins — keys to forward, or a source-key → ctx-key map.
+ */
+mixin(name: K, mixins: (keyof this & keyof this[K])[] | Dict): void
+mixin(source: T, mixins: (keyof this & keyof T)[] | Dict): void
+```
+
+Expose selected members of a service directly on `ctx`.
+
+Each mixed-in key becomes an accessor that forwards to the service (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`. Mixins are removed when the current fiber unloads.
+
+- `name` — the context property holding the source service.
+- `mixins` — keys to forward, or a source-key → ctx-key map.
+
+[Source](../../../vendor/cordis/src/reflect.ts#L66)
diff --git a/website/zh-CN/api/cordis/events.md b/docs/cordis-catalog/core/events.md
similarity index 55%
rename from website/zh-CN/api/cordis/events.md
rename to docs/cordis-catalog/core/events.md
index b56f8096fe..2fb64e78a2 100644
--- a/website/zh-CN/api/cordis/events.md
+++ b/docs/cordis-catalog/core/events.md
@@ -1,12 +1,20 @@
-
+
# Events
-The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).
+The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).
### ctx.parallel(name, ...args)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Dispatch an event, running all listeners concurrently.
+ *
+ * @param name — the event name.
+ * @param args — arguments passed to every listener.
+ * @returns a promise resolving once every listener has settled.
+ */
parallel(name: K, ...args: Parameters): Promise
parallel(thisArg: NoInfer>, name: K, ...args: Parameters): Promise
```
@@ -18,11 +26,17 @@ Dispatch an event, running all listeners concurrently.
**Returns** a promise resolving once every listener has settled.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L43)
+[Source](../../../vendor/cordis/src/events.ts#L43)
### ctx.emit(name, ...args)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Dispatch an event synchronously, ignoring listener return values.
+ *
+ * @param name — the event name.
+ * @param args — arguments passed to every listener.
+ */
emit(name: K, ...args: Parameters): void
emit(thisArg: NoInfer>, name: K, ...args: Parameters): void
```
@@ -32,11 +46,18 @@ Dispatch an event synchronously, ignoring listener return values.
- `name` — the event name.
- `args` — arguments passed to every listener.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L52)
+[Source](../../../vendor/cordis/src/events.ts#L52)
### ctx.serial(name, ...args)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Dispatch an event, awaiting listeners in order until one bails.
+ *
+ * @param name — the event name.
+ * @param args — arguments passed to each listener.
+ * @returns the first bail value (non-null, non-false, non-undefined), if any.
+ */
serial(name: K, ...args: Parameters): Promisify>
serial(thisArg: NoInfer>, name: K, ...args: Parameters): Promisify>
```
@@ -48,11 +69,18 @@ Dispatch an event, awaiting listeners in order until one bails.
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L62)
+[Source](../../../vendor/cordis/src/events.ts#L62)
### ctx.bail(name, ...args)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Dispatch an event, calling listeners in order until one bails.
+ *
+ * @param name — the event name.
+ * @param args — arguments passed to each listener.
+ * @returns the first bail value (non-null, non-false, non-undefined), if any.
+ */
bail(name: K, ...args: Parameters): ReturnType
bail(thisArg: NoInfer>, name: K, ...args: Parameters): ReturnType
```
@@ -64,16 +92,27 @@ Dispatch an event, calling listeners in order until one bails.
**Returns** the first bail value (non-null, non-false, non-undefined), if any.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L72)
+[Source](../../../vendor/cordis/src/events.ts#L72)
### ctx.waterfall(name, ...args)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Dispatch an event whose last argument is a `next` continuation.
+ *
+ * Each listener wraps the rest of the chain: calling `next()` invokes the
+ * next listener (finally the built-in behavior); not calling it vetoes.
+ *
+ * @param name — the event name.
+ * @param args — listener arguments; the final one is the innermost `next`.
+ * @returns the outermost listener's return value.
+ */
waterfall(name: K, ...args: Parameters): ReturnType
waterfall(thisArg: NoInfer>, name: K, ...args: Parameters): ReturnType
```
Dispatch an event whose last argument is a `next` continuation.
+
Each listener wraps the rest of the chain: calling `next()` invokes the next listener (finally the built-in behavior); not calling it vetoes.
- `name` — the event name.
@@ -81,11 +120,19 @@ Each listener wraps the rest of the chain: calling `next()` invokes the next lis
**Returns** the outermost listener's return value.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L85)
+[Source](../../../vendor/cordis/src/events.ts#L85)
### ctx.on(name, listener, options?)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Register an event listener owned by the current fiber.
+ *
+ * @param name — the event name to listen for.
+ * @param listener — called with the dispatch arguments.
+ * @param options — listener options; a boolean is shorthand for `prepend`.
+ * @returns a disposer removing the listener; `true` if it was still registered.
+ */
on(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
```
@@ -97,11 +144,19 @@ Register an event listener owned by the current fiber.
**Returns** a disposer removing the listener; `true` if it was still registered.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L96)
+[Source](../../../vendor/cordis/src/events.ts#L96)
### ctx.once(name, listener, options?)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Same as `on()`, but the listener disposes itself after its first call.
+ *
+ * @param name — the event name to listen for.
+ * @param listener — called at most once with the dispatch arguments.
+ * @param options — listener options; a boolean is shorthand for `prepend`.
+ * @returns a disposer removing the listener; `true` if it was still registered.
+ */
once(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
```
@@ -113,13 +168,14 @@ Same as `on()`, but the listener disposes itself after its first call.
**Returns** a disposer removing the listener; `true` if it was still registered.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L105)
+[Source](../../../vendor/cordis/src/events.ts#L105)
## EventOptions
Options accepted by `ctx.on()` and `ctx.once()`.
-```ts website-api
+```ts cordis-catalog
+/** Options accepted by `ctx.on()` and `ctx.once()`. */
interface EventOptions {
/** Add the listener before existing listeners for the same event. */
prepend?: boolean
@@ -128,15 +184,24 @@ interface EventOptions {
}
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L111)
+[Source](../../../vendor/cordis/src/events.ts#L111)
## DispatchMode
Event dispatch strategy used by the event service.
+
`emit` runs synchronous listeners without awaiting them, `parallel` awaits all listeners together, `serial` awaits them in order until one bails, `bail` stops on the first synchronous bail value, and `waterfall` composes listeners around a final `next` callback.
-```ts website-api
+```ts cordis-catalog
+/**
+ * Event dispatch strategy used by the event service.
+ *
+ * `emit` runs synchronous listeners without awaiting them, `parallel` awaits
+ * all listeners together, `serial` awaits them in order until one bails,
+ * `bail` stops on the first synchronous bail value, and `waterfall` composes
+ * listeners around a final `next` callback.
+ */
type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L31)
+[Source](../../../vendor/cordis/src/events.ts#L31)
diff --git a/website/zh-CN/api/cordis/fiber.md b/docs/cordis-catalog/core/fiber.md
similarity index 50%
rename from website/zh-CN/api/cordis/fiber.md
rename to docs/cordis-catalog/core/fiber.md
index db93a6d273..d865ce01fc 100644
--- a/website/zh-CN/api/cordis/fiber.md
+++ b/docs/cordis-catalog/core/fiber.md
@@ -1,17 +1,32 @@
-
+
# Fiber
-A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it.
+A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.
### ctx.effect(execute, label?)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Register a cleanup-aware effect on this fiber.
+ *
+ * `execute` runs immediately; the disposers it produces are collected and
+ * run (in reverse order) either when the returned disposer is called or
+ * when the fiber unloads, whichever comes first. Calling the disposer twice
+ * is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
+ * already disposed, and `TypeError` if `execute` returns an invalid shape.
+ *
+ * @param execute — the effect body; see {@link Effect} for accepted shapes.
+ * @param label — effect label shown in `getEffects()` diagnostics.
+ * @returns a disposer that tears the effect down and settles once done.
+ */
effect(execute: () => SyncEffect, label?: string): Disposable>
effect(execute: () => Effect, label?: string): AsyncDisposable>
```
Register a cleanup-aware effect on this fiber.
+
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
- `execute` — the effect body; see `Effect` for accepted shapes.
@@ -19,108 +34,124 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
+[Source](../../../vendor/cordis/src/fiber.ts#L419)
### ctx.fiber
-```ts website-api
+```ts cordis-catalog
+/** The fiber (plugin runtime instance) that owns this context. */
fiber: Fiber
```
The fiber (plugin runtime instance) that owns this context.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L11)
+[Source](../../../vendor/cordis/src/fiber.ts#L11)
## The Fiber class
Runtime instance of one plugin application.
+
A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by `ctx.plugin()`.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L183)
+[Source](../../../vendor/cordis/src/fiber.ts#L183)
### fiber.uid
-```ts website-api
+```ts cordis-catalog
+/** Unique id within the registry; 0 for the root fiber, `null` once disposed. */
public uid: number | null
```
Unique id within the registry; 0 for the root fiber, `null` once disposed.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L185)
+[Source](../../../vendor/cordis/src/fiber.ts#L185)
### fiber.ctx
-```ts website-api
+```ts cordis-catalog
+/** The context this fiber's plugin runs in (extends the parent context). */
public readonly ctx: Context
```
The context this fiber's plugin runs in (extends the parent context).
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L187)
+[Source](../../../vendor/cordis/src/fiber.ts#L187)
### fiber.config
-```ts website-api
+```ts cordis-catalog
+/** The validated plugin config (updated by `update()`). */
public config: any
```
The validated plugin config (updated by `update()`).
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L189)
+[Source](../../../vendor/cordis/src/fiber.ts#L189)
### fiber.state
-```ts website-api
+```ts cordis-catalog
+/** Current lifecycle state; transitions emit `internal/status`. */
public state
```
Current lifecycle state; transitions emit `internal/status`.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L191)
+[Source](../../../vendor/cordis/src/fiber.ts#L191)
### fiber.dispose
-```ts website-api
+```ts cordis-catalog
+/** Dispose this fiber: unload the plugin, then settle once cleanup finished. */
public readonly dispose: () => Promise
```
Dispose this fiber: unload the plugin, then settle once cleanup finished.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L193)
+[Source](../../../vendor/cordis/src/fiber.ts#L193)
### fiber.store
-```ts website-api
+```ts cordis-catalog
+/** Snapshot of required service implementations while loaded; `undefined` otherwise. */
public store: Dict | undefined
```
Snapshot of required service implementations while loaded; `undefined` otherwise.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L195)
+[Source](../../../vendor/cordis/src/fiber.ts#L195)
### fiber.inertia
-```ts website-api
+```ts cordis-catalog
+/** The in-flight load/unload transition, if one is currently running. */
public inertia: Promise | undefined
```
The in-flight load/unload transition, if one is currently running.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L197)
+[Source](../../../vendor/cordis/src/fiber.ts#L197)
### fiber.name
-```ts website-api
+```ts cordis-catalog
+/** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */
get name()
```
The plugin's display name, inherited from the nearest named ancestor, else `'root'`.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L340)
+[Source](../../../vendor/cordis/src/fiber.ts#L340)
### fiber.assertActive()
-```ts website-api
+```ts cordis-catalog
+/**
+ * Throw if the fiber has already been disposed.
+ *
+ * @returns nothing when the fiber is still active.
+ * @throws {CordisError} `INACTIVE_EFFECT` when the fiber's uid has been cleared.
+ */
assertActive()
```
@@ -128,16 +159,30 @@ Throw if the fiber has already been disposed.
**Returns** nothing when the fiber is still active.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L355)
+[Source](../../../vendor/cordis/src/fiber.ts#L355)
### fiber.effect(execute, label?)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Register a cleanup-aware effect on this fiber.
+ *
+ * `execute` runs immediately; the disposers it produces are collected and
+ * run (in reverse order) either when the returned disposer is called or
+ * when the fiber unloads, whichever comes first. Calling the disposer twice
+ * is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is
+ * already disposed, and `TypeError` if `execute` returns an invalid shape.
+ *
+ * @param execute — the effect body; see {@link Effect} for accepted shapes.
+ * @param label — effect label shown in `getEffects()` diagnostics.
+ * @returns a disposer that tears the effect down and settles once done.
+ */
effect(execute: () => SyncEffect, label?: string): Disposable>
effect(execute: () => Effect, label?: string): AsyncDisposable>
```
Register a cleanup-aware effect on this fiber.
+
`execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape.
- `execute` — the effect body; see `Effect` for accepted shapes.
@@ -145,11 +190,16 @@ Register a cleanup-aware effect on this fiber.
**Returns** a disposer that tears the effect down and settles once done.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419)
+[Source](../../../vendor/cordis/src/fiber.ts#L419)
### fiber.getEffects()
-```ts website-api
+```ts cordis-catalog
+/**
+ * Return metadata for currently registered effects.
+ *
+ * @returns one {@link EffectMeta} tree per labeled live effect.
+ */
getEffects()
```
@@ -157,11 +207,17 @@ Return metadata for currently registered effects.
**Returns** one `EffectMeta` tree per labeled live effect.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L572)
+[Source](../../../vendor/cordis/src/fiber.ts#L572)
### fiber.await()
-```ts website-api
+```ts cordis-catalog
+/**
+ * Wait for current lifecycle work and rethrow startup errors.
+ *
+ * @returns this fiber, once it has settled into a stable state.
+ * @throws the config-validation or plugin-startup error, if any.
+ */
async await()
```
@@ -169,11 +225,17 @@ Wait for current lifecycle work and rethrow startup errors.
**Returns** this fiber, once it has settled into a stable state.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L701)
+[Source](../../../vendor/cordis/src/fiber.ts#L701)
### fiber.restart()
-```ts website-api
+```ts cordis-catalog
+/**
+ * Dispose and immediately reload this plugin with its current config.
+ *
+ * @returns a promise resolving once the reload settled.
+ * @throws {CordisError} `INACTIVE_EFFECT` when the fiber is already disposed.
+ */
async restart()
```
@@ -181,15 +243,27 @@ Dispose and immediately reload this plugin with its current config.
**Returns** a promise resolving once the reload settled.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L715)
+[Source](../../../vendor/cordis/src/fiber.ts#L715)
### fiber.update(config, noSave?)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Validate and apply new config, then restart the plugin.
+ *
+ * Runs the `internal/update` waterfall first, so update hooks (and HMR)
+ * can veto or replace the restart.
+ *
+ * @param config — the new raw config; validated before anything restarts.
+ * @param noSave — hint for persistence hooks not to write the change back.
+ * @returns nothing; the restart runs behind the `internal/update` waterfall.
+ * @throws {ValidationError} when the new config fails validation.
+ */
update(config: any, noSave = false)
```
Validate and apply new config, then restart the plugin.
+
Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto or replace the restart.
- `config` — the new raw config; validated before anything restarts.
@@ -197,37 +271,53 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
**Returns** nothing; the restart runs behind the `internal/update` waterfall.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L733)
+[Source](../../../vendor/cordis/src/fiber.ts#L733)
## Effect
Effect body result accepted by `ctx.effect()` and plugin startup.
+
Either a single disposer, a promise of one, or a (possibly async) iterable yielding several — generator effects register each yielded disposer as it is produced.
-```ts website-api
+```ts cordis-catalog
+/**
+ * Effect body result accepted by `ctx.effect()` and plugin startup.
+ *
+ * Either a single disposer, a promise of one, or a (possibly async) iterable
+ * yielding several — generator effects register each yielded disposer as it
+ * is produced.
+ */
type Effect =
| SyncEffect
| AsyncEffect
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L82)
+[Source](../../../vendor/cordis/src/fiber.ts#L82)
## Disposable
Function returned by an effect to release resources during disposal.
+
Disposers run in reverse registration order when the owning fiber unloads; they may be async, in which case unloading awaits them.
-```ts website-api
+```ts cordis-catalog
+/**
+ * Function returned by an effect to release resources during disposal.
+ *
+ * Disposers run in reverse registration order when the owning fiber unloads;
+ * they may be async, in which case unloading awaits them.
+ */
type Disposable = () => T
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L73)
+[Source](../../../vendor/cordis/src/fiber.ts#L73)
## EffectMeta
Tree node used to expose nested effect labels for diagnostics.
-```ts website-api
+```ts cordis-catalog
+/** Tree node used to expose nested effect labels for diagnostics. */
interface EffectMeta {
/** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */
label: string
@@ -236,13 +326,14 @@ interface EffectMeta {
}
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L95)
+[Source](../../../vendor/cordis/src/fiber.ts#L95)
## CordisError
Framework error with a stable machine-readable code.
-```ts website-api
+```ts cordis-catalog
+/** Framework error with a stable machine-readable code. */
class CordisError extends Error {
/**
* @param code — the stable error code; also the default message.
@@ -251,6 +342,7 @@ class CordisError extends Error {
constructor(public code: CordisError.Code, message?: string)
}
+/** Cordis error code definitions. */
namespace CordisError {
export type Code = keyof typeof Code
@@ -260,13 +352,14 @@ namespace CordisError {
}
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156)
+[Source](../../../vendor/cordis/src/fiber.ts#L156)
## ValidationError
Error raised when plugin configuration fails standard-schema validation.
-```ts website-api
+```ts cordis-catalog
+/** Error raised when plugin configuration fails standard-schema validation. */
class ValidationError extends TypeError {
name = 'ValidationError'
@@ -279,4 +372,4 @@ class ValidationError extends TypeError {
}
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L18)
+[Source](../../../vendor/cordis/src/fiber.ts#L18)
diff --git a/website/zh-CN/api/cordis/registry.md b/docs/cordis-catalog/core/registry.md
similarity index 70%
rename from website/zh-CN/api/cordis/registry.md
rename to docs/cordis-catalog/core/registry.md
index 55f6d666e5..2772dca723 100644
--- a/website/zh-CN/api/cordis/registry.md
+++ b/docs/cordis-catalog/core/registry.md
@@ -1,4 +1,5 @@
-
+
# Registry
@@ -6,11 +7,22 @@ Plugin loading and dependency injection.
### ctx.inject(deps, callback)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Run a callback once the requested services are available.
+ *
+ * Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback
+ * is unloaded and re-run whenever a required service changes.
+ *
+ * @param deps — required services, as an array or a name → config map.
+ * @param callback — plugin body called with `(ctx, config)`.
+ * @returns the fiber; awaiting it settles once loading finished.
+ */
inject(deps: Inject, callback: Plugin.Function): Fiber & PromiseLike
```
Run a callback once the requested services are available.
+
Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloaded and re-run whenever a required service changes.
- `deps` — required services, as an array or a name → config map.
@@ -18,11 +30,19 @@ Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloade
**Returns** the fiber; awaiting it settles once loading finished.
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L175)
+[Source](../../../vendor/cordis/src/registry.ts#L175)
### ctx.plugin(plugin, ...args)
-```ts website-api
+```ts cordis-catalog
+/**
+ * Load a plugin in the current context.
+ *
+ * @param plugin — a function, class, or `{ apply }` object plugin.
+ * @param args — the plugin config, validated against its `Config` schema.
+ * @returns the fiber; awaiting it settles once loading finished
+ * (rejecting on config or startup errors).
+ */
plugin
(plugin: P, ...args: Spread>): Fiber & PromiseLike
```
@@ -33,18 +53,20 @@ Load a plugin in the current context.
**Returns** the fiber; awaiting it settles once loading finished (rejecting on config or startup errors).
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L184)
+[Source](../../../vendor/cordis/src/registry.ts#L184)
## Plugin
Supported plugin entrypoint shapes.
-```ts website-api
+```ts cordis-catalog
+/** Supported plugin entrypoint shapes. */
type Plugin =
| Plugin.Function
| Plugin.Constructor
| Plugin.Object
+/** Types associated with plugin entrypoints and runtime records. */
namespace Plugin {
/** Shared metadata understood by the plugin registry and related tooling. */
export interface Base {
@@ -96,16 +118,25 @@ namespace Plugin {
}
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L91)
+[Source](../../../vendor/cordis/src/registry.ts#L91)
## Inject
Service dependency declaration accepted by plugins and the `@Inject` decorator.
+
Array form requests services without intercept config. Object form maps each service name to optional intercept config for the plugin context.
-```ts website-api
+```ts cordis-catalog
+/**
+ * Service dependency declaration accepted by plugins and the `@Inject`
+ * decorator.
+ *
+ * Array form requests services without intercept config. Object form maps each
+ * service name to optional intercept config for the plugin context.
+ */
type Inject = (keyof M)[] | { [K in keyof M]?: M[K] }
+/** Utilities for normalizing plugin dependency declarations. */
namespace Inject {
/**
* Convert array/object/class-inherited inject metadata into a plain map.
@@ -118,4 +149,4 @@ namespace Inject {
}
```
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L18)
+[Source](../../../vendor/cordis/src/registry.ts#L18)
diff --git a/docs/cordis-catalog/core/service.md b/docs/cordis-catalog/core/service.md
new file mode 100644
index 0000000000..84b74f98df
--- /dev/null
+++ b/docs/cordis-catalog/core/service.md
@@ -0,0 +1,102 @@
+
+
+# Service
+
+The base class for context services. A subclass loaded as a plugin registers itself as `ctx.`.
+
+Base class for services that expose a named API on `ctx`.
+
+Subclasses call `super(ctx, name)` from their constructor. The service is registered immediately and is automatically removed with the owning fiber.
+
+[Source](../../../vendor/cordis/src/service.ts#L11)
+
+### service.name
+
+```ts cordis-catalog
+/** The service name this instance is registered under. */
+public name!: string
+```
+
+The service name this instance is registered under.
+
+[Source](../../../vendor/cordis/src/service.ts#L30)
+
+## Static members
+
+### Service.init
+
+```ts cordis-catalog
+/** Symbol key of an instance method run after construction (class plugins). */
+static readonly init: unique symbol
+```
+
+Symbol key of an instance method run after construction (class plugins).
+
+[Source](../../../vendor/cordis/src/service.ts#L13)
+
+### Service.check
+
+```ts cordis-catalog
+/** Symbol key of the availability predicate passed to `ctx.provide()`. */
+static readonly check: unique symbol
+```
+
+Symbol key of the availability predicate passed to `ctx.provide()`.
+
+[Source](../../../vendor/cordis/src/service.ts#L15)
+
+### Service.config
+
+```ts cordis-catalog
+/** Symbol key of the phantom intercept-config type parameter. */
+static readonly config: unique symbol
+```
+
+Symbol key of the phantom intercept-config type parameter.
+
+[Source](../../../vendor/cordis/src/service.ts#L17)
+
+### Service.invoke
+
+```ts cordis-catalog
+/** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */
+static readonly invoke: unique symbol
+```
+
+Symbol key of the call body making a service callable (e.g. `ctx.logger()`).
+
+[Source](../../../vendor/cordis/src/service.ts#L19)
+
+### Service.extend
+
+```ts cordis-catalog
+/** Symbol key of the helper deriving an extended service instance. */
+static readonly extend: unique symbol
+```
+
+Symbol key of the helper deriving an extended service instance.
+
+[Source](../../../vendor/cordis/src/service.ts#L21)
+
+### Service.tracker
+
+```ts cordis-catalog
+/** Symbol key of the tracker metadata used for context tracing. */
+static readonly tracker: unique symbol
+```
+
+Symbol key of the tracker metadata used for context tracing.
+
+[Source](../../../vendor/cordis/src/service.ts#L23)
+
+### Service.resolveConfig
+
+```ts cordis-catalog
+/** Symbol key of the intercept-config resolution helper below. */
+static readonly resolveConfig: unique symbol
+```
+
+Symbol key of the intercept-config resolution helper below.
+
+[Source](../../../vendor/cordis/src/service.ts#L25)
diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md
index 1dc0a86b0a..4cdef11bfd 100644
--- a/docs/cordis-catalog/events.md
+++ b/docs/cordis-catalog/events.md
@@ -3,11 +3,11 @@
# Cordis Events Catalog
-Every cordis event a plugin can listen to: exact signature, dispatch mode, and the declaration's JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.
+Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.
-This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them.
+This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.
-The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.
+The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).
Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).
@@ -18,156 +18,356 @@ Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `n
A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
```ts cordis-catalog
+/**
+ * A fully configured agent and live session were published. Setup is
+ * composition-only; `agent/session-start` is the first startup-driving seam.
+ * Synchronous listener failure vetoes publication, while returned-promise
+ * rejection is reported. Detach requested during dispatch waits until every
+ * creation listener has observed the stable entry.
+ * @param agent - the newly registered agent with its live session and completed setup.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode emit
+ */
'agent/created'(this: Scoped, agent: Agent): void
```
-Types: [Agent](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:151`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts)
### `agent/disposed` — emit
An agent left the registry; AgentLoop emits this after driver quiescence but before session detachment and scoped-registration unwind. Custom registry users own their driver-ordering contract.
```ts cordis-catalog
+/**
+ * An agent left the registry; AgentLoop emits this after driver quiescence
+ * but before session detachment and scoped-registration unwind. Custom
+ * registry users own their driver-ordering contract.
+ * @param agent - the exact agent removed from the registry.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode emit
+ */
'agent/disposed'(this: Scoped, agent: Agent): void
```
-Types: [Agent](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:160`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:159`](../../packages/core/agent/src/types.ts)
### `agent/error` — emit
A step or turn errored. The loop reports a failure here (plus the logger) even when the error has no in-turn position for a session `error` event.
```ts cordis-catalog
+/**
+ * A step or turn errored. The loop reports a failure here (plus the logger)
+ * even when the error has no in-turn position for a session `error` event.
+ * @param agent - the agent whose turn errored.
+ * @param turn - the turn in which the failure surfaced.
+ * @param step - the step at which the failure surfaced.
+ * @param error - the failure, verbatim.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode emit
+ */
'agent/error'(this: Scoped, agent: Agent, turn: number, step: number, error: Error): void
```
-Types: [Agent](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:295`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:315`](../../packages/core/agent/src/types.ts)
+
+### `agent/post-step` — serial
+
+Awaited serial checkpoint after the response, real or synthetic tool results, injected context, and steering are durable but before `step/end`. A cancelled tool batch reaches this checkpoint with an aborted signal.
+
+```ts cordis-catalog
+/**
+ * Awaited serial checkpoint after the response, real or synthetic tool
+ * results, injected context, and steering are durable but before `step/end`.
+ * A cancelled tool batch reaches this checkpoint with an aborted signal.
+ * @param agent - the agent whose step is settling.
+ * @param turn - the open turn number.
+ * @param step - the open step number.
+ * @param signal - the turn abort signal.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode serial
+ */
+'agent/post-step'(this: Scoped, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise | void
+```
+
+Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
+
+Source: [`packages/core/agent/src/types.ts:267`](../../packages/core/agent/src/types.ts)
### `agent/pre-step` — serial
-Awaited serial checkpoint for session-surface mutation after prompt assembly and before `step/start`; appends land outside the pending step. The loop derives history once afterward, so compaction records and replacements are included without rewriting an assembled request. The prompt and prefix are the exact pressure inputs for that request, and `signal` cancels listener work. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+Awaited serial checkpoint before `step/start`; appends land outside the pending step and are included when the loop derives request history. `signal` cancels listener work. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
```ts cordis-catalog
-'agent/pre-step'(this: Scoped, agent: Agent, turn: number, step: number, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal): Promise | void
+/**
+ * Awaited serial checkpoint before `step/start`; appends land outside the
+ * pending step and are included when the loop derives request history.
+ * `signal` cancels listener work.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @param agent - the agent opening the step.
+ * @param turn - the open turn number.
+ * @param step - the pending step number.
+ * @param signal - the turn abort signal.
+ * @mode serial
+ */
+'agent/pre-step'(this: Scoped, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise | void
```
-Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:214`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:207`](../../packages/core/agent/src/types.ts)
### `agent/prompt-submit` — waterfall
-Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default.
+Allow, rewrite, or block one claimed prompt before it becomes a user message. Call `next()` for the unchanged default.
```ts cordis-catalog
+/**
+ * Allow, rewrite, or block one claimed prompt before it becomes a user
+ * message. Call `next()` for the unchanged default.
+ * @param agent - the agent whose turn claimed the message.
+ * @param content - the claimed message's blocks, as queued.
+ * @param source - the message's resolved source.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode waterfall
+ */
'agent/prompt-submit'(this: Scoped, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise): Promise
```
-Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:224`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:217`](../../packages/core/agent/src/types.ts)
### `agent/queued` — emit
Detached, frozen content entered the agent's inbox. Source defaults have already been applied, so these are the exact values retained for the log.
```ts cordis-catalog
+/**
+ * Detached, frozen content entered the agent's inbox. Source defaults have
+ * already been applied, so these are the exact values retained for the log.
+ * @param agent - the agent whose inbox received the message.
+ * @param content - the accepted content blocks retained by the inbox.
+ * @param info - the accepted source plus whether it entered as steering.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode emit
+ */
'agent/queued'(this: Scoped, agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void
```
-Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:179`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:178`](../../packages/core/agent/src/types.ts)
### `agent/request` — waterfall
Replace the frozen call configuration. Model-visible content must use logged channels; this seam cannot mutate messages. Injection here joins the next request because the current step boundary is already fixed.
```ts cordis-catalog
+/**
+ * Replace the frozen call configuration. Model-visible content must use
+ * logged channels; this seam cannot mutate messages. Injection here joins
+ * the next request because the current step boundary is already fixed.
+ * @param agent - the agent making the model call.
+ * @param turn - the open turn number.
+ * @param step - the step whose request this is.
+ * @param config - the config the loop would use (frozen); return a replacement to switch.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode waterfall
+ */
'agent/request'(this: Scoped, agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise): Promise
```
-Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:236`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:229`](../../packages/core/agent/src/types.ts)
+
+### `agent/request-error` — waterfall
+
+Recover a model-request failure after its failed step has closed. `retry` opens a new numbered step; `fail` preserves the original request error. Call `next()` to delegate to the next recovery listener or the default.
+
+```ts cordis-catalog
+/**
+ * Recover a model-request failure after its failed step has closed. `retry`
+ * opens a new numbered step; `fail` preserves the original request error.
+ * Call `next()` to delegate to the next recovery listener or the default.
+ * @param agent - the agent whose request failed.
+ * @param turn - the open turn number.
+ * @param step - the failed step number.
+ * @param error - the original model-request failure.
+ * @param failure - serializable facts normalized at the final adapter boundary.
+ * @param priorFailures - immutable failures that already authorized another request in this consecutive sequence.
+ * @param signal - the turn abort signal.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode waterfall
+ */
+'agent/request-error'(this: Scoped, agent: Agent, turn: number, step: number, error: RequestError, failure: LlmFailure, priorFailures: readonly LlmFailure[], signal: AbortSignal, next: () => Promise): Promise
+```
+
+Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
+
+Source: [`packages/core/agent/src/types.ts:282`](../../packages/core/agent/src/types.ts)
### `agent/session-prefix` — waterfall
-Compose request-only messages placed before derived history. The frozen result is computed once per loop instance, logged on its anchoring request header, and reused so the provider prefix remains stable. Interrupted composition is discarded. Composition precedes the first `agent/pre-step` and request boundary, so listener appends join the current request and pressure accounting sees the composed prefix. Changing context belongs in history; contributors should prepend to `await next()` to preserve registration order. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+Compose request-only messages placed before derived history. The frozen result is computed once per loop instance, logged on its anchoring request header, and reused so the provider prefix remains stable. Interrupted composition is discarded. Composition precedes the first `agent/pre-step` and request boundary, so listener appends join the current request. Changing context belongs in history; contributors should prepend to `await next()` to preserve registration order. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
```ts cordis-catalog
+/**
+ * Compose request-only messages placed before derived history. The frozen
+ * result is computed once per loop instance, logged on its anchoring request
+ * header, and reused so the provider prefix remains stable. Interrupted
+ * composition is discarded. Composition precedes the first `agent/pre-step`
+ * and request boundary, so listener appends join the current request.
+ * Changing context belongs in history; contributors should prepend to
+ * `await next()` to preserve registration order.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @param agent - the agent whose session prefix is being composed.
+ * @param prefix - the frozen seed; return an extended replacement.
+ * @param signal - aborts composition when the step is torn down.
+ * @mode waterfall
+ */
'agent/session-prefix'(this: Scoped, agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise): Promise
```
-Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:251`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:244`](../../packages/core/agent/src/types.ts)
### `agent/session-start` — emit
The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
```ts cordis-catalog
+/**
+ * The session lifecycle began, once before the first turn. Use
+ * `agent.inject()` to seed model-facing context. This is a notification, not
+ * a veto; disposal requested by a lifecycle owner is rechecked before the
+ * driver starts.
+ * @param agent - the agent whose session lifecycle began.
+ * @param source - why the session started (fresh startup, resume, …).
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode emit
+ */
'agent/session-start'(this: Scoped, agent: Agent, source: SessionStartSource): void
```
-Types: [Agent](../core-data-structures/core.md) · [SessionStartSource](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md)
-Source: [`packages/core/agent/src/types.ts:192`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:191`](../../packages/core/agent/src/types.ts)
### `agent/status` — emit
Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does not enter `running` synchronously; drive lifecycle from this event.
```ts cordis-catalog
+/**
+ * Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does
+ * not enter `running` synchronously; drive lifecycle from this event.
+ * @param agent - the agent whose status flipped.
+ * @param status - the status just entered (the transition's destination).
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode emit
+ */
'agent/status'(this: Scoped, agent: Agent, status: AgentStatus): void
```
-Types: [Agent](../core-data-structures/core.md)
+Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
-Source: [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/types.ts)
+Source: [`packages/core/agent/src/types.ts:168`](../../packages/core/agent/src/types.ts)
### `agent/step-result` — waterfall
Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …).
```ts cordis-catalog
+/**
+ * Waterfall: post-process the assembled assistant {@link Message} before
+ * tool dispatch (validation, content rewriting, …).
+ * @param agent - the agent that received the step's response.
+ * @param turn - the open turn number.
+ * @param step - the step that produced the message.
+ * @param message - the assistant message as assembled from the stream.
+ * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
+ * @mode waterfall
+ */
'agent/step-result'(this: Scoped, agent: Agent, turn: number, step: number, message: Message, next: () => Promise