The zh.md counterpart (section-for-section mirror per the i18n contract:
identical heading structure, byte-identical text fences, same link targets)
plus the recorded i18n.yaml and the language-switcher lines on both sides —
matching the approval / sandbox / env-state RFC practice.
Re-audited against origin: master unmoved; approval seam still on
feat/sandbox-support; three relevant branches appeared since drafting.
- env-state-visibility RFC (fold-from-log + boundary application for env
facts): adopt its boundary-narration principle for user-driven mode flips
(one coalesced notice when the flushed mode differs from the last request
header's fold; net-zero silent; tool exits self-narrate via result), and
resolve the one real collision — its config-phase sketch routes ACP
session/set_mode to env facts while this RFC claims the picker. Proposed
division: picker-to-modes / knobs-to-config-options; a mode definition may
later bundle env facts; second lander amends. Also note the source-field
contrast (env drift has no log-adjacent cause; every mode/set does).
- sandbox-escalation: first live approval composition — named as the
precedent our recorded scenarios follow.
- ask-user-question / user-interaction seam: the stdio approval answerer
rides its one-prompt-owns-stdin queue where mounted.
- Config: pin the allowlist as the degenerate form of a per-tool
allow|deny|ask map so execution-phase ask policies arrive without a
config migration (deferred on approval's allow_always).
- Dedup the ACP mapping (one home: Protocol and UI surfaces).
Adds the Detailed design block: the mode/set vocabulary (no provenance
field — cause is log-adjacent; bare-string mode names), ModeConfig with an
explicit resolve step, the cursor-cached fold + ctx.modes service with
turn/start|step/end pending flush, the computed mode:policy section (order
50) + post-next() assembly filter (exit_plan_mode visible IFF plan mode —
what keeps default-mode assemblies byte-identical), the fold-only gate with
its ask on exit_plan_mode (no dsh-approval dependency — the registry
routes), the exit tool contract and render intent, dependency edges and the
stdio/ACP surfaces, the input.json setMode op + plan-mode/plan-mode-reject
scenarios (mode set before turn 1 → the initial header snapshot is already
in plan shape; the widening delta appears at exit), and the mechanical tail.
Consistency fixes: the plan allowlist names real tools (no grep tool
exists); the re-widen risk now states the post-next() mechanism instead of
a listener-order convention.
A session mode is a named, logged, per-agent policy state: mode/set as a
log-only SessionEventMap member (todo/write shape), a foldMode over the log,
soft enforcement at system-prompt/assemble (filtered schemas + mode section,
auto-logged as request/header-delta), hard enforcement at tools/pre-execute
(deny-by-default against the mode allowlist), and a thin ctx.modes service
with turn-boundary pending-intent flush for user flips. One new product
package dsh-mode (packages/mode/mode, the approval-group shape); plan is the
only shipped definition.
Lands as ONE feature — a plan mode without a model-driven, approvable exit is
not a smaller version of the feature. The roadmap's two stages are build-and-
review order for a single stacked landing: the mode core, then exit_plan_mode
(ask-gated through the approval seam; the plan text rides the tool args as
the reviewable log artifact), the stdio readline answerer, and the ACP
session-mode surface (session/set_mode, current_mode_update, available
modes). Hard prerequisite: the approval seam lands first; the stack bases on
feat/sandbox-support meanwhile.
Residuals from the Codex re-review:
1. A system delta's insert was flattened to one token, so deltas differing
only in inserted-line count compared equal. Now one {{system}} token per
inserted line — position AND extent survive, content does not.
2. The live uniformity guard folded only request/header snapshots, so a
mid-run header CHANGE (request/header-delta) could diverge from the pin
invisibly. Non-pinning runs now assert zero header-delta events: a
scenario that legitimately changes its header mid-run exists to show
that change, so it must pin (fail-loud until it does).
Codex review findings on the pinned-header change:
1. scrubRequestHeaders flattened a request/header-delta's whole
system/tools payload to one token, so two meaningfully different deltas
compared equal. Now the structural facts survive — keepStart/keepEnd
line positions, added/removed/changed tool NAMES — and only the bulk
(inserted prompt lines, schema bodies) is tokenized.
2. The one-pin design rested on an unasserted premise (all sessions
compose the same header). Every non-pinning scenario now asserts, live,
that each request/header its run produces equals the pinned fixture's
header (both sides normalized against their own volatile values), so a
session-dependent header fails loud until it gets its own pin.
Verified the guard bites: perturbing the pinned fixture's prompt fails
a non-pinned scenario with the intended message.
3. RFC de-slopped per docs/AGENTS.md: no PR reference, no SHOULD
spec-speak; Decision/Verification/Consequences updated for 1 and 2.
Every session.jsonl fixture embedded the full composed system prompt and
complete tool-schema list in its request/header event (~8 KB on one line,
identical across the suite), so any prompt or tool-schema edit forced a
re-record or hand-edit of every fixture — see the dynamic-workflows PR for
the churn pattern this removes.
Now exactly one scenario (text-turn, flagged pinsHeader) commits and
compares that content verbatim; every other fixture stores and compares it
as {{system}}/{{tools}} tokens via the new pure scrubRequestHeaders
normalizer (applied to both compare sides and to record-mode writes, so a
re-record cannot reintroduce the content). request/header-delta payloads
are scrubbed the same way; config/reason stay verbatim — a model swap
SHOULD churn every fixture, a prompt edit should not. Replay is unaffected:
script derivation reads only assistant/chunk events.
Fixture meta-guards enforce the split: non-pinning fixtures must be fixed
points of the scrub, the pinning fixture must not be, and exactly one
scenario pins. Committed fixtures migrated through the same function.
Docs: pinned-header RFC (implemented/testing), base snapshot RFC + testing
policy + llm-replay module doc/README updated.
Review findings on the config catalog:
The schema-subset check compared only top-level z.object keys against
top-level type members, so a nested loader-accepted key (agents[].id,
agentOptions.model, capabilities.*) missing from the declared type would
pass the gate unseen. The walk now collects nested object/array
compositions as key paths and resolves each against the declared config
type — through interfaces (heritage included), aliases, literals,
intersections, unions, arrays, indexed access, Partial-style wrappers, and
type references across package-local and workspace imports (re-export
chains included). The check stays presence-only and one-directional, and
only a definite miss is a violation: a path crossing a type the walk
cannot enumerate (an external package's) is skipped, never mis-reported.
The recursion guard applies at named declarations only — a structural
first child shares its span start with its parent, so a span-keyed guard
on every node mistakes ordinary descent for a cycle and silently turns
definite misses into unknowns.
The page and RFC framing also overstated the catalog as the exact
cordis.yml-settable surface: the paste is the plugin's full declared
config type, and a field the runtime schema deliberately excludes (the ACP
bridge's test-injected stream) is a runtime-only seam its own JSDoc marks.
Both now say so.
Five spec cases pin the new behavior: nested hidden key, workspace
intersection via star re-export, Partial wrapper, indexed-access
composition, and the external-type unknown path.
A directory holding exactly one file adds a path level for nothing;
cordis-catalog/ keeps its folder because it holds two sibling pages
(events.md + services.md), matching config-catalog.md which was born flat.
docs/tool-catalog/tools.md -> docs/tool-catalog.md and
docs/persistence-catalog/log-events.md -> docs/persistence-catalog.md; their
generators' OUT paths and in-page relative links drop one directory level,
and every citation repo-wide (docs, RFCs, package READMEs, generator
headers) is updated. graph-atlas.md, config-catalog.md, and the doc graphs
regenerate with the new paths.
scripts/gen-config-catalog.ts walks every packages/<group>/<pkg> entry with
the TypeScript compiler API and emits docs/config-catalog.md: per loadable
plugin, the verbatim config declaration (JSDoc included) its apply/constructor
receives in a ts config-catalog fence, the inject requirements, resolved links
for every referenced type (package-local types pasted transitively, other
plugins' config types as intra-page anchors, LINK_MAP names to
core-data-structures, workspace types to source), and terse classification
lists for config-free plugins, abstract seams, and libraries — classification
is total, so a new package cannot go undocumented.
The walk enforces per-field JSDoc prose on every pasted declaration and
statically cross-checks the schemastery schema (z.object keys, z.intersect
composition across packages): every schema-validated key must be a declared
member of the config type. One violation existed repo-wide — the agents[].id
field in dsh-agent-loop — fixed by adding its JSDoc (which shifts the
cordis-catalog services page's source pointers; regenerated).
verify-config-catalog (--check) joins doc-sync; doc-typecheck learns the
ts config-catalog fence; gen-cordis-catalog exports its JSDoc/pointer helpers
and LINK_MAP for reuse. Negative-path spec in
packages/core/agent-core/tests/gen-config-catalog.spec.ts mirrors the
gen-cordis-catalog spec. Decision record:
docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md (includes
the deliberate acceptance of README ## Config overlap).
The compact-basic README's opening line still described summarization
as routed through the agent request pipeline — corrected to the direct
one-shot llm/stream contract the rest of the package documents. The
proposed Code Mode RFC gets a premise-stale note: it names the old
full-request agent/request seam, which now shapes call config only —
re-map onto the log channels and system-prompt/assemble before
implementing from it.
Codex diff review, round 1, two (A) findings:
- The agent/request fallback resolved the RAW seed object — on later
steps the session's cached header fold — so a delegating listener
(await next(), mutate, return) could rewrite the fold in place and
the change would compare as already-baseline: no delta logged, the
persisted log unable to reconstruct the request (the dev invariant
would fire on the divergence, but the log would still lie). One
structuredClone'd, deep-frozen seed now serves both the listener
chain and the fallback — in-place shaping after delegation throws —
and Session.requestHeader() freezes its fold on update, so the leak
class is unrepresentable from either side. Pinned by a loop-level
delegating-mutator test.
- Doc sweep for the old contract: agent README's event row (mutate
GenerateOptions / tool filtering → frozen config seed, replacement
out, logged header), compact-basic's module JSDoc (summarize routed
through agent/request → direct one-shot at llm/stream), and
architecture.md's event-domain line (request mutation → call-config
shaping).
The reconstructability RFC is the principle's home: model-visible ⟺
logged in both forms, the mechanism (boundary derivation + header
fold), the enforcement (write-time round-trip guard, the dev
invariant), the corollaries ranked (prefix-cache stability first), the
MiniCode lineage with the provenance arrow inverted, and the
alternatives it beat — including the stateful transmission client
whose three-design archaeology lives in PR #162.
Placements per the one-home-per-fact taxonomy: a standing-order line in
root AGENTS.md (with displacement trims to stay inside the 1,575-word
ceiling), the principle statement in architecture.md § Session Log and
its Turn Flow lines (condensed to the ratcheted 1,630 ceiling), the
request-envelope section in core-data-structures/core.md with the
LlmCallConfig paste, both review-requested FIXMEs
(FIXME(call-config-shape) beside the type, FIXME(catalog-verbs) at the
catalog's drift-gate note), cookbook rows redirected off agent/request
(tool filtering → system-prompt/assemble, plan-mode prompt → sections/
inject()), and the llm/stream JSDoc stating the frozen-request
contract. RFC index and all generated catalogs regenerated.
compact/summary gains { model, maxTokens? } — the envelope the
summarize call actually used, reported by the backend that made the
call: summarize() now returns { summary, model, maxTokens? } instead of
bare blocks, so an overriding backend (template or remote summarizer)
reports its own envelope honestly and compactRegion logs it. 'Which
model wrote this summary' becomes answerable from the log alone, and
the one-shot summarize request — outside the loop's header-event fold
by design — is reconstructable from log + code (the reconstructability
RFC's scope statement).
The loop is now transmission-stateless; a request is a pure function of
(session log, this step's rendered assembly, current AgentOptions):
- The reconstruction boundary is step/start: the messages snapshot is
taken in the same synchronous frame immediately before the step/start
append, so the request's messages are exactly the derivation over
events[0..stepStartSeq) — an inject() from an agent/request listener
(or any concurrent task) lands after the boundary and joins the NEXT
request. This changes behavior for a synchronous step/start
session/event listener that appends content (master derived after the
append, so such a listener could reach the current request):
agent/pre-step is the sanctioned seam for current-request content.
- agent/request is re-typed to config-only: (agent, turn, step,
config: LlmCallConfig, next) → LlmCallConfig. The frozen seed comes
from AgentOptions on a loop instance's first request (explicit options
beat the logged baseline — fork overrides and resume reconfiguration
stay correct) and from the log's folded header afterwards; listeners
return a replacement to switch. Content shaping through the request is
no longer expressible — model-visible content flows through the log
channels.
- recordRequestHeader appends whatever header event the request owes the
log before dispatch: an 'initial'/'resume' snapshot anchoring each
loop instance, a round-trip-verified delta on change, a 'fallback'
snapshot when the encoding cannot express it. Session.requestHeader()
is the log's incrementally-folded baseline.
- Requests are deep-frozen before dispatch (deepFreeze exempts the
AbortSignal — freezing one breaks AbortController.abort() outright);
frozen + sessionId is the loop-built marker the dev invariant keys on.
Ported from #162 and re-anchored on the log: the append-extension /
frozen-end-to-end / compaction-resend / prompt-change property tests,
plus new specs for the boundary semantics, resume anchoring, and the
end-to-end theorem (every recorded request rebuilds byte-equal from the
log alone). Live cache-hit e2e (request-cache.e2e.ts) verified against
the real DeepSeek API. Snapshot goldens intentionally stale until the
single re-record after the compact/summary envelope lands.
deriveMessages() no longer re-derives and re-clones the whole surface
per call: each node is projected exactly once, when first seen (O(new
nodes) per access), through the now-public deriveEventMessage — the
per-node pure function external reconstructors and the dev invariant
fold over a log prefix, so no two paths can disagree about a request's
messages. A surface rewrite (replace, invalidate) rebuilds, signalled
by SurfaceManager.replaceGeneration — monotonic, bumped by every folded
replace and by invalidate(), never reset. Callers get a fresh array
snapshot per call over SHARED deep-frozen messages: isolation of the
append-only log moves from per-call cloning to unrepresentability
(mutation throws), which the two isolation tests now pin directly.
Every conversation request's non-content half (system prompt, tool
schemas, call config — the EpochHeader) is now recorded in the session
log: a 'request/header' full snapshot (reason 'initial' | 'resume' |
'fallback') anchors the fold at conversation birth and process
boundaries, and 'request/header-delta' events (system line-trim,
name-keyed tools delta, whole config) encode mid-run changes. The pure
trio — foldRequestHeader / diffHeader / applyHeaderDelta — reconstructs
the header any request was built under from the log alone; the writer
contract round-trip-verifies every delta with a 'fallback' snapshot
when the encoding cannot express a change (pure tool reordering), so a
well-formed log always folds cleanly. Canonical absence: empty system
and empty tools normalize to absent fields, matching request builds.
Persistence and cordis catalogs regenerated; SessionEventMap paste and
EpochHeader added to the core-data-structures session page.
The summarization request no longer dispatches agent/request — that seam
shapes the loop's conversation requests; a hand-built one-shot's
interception surface is llm/stream, like every other direct call. The
model comes from summarizationModel falling back to the agent's own. The
turn/step parameters existed on the compact seam only to feed the
agent/request dispatch and leave compactIfNeeded/compactRegion.
Groundwork for making every conversation request a pure function of the
session log (reconstructability RFC, this branch): the seam split keeps
the loop's request path as the single thing the log must explain.
Ported from worktree-prompt-prefix-stability (PR #162) where it was
review-converged; catalog and producer/consumer graphs regenerated.
Review comment on the verify step: dumping every open PR under
--limit 1000 wastes agent context and re-truncates past 1000. Ask the
exact question server-side instead: gh pr list --state open
--base <branch> --json number --jq length, whose output is a single
count that is exact at any number of open PRs. Skill, checklist, and
the cookbook dependent-check line all use the per-branch form.
Codex review caught three procedure defects: the dependent check used
bare 'gh pr list', whose default pagination can hide an open dependent
and make branch deletion look safe (now --state open --limit 1000); the
refresh step merged local master, which 'gh pr merge' leaves stale (now
fetch and merge origin/master); and the quick checklist omitted pushing
the dependent after the local merge.
Commit the stacked-PR landing procedure as a repo skill so every
collaborator's agent discovers it: merge bottom-up, retarget and refresh
each dependent, and delete no branch until the whole stack has landed
(deleting a base branch auto-closes the open PR that bases on it).
The responding-to-pr-review-on-a-stack cookbook guide's dependent-check
step links the skill as the full landing procedure.
ds-review-bot round 2, both warnings:
- subagent/provider-removed now routes through emitLifecycle (per-listener
containment, the subagent/start|end precedent) instead of raw ctx.emit,
whose dispatch halts on the first throw: a throwing subscriber can no
longer starve a later mirror into keeping a stale tool, nor disrupt the
backend fiber's teardown mid-disposer. provider-added deliberately keeps
propagation (register-time rollback semantics, like the system-prompt
registries); the asymmetry is documented on emitLifecycle, the event
JSDoc, and the provider-lifecycle RFC.
- The documented model-via-agent/request fallback composes with a
{{model}} persona via the ownership rule itself: the plugin supplying
the model late states it early on the system-prompt/assemble waterfall.
Declined re-ordering render after agent/request — it would break the
agent/pre-step contract (compaction must measure the prompt the model
sees). New loop test pins the supply path end-to-end; the RFC's
{{model}} consequence bullet now covers supply as well as switch.
This branch's two RFCs (prompt-variables, provider-lifecycle events)
conform to the new contract in the same merge: 'Status: implemented'
dateless header, 'Rejected alternatives' -> 'Alternatives considered',
'What we give up' folded into '## Consequences' (per the uniform-format
RFC's own rejection of a costs-only closer), and the index regenerated
into the new INDEX.md. docs/rfc/README.md resolved to master's rewrite.