Commit Graph
60 Commits
Author SHA1 Message Date
Tianyi Cui 3633cf90c8 fix(review): contain provider-removed listener failures; pin the model-via-request path
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.
2026-07-06 00:34:47 +08:00
Tianyi Cui 3f83a4ee96 review: the persona becomes the system-prompt plugin's deployment config
Review round 2 (tianyicui inline comments):

- dsh-system-prompt itself registers the harness:identity (-100) and
  deployment:persona (0) sections — they must survive a swapped loop
  plugin, so they leave dsh-agent-loop; the persona text is the plugin's
  own validated 'persona' config. The model/cwd variables STAY on the
  loop: runtime facts of the agents it drives.
- AgentOptions.systemPrompt is deleted with all its forwarding plumbing:
  the app configs' systemPrompt keys become 'persona' routed through
  dsh-agent-core (schema = z.intersect of the owners'), the ACP bridge
  and tool-subagent stop carrying persona configuration, and subagent
  children now render the deployment persona like every other agent.
- Example personas drop transport/interface trivia (ACP, CLI) — facts
  irrelevant to the model.
- Root CONTEXT.md removed (not idiomatic); its persona definition was
  wrong under the new ownership anyway.
- Docs, READMEs, the prompt-variables RFC, and generated catalogs
  updated; new loop test pins the assemble-waterfall escape valve
  (an emptied assembly sends NO system field).
2026-07-05 23:23:46 +08:00
Tianyi Cui 00cf8b693a feat(agent-loop): open every prompt with the harness identity section
A static harness:identity section at order -100 — the first occupant of
the documented negative band — states that the agent is powered by the
DeepSeek Harness SDK before the deployment's persona renders. Harness
attribution is a harness fact: it lives on the loop plugin, not in each
deployment's persona, so every agent (subagents included) carries it and
no YAML can forget it. A deployment that must drop it can remove the
section in the system-prompt/assemble waterfall.

Order-band docs updated in all five homes (PromptSection JSDoc, the
system-prompt and agent-loop READMEs, architecture.md, the RFC).
2026-07-05 11:37:32 +08:00
Tianyi Cui e85e21c8b0 fix(review): close interpolation strictness holes; make tool-subagent mirror provider lifecycle
Codex round-1 findings, both confirmed:

- renderPrompt: variable lookup now uses Object.hasOwn (an unregistered
  {{constructor}} previously resolved through Object.prototype and spliced
  function source into the prompt), and a {{ that opens no complete group
  while a }} still follows ({{{model}}}, {{a{b}}) now throws instead of
  passing or partially interpolating. A lone {{ with no }} after it stays
  verbatim; substituted values are never re-scanned.
- tool-subagent: the apply-time provider lookup assumed a load order the
  cordis Loader does not guarantee (siblings start concurrently). The seam
  now announces subagent/provider-added/-removed and the tool mirrors the
  provider's lifecycle: registers when the provider is (or becomes)
  available, unregisters when it goes away, re-derives wording on reload.
  No load-order requirement remains.
- loop.spec containment test now proves live continuation: after the
  contained render failure, a waterfall listener rescues {{cwd}} and the
  same agent completes a real model turn.

RFC/READMEs updated to the shipped contract; cordis catalog regenerated.
2026-07-05 02:42:48 +08:00
Tianyi Cui f256f3961d feat(system-prompt): prompt variables, persona-as-section, tool-guidance ownership
One principle: every fact in the assembled prompt has exactly one owner.

- dsh-system-prompt: merge-extensible AssembleContext on assemble();
  a variable(name, provider) registry; {{name}} interpolation in
  renderPrompt, strict (unknown/valueless/malformed references throw);
  duplicate section and variable names rejected; assembly carries
  resolved section text + variables through the assemble waterfall.
- dsh-agent declares AssembleContext.agent; dsh-agent-loop registers
  the agent:persona section (order 0 - identity renders before tool
  guidance) and the model/cwd variables, and drops its string join:
  renderPrompt(assembly) IS the full prompt.
- Tool guidance moves to its owners: descriptions carry per-tool
  semantics; sections only cross-call habits (tool:bash exit-code
  habit at order 105; read's not-shell nudge). todo/subagent need no
  section - their descriptions already carry the contract.
- SubagentProvider.inheritsParentContext (spawn/acp false, fork true);
  dsh-tool-subagent derives truthful per-provider wording and resolves
  the provider at load (backend must be listed first).
- Example personas shrink to identity + behavior with {{model}} (and
  {{cwd}} in the ACP tree); the welcome banner stops enumerating tools.

RFC: docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
2026-07-05 01:54:46 +08:00
Tianyi Cui 582caee90a Merge remote-tracking branch 'origin/master' into simpl-f-steering-mirror
# Conflicts:
#	docs/cordis-catalog/events-and-services.md
#	packages/core/agent/src/types.ts
2026-07-04 20:44:34 +08:00
Tianyi Cui a29bbe1453 Add JSDoc completeness gate for the cordis surface
gen-cordis-catalog now hard-errors (aggregated, not fail-fast) when an
event lacks description prose or a payload @param, or a public service
method lacks JSDoc, a @param per parameter, a @returns on a non-void
result, or an explicit return type annotation. The this receiver and the
trailing waterfall next are exempt on events (mode machinery owned by
@mode); a stale @param naming no real parameter errors, mirroring the
@mode contradiction check. parseJsDoc now ends prose at the first block
tag (standard JSDoc semantics), so the tags never change the rendered
catalog — only Source: line pointers moved.

Fills the ~139 gaps found across the 15 surface files, extends the spec
with negative-path fixtures for every new guard plus the exemptions,
records the decision as an implemented process RFC, and extends the
AGENTS.md typed-events bullet with the authoring rule. Runs inside
verify-cordis-catalog -> doc-sync, so CI and pre-push enforce it with
zero new wiring.
2026-07-04 19:06:35 +08:00
Tianyi Cui c6d2eeea6b refactor(events): remove the agent/steering mirror emit
agent/steering was the last transient mirror of a durable session event:
drainSteering appended the durable steering/message {turn, content, source}
and emitted the identical fact one line later. Zero production listeners
existed — every steering consumer (hook bridges, goldens, deriveMessages)
reads the durable event — and the one regression test subscriber asserted
a fact the log already records.

Remove the declaration (dsh-agent types + JSDoc list + README row), the
emit in drainSteering (its ctx parameter goes too), and the emit line in
the loop-pseudocode blocks (loop.ts module doc, architecture.md); the
cordis catalog is regenerated. The regression test now pins source
preservation on the durable steering/message event. Live-notification
needs keep their surviving homes: agent/queued at enqueue time,
session/event at drain time.

RFC: docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md
(moved from proposed/, amended to shipped reality). The three implemented
RFCs that stated the retention — the boundary-mirror removal, the
stream-chunk removal, and event-domain-semantics — are amended to point
at that RFC as the record of the removal, per implemented/AGENTS.md.
The rejected retire-mid-turn-steering RFC keeps its frozen text (it
records the declined proposal); the steering capability itself —
steer(), the durable event, continuation forcing — is untouched.
2026-07-04 15:36:40 +08:00
Tianyi Cui c3424e5487 fix(docs): address Codex review round 1 on the AGENTS.md rewrite
- Restore the universal JSDoc rule the rewrite dropped (module doc
  comment + semantic JSDoc on every export), in root AGENTS.md § Type
  safety and documentation — the generated-catalog RFC cites it as the
  rule the generator enforces at the source.
- Repoint the six remaining citations of moved content that the
  section-name grep missed (rule-title quotes and prose references):
  agent-loop agent.ts, acp index.ts, acp turns.spec.ts, the Exa e2e
  header, the real-api-e2e RFC, the doc-sync-enforcement RFC amendment,
  and rfc/implemented/AGENTS.md's section-name casing.
- Fix two docs/testing.md overstatements: the unit tier also runs
  examples/*/tests specs, and keyless-by-nature examples have no
  with-key smoke.
- Displacement trims keep root AGENTS.md at 1,498/1,500.
2026-07-04 15:11:49 +08:00
Tianyi Cui 81c27bed36 Merge branch 'worktree-hooks-f-bridges' into worktree-hooks-g-stream-chunk
# Conflicts:
#	docs/rfc/README.md
2026-07-04 03:21:00 +08:00
Tianyi Cui 9033837081 Merge branch 'worktree-hooks-b-bash-seam' into worktree-hooks-c-interception
# Conflicts:
#	docs/architecture.md
#	docs/cordis-catalog/events-and-services.md
#	docs/core-data-structures/tools.md
#	packages/core/tools/README.md
#	packages/core/tools/src/index.ts
2026-07-04 02:44:26 +08:00
Tianyi Cui c5ed5ff490 Merge remote-tracking branch 'origin/master' into worktree-hooks-a-taxonomy
# Conflicts:
#	docs/rfc/README.md
2026-07-04 02:15:52 +08:00
Tianyi Cui d8fd3225af feat(tool-fs): result-time applied-hunk diffs for write/edit
fs write/edit now emit a result-time contextual-diff tool_call_update
(the applied hunk with ±3 context lines, one hunk per replace_all site),
matching what claude-agent-acp sends and what makes an editor render the
change in place. The call-time snippet diff stays; the result hunk
supersedes it (ACP content-replace).

Mechanism:
- A persisted tool-private `meta` channel: execute may return
  `{ content, meta }`; `meta` (JsonValue) rides on the tool/result event
  and is handed back to presentResult, so the diff reproduces on replay
  (event-sourced). JsonValue is now exported from dsh-session.
- The backend returns raw before/after text (storage facts) on
  FsWriteOutcome/FsEditOutcome; the tool computes the hunk via the npm
  `diff` package's structuredPatch. A create has no before → no result
  diff; a failed/aborted mutation carries no meta.
- ToolResultView gains a DiffResultView; the bridge's result-side switch
  renders it as {type:'diff'} content blocks.

RFC: docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md
(justifies the npm `diff` runtime dep over vendoring and the meta channel);
the render-intent-union RFC's Non-goal is updated to record this shipped.
All fs snapshot goldens re-recorded; edit/overwrite gain the contextual
result diff, create/read/policy-reject unchanged in structure.
2026-07-03 17:12:00 +08:00
Tianyi Cui e100c52154 docs(agent): drop agent/stream-chunk from the two package READMEs
Codex convergence caught stale prose the doc-sync gate does not scan: the
dsh-agent README still listed `agent/stream-chunk` as an emit and the
dsh-agent-loop README still told UI plugins to listen on it. A plugin author
following either would silently get no chunks. Point both at the
`session/event` `assistant/chunk` feed (the agent-loop UI line also names the
`agent/*` control events a UI still uses).
2026-07-03 00:06:39 +08:00
Tianyi Cui b84d4828a8 refactor(events): remove the agent/stream-chunk mirror of assistant/chunk
The loop recorded every model token delta as a durable `assistant/chunk`
session event AND emitted an identical live `agent/stream-chunk` Cordis event
one line later. Same StreamChunk, same turn/step; the emit added only the live
Agent handle, which the sole consumer discarded. This is the boundary-mirror
duplication the event-domain work removed for turn/step boundaries, applied to
the token stream — a follow-up the boundary RFC explicitly deferred.

The premise is settled: chunk persistence is authoritative (the proposal to
stop persisting chunks was rejected — replay/snapshots depend on it), so
`assistant/chunk` on `session/event` is the load-bearing token stream and
`agent/stream-chunk` is pure redundancy.

- Remove the `agent/stream-chunk` declaration + emit; drop the now-unused
  StreamChunk import from dsh-agent's types.
- Migrate `dsh-ui-stdio` (the only live consumer; ACP already reads
  assistant/chunk off session/event) to render assistant/chunk in its existing
  session/event listener. Consolidating to one listener also makes the
  inReasoning dim-SGR flag deterministic across chunk/boundary events (they no
  longer race across two listeners).
- Repoint the agent-loop tests (cancel/loop) and ui-stdio tests to the
  session/event assistant/chunk feed.
- New RFC (implemented/simplification/2026-07-02-remove-stream-chunk-mirror);
  amend the boundary RFC's retained-list entry to cross-link; update
  architecture, cookbook, event-domain-semantics, the ACP proposal, and the
  regenerated cordis catalog.

Snapshot goldens unchanged (ACP never used the mirror), confirming no
editor-facing transcript change.
2026-07-02 23:42:16 +08:00
Tianyi Cui abe80cec68 fix(loop): record a durable prompt/blocked for every vetoed prompt
Address review on the interception-seams PR: PromptDecision.reason is
documented as the durable record of why a prompt was blocked, but the loop
only surfaced it via the fully-blocked batch's `rejected` turn/end. In a MIXED
batch — one queued prompt blocked, another allowed — the turn does not end
`rejected`, so the blocked prompt and its reason vanished from the session log
entirely.

Add a `prompt/blocked` SessionEventMap variant (content + source + reason),
appended in the open turn at the veto point in place of the user/message the
prompt would have become. It is a non-surface, turn-enclosed event (like
todo/write): it never reaches deriveMessages(). The fully-blocked batch still
also ends `rejected` for boundary balance + ACP settlement. Regression test
drives a mixed batch and asserts the blocked prompt is recorded while the
allowed one runs — proven red without the append.
2026-07-02 17:03:53 +08:00
Tianyi Cui 1323366da3 Merge worktree-hooks-b-bash-seam into worktree-hooks-c-interception
Bring the interception-seams branch onto current master (via A→B). The
substantive reconciliation is master's compaction `agent/pre-step` serial seam
meeting C's interception seams:

- types.ts: keep BOTH master's `agent/pre-step` AND C's new interception events
  (`agent/prompt-submit`, `agent/session-start`, `agent/turn-continuation`→
  `ContinuationDecision`); drop the turn-mirror declarations (removed on A).
- loop.ts: the merged per-turn order is `turn/start` → per queued msg
  `agent/prompt-submit` (rewrite/inject/block) → (fully-blocked ⇒ zero-step
  `rejected`) → per step: drain steering → assemble system prompt →
  `agent/pre-step` (compaction, OUTSIDE the step) → `step/start` → single
  `deriveMessages()` → model → tools/pre-execute·dispatch·post-execute. No
  turn-mirror emits; `closeTurn()` is the A-simplified single-call form.
- Docs (architecture, core.md, agent/agent-loop READMEs, catalog) reconciled to
  show C's interception seams alongside `agent/pre-step`, no turn/step mirrors.
- rfc/README: dropped the stale `proposed/` compaction row (master moved that RFC
  to implemented/); kept C's new `pre-tool-input-rewrite` proposed row.
- interception.spec.ts: migrated its two `agent/turn-end` reason collectors to
  the `turn/end` session event, and ADDED a cross-test proving a
  `prompt-submit` rewrite + additionalContext is VISIBLE to an `agent/pre-step`
  listener on the same turn — pinning the merged seam ordering (compaction sees
  the post-prompt-submit surface, not stale history).
2026-07-02 04:51:24 +08:00
Tianyi Cui 9e575a2a2c docs(events): fix stale turn-mirror references caught in review
Codex review of the turn-mirror removal found current-state docs/comments that
still claimed the removed `agent/turn-start`/`agent/turn-end` events exist:

- docs/architecture.md: the loop diagram's turn-start line still said "emit
  agent/turn-start" (the turn-end line was already fixed).
- event-domain-semantics RFC: the `agent/*` domain description listed "the turn
  boundaries" among the transient emits.
- docs/core-data-structures/core.md: the agent/* taxonomy blurb listed
  "turn/step boundaries" as agent events.
- the proposed ACP RFC: the settle-signal rows named agent/turn-start /
  agent/turn-end; retargeted to the durable `turn/end` session event + the
  session/event owning-turn correlation.
- loop.ts outer-catch comment: said "closeTurn/failTurn are idempotent" — after
  the emit-param removal closeTurn is called exactly once (mutually exclusive
  normal/catch paths), so corrected to state that and to scope idempotency to
  closeStep (which is still guarded by stepOpen).

Regenerated the cordis catalog. No behavior change.
2026-07-02 03:47:37 +08:00
Tianyi Cui 140f818a42 refactor(events): remove the turn boundary mirror events
Complete the boundary-mirror removal begun with the step mirrors: drop
`agent/turn-start` and `agent/turn-end` from the agent event taxonomy. Turn and
step boundaries are now read exclusively off the durable `session/event` feed
(`turn/start`/`turn/end`/`step/start`/`step/end`) — there is no `agent/*` mirror
for any boundary.

- loop.ts: delete both turn emits; `closeTurn` loses its `emit` parameter and
  its now-unreachable idempotency guard (it is called exactly once per turn, on
  mutually exclusive normal/catch paths); `failTurn` loses the dead post-close
  branch that only a throwing turn-end LISTENER could reach.
- ui-stdio: render turn boundaries from `session/event`, recovering the short
  agent label from an `agent/created`→id map (the `turn/start` event carries only
  the turn number, and the session id is not reliably the agent id). ui-stdio is
  a disposable test REPL, so this migration retires the sole justification the
  event-domain-semantics RFC gave for KEEPING the turn mirrors.
- Tests: reason/turn-number collectors and the boundary-ordering test now read
  `session/event`; the throwing-turn-boundary-LISTENER tests are deleted (that
  code path no longer exists). A new test covers the outer-catch disposed branch
  via a pre-step listener that disposes-then-throws (the surviving real path).
- Docs: promote the "remove agent boundary mirror events" RFC to implemented
  (amended/narrowed — `agent/steering` is RETAINED, not a boundary mirror);
  update the event-domain-semantics + turn-enclosure RFCs, architecture.md, the
  cookbook, the ACP/agent/ui-stdio prose, and regenerate the cordis catalog.

`agent/steering` and `agent/stream-chunk` are explicitly out of scope (not
durable-boundary mirrors). ACP is unaffected — it already settles from the log's
`turn/end` + `agent/status`; snapshot goldens are byte-unchanged.
2026-07-02 03:26:45 +08:00
Tianyi Cui bdf390679b Merge origin/master into worktree-hooks-a-taxonomy
Bring the event-taxonomy branch up to date with master's compaction work.
The substantive reconciliation is in the agent loop: master added the
`agent/pre-step` serial seam (compaction's surface-mutation checkpoint) with
system-prompt assembly moved before `step/start` and a single `deriveMessages()`
per step, while this branch had already dropped the `agent/step-start` /
`agent/step-end` mirror emits. Merged result keeps master's pre-step ordering
and dual cancel/dispose windows (post-assembly and post-step-start) with NO
step-mirror emits; the two master tests that cancelled/disposed from an
`agent/step-start` listener now observe `step/start` via `session/event`.

Regenerated the cordis catalog and module graph from source. Gates: typecheck
clean, agent-loop + compact suites green (226 tests).

Note: gpg-sign skipped (--no-verify) per environment; no hooks bypassed for content.
2026-07-02 02:16:07 +08:00
Tianyi Cui 8a8b88b0a7 Merge remote-tracking branch 'origin/master' into compact-basic-refactor
# Conflicts:
#	docs/architecture.md
#	examples/coding-agent/README.md
#	packages/core/session/README.md
2026-07-01 23:29:20 +08:00
Tianyi Cui 5bdb40ff34 Merge remote-tracking branch 'origin/worktree-hooks-b-bash-seam' into worktree-hooks-c-interception
# Conflicts:
#	docs/architecture.md
#	packages/core/agent-loop/README.md
2026-07-01 15:37:54 +08:00
Tianyi Cui 3e26102baf Merge remote-tracking branch 'origin/master' into worktree-hooks-a-taxonomy 2026-07-01 15:10:31 +08:00
Tianyi Cui 0478f5965a docs: address review sync gaps 2026-07-01 12:56:13 +08:00
Tianyi Cui 483e0e5edf fix(events): address Codex review — protect post-execute result, purge stale tools/execute refs
Codex's PR-C review found two (A) blockers:

- tools/post-execute could corrupt the protected outcome. postExecute passed the
  mutable `result` to listeners and then read result.callId / spread result on the
  return paths, so a listener mutating the reference (flipping isError, rewriting
  callId, injecting an error) escaped the decision channel. Now the authoritative
  callId/isError/error are SNAPSHOT before the waterfall and the return value is
  rebuilt from the snapshot + the typed PostToolDecision — the decision is the only
  sanctioned way to change the outcome, and callId is always exec.callId. Added a
  regression test that mutates the result reference and asserts it has no effect;
  proven to fail red on the unfixed code.

- Public docs/JSDoc still advertised the removed `tools/execute` waterfall after the
  split. Swept every current-state reference to tools/pre-execute + tools/post-execute:
  the ToolRegistry class JSDoc (and the regenerated catalog), loop.ts's ASCII flow
  (also added the prompt-submit/session-start steps it was missing), the package-map
  READMEs (packages, core, agent-core), core-data-structures core.md/tools.md, the
  bash + acp + invariants src/READMEs (the deferred permission gate is the
  tools/pre-execute deny/ask seam now), the cookbook, and the implemented RFCs whose
  factual seam catalog drifted. codec.ts's totality prose now lists `rejected`.
  Proposed-RFC references are left as-is (frozen proposals, validated when built).
2026-06-30 20:25:30 +08:00
Tianyi Cui dc95a7881d feat(events): interception seams — the typed-Decision surface for hooks
Reshape the agent's interception surface so every seam returns a small, typed
Decision union, and the set covers the hook points a CC/Codex bridge (and a
native plugin) needs. "Native hooks" are not a package — a native hook is just a
cordis plugin on these canonical events; the bridges (a later PR) only translate
an external protocol onto the same surface.

dsh-agent:
- NEW agent/session-start(agent, source) emit (once before turn 1; SessionStartSource
  startup|resume|clear|compact) — a pure notification, seeds context via inject().
- NEW agent/prompt-submit waterfall → PromptDecision (allow, optionally rewriting the
  prompt or attaching additionalContext, or block).
- RESHAPE agent/turn-continuation boolean → ContinuationDecision ({action:'stop'} |
  {action:'continue', reason?}; a continue reason is recorded as next-step steering).
- New HookContext envelope (required source — inject() would mislabel a missing one).

dsh-tools: split the single tools/execute waterfall into tools/pre-execute
(PreToolDecision allow/deny/ask gate) and tools/post-execute (PostToolDecision
accept/block, optionally replacing content or attaching additionalContext). Core
dispatch sits between as plain code; the tool body keeps its inner try/catch so a
thrown tool still reaches post-execute as an isError. ToolExecutionResult gains
additionalContext (ferried to the loop's per-step buffer). Input rewrite is
deliberately NOT offered (a proposed RFC designs it consistently).

dsh-session: new `rejected` TurnEndReason — a turn whose whole prompt batch was
blocked by prompt-submit.

agent-loop firing points: session-start emitted at create (source threaded —
startup for create/fork, resume for resume()); prompt-submit per drained message
with the always-open-turn rule (a fully-blocked batch is a zero-step rejected
turn); the continuation reshape; post-tool additionalContext buffered and appended
after all tool/results (adjacency). ACP codec maps rejected→cancelled.

A worked native-plugin example (interception.spec.ts) proves all four seams compose
end-to-end through the real loop with NO hook/* events (those belong to the bridge
lib). All existing tools/execute + turn-continuation tests migrated. The
tool-subagent abort test now aborts after a microtask so it still exercises the
live onAbort bridge (execute() awaits pre-execute before the body runs).

RFCs: implemented/feature/2026-06-30-interception-seams.md (the reshape) +
proposed/feature/2026-06-30-pre-tool-input-rewrite.md (the deferred rewrite design).
2026-06-30 17:11:18 +08:00
Tianyi Cui b8d0da9f8c docs(loop): clarify the /goal steering comment names the step/end session event
The continuation-override comment said "step-end/continuation listeners". With no
agent/step-end emit, the surviving step-boundary listener is the durable step/end
SESSION event, so spell it "step/end session-event/continuation listeners" to avoid
implying a removed agent/* mirror. Comment-only; no behavior change.
2026-06-30 13:02:56 +08:00
Tianyi Cui a821dcbe0d fix(events): address Codex confirmation review — strengthen /goal guard, fix doc drift
Second-round Codex review of the PR-A taxonomy change found four issues, all
verified against the code:

- The /goal regression guard asserted only that the steered content reached
  requests[1], which passes even with the hasSteering override (loop.ts) disabled:
  leftover steering is re-enqueued as a next-turn queued message and also lands in
  requests[1], one turn later. The guard now asserts the same-turn shape — ONE
  turn, TWO steps, a steering/message recorded before step 2 — which is the
  mechanism the override drives. Proven to fail red with the override disabled.
- The event-domain-semantics RFC's consequence list still described the pre-fix
  behavior (step marked open AFTER step/start, so no step/end owed). It now states
  the shipped behavior: the loop marks the step open BEFORE the append, so a
  throwing step/start listener gets a balancing step/end via closeStep().
- architecture.md's loop pseudocode said only continuation listeners force
  continuation; step/end session-event listeners (the /goal pattern) do too.
- The agent/turn-end JSDoc listed a `rejected` TurnEndReason that does not exist on
  this branch (it belongs to the later interception work). Removed it and
  regenerated the cordis catalog; `interrupted` (a real variant) stays.
2026-06-30 12:46:59 +08:00
Tianyi Cui 8df89d8e33 fix(events): address Codex review — balance step on step/start-listener throw, restore /goal guard
Codex review of PR-A found three blockers:

- A throwing step/start session-event listener left an unbalanced log
  (turn/start → step/start → turn/end with no step/end), which the invariants
  oracle rejects — masked because that rejection was itself contained as a
  throwing turn/end listener. Fix the root cause in the loop: mark the step open
  BEFORE appending step/start (Session.append pushes before notifying), so the
  outer catch's closeStep() appends the balancing step/end. The test now asserts
  the balanced outcome (stepEnd:1, step/end before turn/end); proven load-bearing
  (revert the reorder → the test goes red with stepEnd:0).
- Reintroduce the /goal-pattern guard deleted in the prior commit, migrated to a
  step/end session-event listener (the surviving step-boundary hook point), with
  a no-tools first step so it exercises the hasSteering continuation override.
- Update packages/core/agent/README.md: step boundaries are no longer agent/*
  emits.
2026-06-30 12:19:18 +08:00
Hypatia May ed5d8550ae test(agent-loop): cover step-start disposal 2026-06-30 11:33:40 +08:00
Hypatia May b0eae94fc8 fix pre-step cancellation and compaction convergence 2026-06-30 10:56:34 +08:00
Tianyi Cui 05b75abbca refactor(events): document event-domain semantics, drop step-boundary mirror emits
Pin the three-domain rule (session = durable fact log, agent = live runtime
surface, tools = registry/exec): a durable replayable fact is a SessionEvent; a
live interception or transient/live-object signal is an agent/tools Cordis
event. A boundary that is both is mirrored as an agent/* emit ONLY where a live
consumer needs the Agent handle.

Apply it to the boundary twins: drop agent/step-start and agent/step-end (no
production consumer needs the live Agent at a step boundary — consumers read the
durable step/start/step/end session events). Keep agent/turn-start/turn-end (the
stdio UI labels output by agent.id). Tests that observed step boundaries via the
removed emits now observe the durable session events; the pinned behavior is
unchanged.

Conservative subset of the proposed "remove boundary mirror events"
simplification; foundation for the Hooks subsystem's canonical event surface.
2026-06-30 10:32:55 +08:00
Hypatia May 1f35a4446d fix(compact): address PR 110 review findings
Honor cancellation and disposal around async pre-step setup before the loop can open a step or call the model.

Route compaction summarization through agent/request so router agents can select the model, and remove the stale model argument from agent/pre-step.

Document serial events and the approximate convergence bound, regenerate the Cordis catalog, and add regression coverage for router compaction, HMR cleanup, and assembly/pre-step interruption.
2026-06-29 16:02:27 +08:00
Hypatia May b13586ff8e docs(compact): align seam docs with the pre-step seam and record session/invariants changes (CBR-003)
Codex round 1 CBR-003: several docs still described compaction as an
`agent/request` waterfall concern, and the implemented compaction RFC
claimed "No changes to dsh-session or dsh-invariants" while the diff
changed both.

- Package READMEs / JSDoc (agent, agent-loop, system-prompt, compact,
  compact-basic): compaction now lives on the serial `agent/pre-step`
  seam (fired after turn/start, before step/start); the structural guard
  is tool-pairing balance (`isToolPairingBalanced`), not step-alignment;
  the convergence bound is strict (`>=` rejects).
- architecture.md / core-data-structures/compaction.md: same seam +
  predicate + dispatch-mode updates; regenerated cordis catalog.
- Implemented compaction RFC, updated in place to describe shipped
  reality: the seam is `agent/pre-step` (@mode serial) fired before
  step/start; alignment is surface tool-pairing balance; the convergence
  invariant rejects `>=`; and the "no dsh-session/dsh-invariants changes"
  claim is corrected — dsh-session gains the tool-pairing predicate and
  dsh-invariants drops its `start <= end` replace assertion (a positional
  replace makes start > end normal).
2026-06-26 13:51:45 +08:00
Hypatia May d6da8ca29a fix(compact): decide step-alignment from surface tool-pairing, fire compaction pre-step (CBR-001)
Codex round 1 CBR-001: a head-anchored compaction checkpoint was
mis-classified by the log-position step-alignment scan, so a second
auto-compaction over a checkpoint-headed surface silently failed.

Root cause: `isStepAlignedStart/End` scanned the LOG by seq, but a
`replace` op lands a checkpoint at a high log seq whose SURFACE position
is the head — its log neighbours (the open step's assistant/message) are
not its surface neighbours, so the forward scan wrongly reported mid-step.

Fix, per the agreed direction:
- Replace the two log-position predicates with one surface-anchored
  helper `isToolPairingBalanced(nodes, events, beforeSeq)` in
  `dsh-session` (renamed step-boundary.ts → tool-pairing.ts). A cut is
  balanced when no unanswered tool-call precedes it on the surface; a
  region is collapsible iff both edges are balanced cuts. The open-tail
  and free-node cases fall out of the same counter. It also throws on a
  corrupt surface (a tool/result with no matching call).
- Move compaction off the in-step seam to a new "pre-step" seam fired
  after turn/start and before step/start, so a compaction's log-only
  compact/* records and its replacement node land cleanly OUTSIDE any
  step (the honest structure crash-safety relies on). Renamed the event
  agent/pre-request → agent/pre-step and switched its dispatch from
  parallel → serial (listeners mutate the surface as a side effect;
  serial isolates them so concurrent appends can't interleave). Extended
  the catalog generator to accept @mode serial.

Regression coverage: a real-loop test driving an auto-compaction asserts
the landed checkpoint is a balanced cut on both sides; unit tests pin the
checkpoint case, the mid-step injection case, multi-call steps, and the
corrupt-surface guard. Proven red on the old log-position logic.
2026-06-26 13:51:01 +08:00
Hypatia May cec32faa4e refactor(compact): turn-agnostic retention + dedicated agent/pre-request seam
Reform the compaction blueprint so a runaway turn survives and the design
stops drifting across review rounds:

- Drop in-flight-turn protection ("layer 2"). Retention is a uniform tail→head
  whole-unit walk; the only structural guard is step-alignment. A single turn
  that alone exceeds the window now compacts its own early closed steps instead
  of being retained verbatim (the failure mode that motivated this).
- Move auto-compaction off the agent/request waterfall onto a new awaited
  agent/pre-request loop seam, fired before history derivation. Compaction
  mutates the surface; the loop derives once from the result — no double-derive,
  and a listener structurally cannot act on not-yet-derived messages.
- Tighten compactIfNeeded to required (session, system, model, signal).
- Enforce a single-pass convergence invariant in resolveConfig: reject configs
  where summarizationMaxTokens + retainTokens exceeds the threshold, so a
  compaction can never immediately re-trigger.
- Document the crash vs recoverable failure taxonomy; core session repair stays
  compaction-agnostic (a log-only orphaned compact/start is inert).
- Wire dsh-compact-basic into examples/coding-agent and add a with-key
  compaction e2e (compaction's first real-world exercise + runaway net).
- Rewrite the RFC to encode the blueprint and move it to implemented/.

The runaway-turn snapshot is a named deferred follow-up: dsh-llm-replay cannot
yet serve the interleaved summarization model call.
2026-06-26 08:59:33 +08:00
Hypatia May 9cc8dc371e Merge remote-tracking branch 'origin/master' into session-surface
Reconciles the session-surface work (surfaceOp/sourceEventSeqs provenance as
the sole derivation path) with master's worktree-subagent series (fork-seed
boundary + out-of-process subagent backends).

Semantic reconciliations beyond the textual auto-merge:
- SQLite SCHEMA_VERSION: both sides bumped 2->3. Merged to a single v3 carrying
  BOTH column families — master's seed_length on `sessions` and surface's
  source_event_seqs/surface_op on `events`. writeRow + both INSERT sites bind
  the full set; the schema doc lists all three added columns as the v2->v3 gap.
- agent-loop runStep request: master's `sessionId: session.id` and surface's
  per-append surfaceOp/sourceEventSeqs coexist (different regions).
- Fork seed + surface: a fork seeds the child from the parent's LIVE events,
  which now carry surfaceOp, so the child's surface rebuilds correctly. Verified
  end-to-end — the subagent-fork replay recalls the inherited "SAFFRON" codeword
  through the seeded prefix.
- Subagent snapshot fixtures (recorded pre-surface) re-enriched via KEYLESS
  deterministic replay: only surfaceOp/sourceEventSeqs added onto existing
  recorded lines (matched by seq), no recorded value changed. Not re-recorded
  against the live API.

Gates: typecheck, test (1112), test:snapshot (14), doc-sync, lint, build,
hygiene all green.
2026-06-24 10:48:01 +08:00
Tianyi Cui b3d40d427e Persist the seed boundary so fork-child replay routes correctly
A fork subagent seeds its child session with a prefix of the parent's log, and
that seed becomes the child's persisted log — so a fork child's .jsonl begins
with the PARENT's events, including the parent's assistant/chunk events. The
snapshot replay harness derived a child's script from its whole log, which would
replay the parent's recorded responses as the child's model calls. Spawn-only
scenarios never hit it, but a fork snapshot would mis-route silently.

Record the seed boundary and skip the inherited prefix at replay:

- SessionHeader gains an optional `seedLength` (how many leading events were
  inherited via a seed), threaded through CreateSessionOptions/CreateAgentOptions
  meta and stamped by the fork backend (= seeded-prefix length; absent for spawn).
  It is EXPLICIT, never inferred from seed.length: a resume seeds the whole stored
  log, so the resume path passes the persisted boundary back.
- Both persistence backends round-trip it: JSONL header line, SQLite seed_length
  column. The SQLite table change bumps SCHEMA_VERSION 2->3; per the pre-release
  stance the backend rejects an older user_version on open with NO migration.
- llm-replay's parseSessionHeader reads seedLength and loadSessionScripts derives
  a child script from events AFTER the boundary. seedLength is 0 for spawn, so
  spawn replay is byte-for-byte unchanged.

Closes the routing-correctness gap the per-session snapshot replay RFC under-
stated; a recorded fork scenario remains a future addition but now derives
correctly. RFC: docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md.

Regression coverage: a fork child fixture whose seeded prefix carries a parent
chunk (derived script must exclude it, proven red without the slice); a seedLength
persistence round-trip through the shared coordinator contract (both backends);
the fork backend stamping it; resume preserving it from the persisted header.
2026-06-22 20:55:32 +08:00
Tianyi Cui e46ebcec89 Merge branch 'worktree-subagent-seam-pr2' into worktree-subagent-seam-pr2.5 2026-06-22 14:45:09 +08:00
Tianyi Cui 398b4f2ccf Merge branch 'worktree-subagent-seam-pr1' into worktree-subagent-seam-pr2 2026-06-22 14:36:57 +08:00
Hypatia May 0298f5c6f0 Merge remote-tracking branch 'origin/master' into session-surface
Reconcile the session-surface feature with master's package reorg and
simplifications:

- Adopt master's folded usage (assistant/message.usage; standalone `usage`
  event dropped) and re-attach surface metadata (surfaceOp/sourceEventSeqs).
- Add surface opts to master's new max-tokens assistant/message append.
- Port surface columns onto the coordinator-refactored SQLite backend at its
  new path; drop the dead v1->v2 migration (bump-and-reject, no migration per
  pre-release policy).
- Move the session-surface RFC into implemented/architecture/ and refresh its
  stale body (no migration, SESSION_FORMAT_VERSION=0, renamed package paths).
- Update the core-data-structures catalog SessionEvent blocks for the two new
  surface fields; regenerate the cordis catalog.
- Re-harvest ACP snapshot fixtures (keyless replay) to carry surface metadata.
2026-06-22 10:35:59 +08:00
imccyu 6731793859 revert: use rewriteRelativeImportExtensions for NextNode .d.ts resolve 2026-06-22 09:02:58 +08:00
Tianyi Cui e68496fd79 Add per-session snapshot replay for nested agents (PR2.5)
The snapshot tier was built single-session: dsh-llm-replay served calls from
one global positional cursor, and the harness harvested one session log. A
subagent runs as a second agent with its own session, so a parent→child
scenario could neither replay deterministically nor harvest the child's log.
This resolves the TODO(subagent-snapshots) deferral from the subagent RFC.

- Stamp the calling session id onto the model request: GenerateOptions.sessionId
  (typed Branded<'SessionId'> to avoid the dsh-llm↔dsh-session cycle), set by the
  agent loop from agent.session.id. Adapters ignore it; an llm/stream listener
  routes by it.
- Key replay per session: dsh-llm-replay loads the parent log plus one per child
  (childFiles / $DSH_SNAPSHOT_CHILD_FILES), derives a script per recorded session,
  and binds each live (freshly-random) session to a recorded script by first-call
  order — parent first (earliest createdAt, first to stream). Keys by WHO calls,
  so it survives a future concurrent/backgrounded subagent; a global cursor would
  not. An unrecorded extra session fails loud.
- Harvest every log: the harness collects all .jsonl across cwd buckets, ordered
  primary-first (top-level, then children by createdAt), and RunResult exposes the
  plural sessionLogs. The spec writes each back on record (session.jsonl +
  session.<n>.jsonl) and diffs each against its fixture on replay.
- Wire the subagent seam + spawn + fork + tool into the acp-agent example (both
  cordis configs) and add two nested scenarios recorded against the real API:
  subagent-spawn (parent + 1 child) and subagent-multi (parent + 2 children, 3
  sessions). Both replay keyless in the default gate.

A new RFC documents the design (docs/rfc/implemented/testing/). Single-session
replay is unchanged (a call with no sessionId is one anonymous primary session).

TODO follow-up: a dedicated branded-ids package could own the SessionId brand and
dissolve the cross-package cycle note; out of scope for this testing PR.
2026-06-22 08:39:36 +08:00
Tianyi Cui b82c310db3 Fix subagent in-process result scoping (Codex review round 1)
Two merge-blocking bugs in the shared in-process run driver, both rooted in
`readResult` scanning the whole child session and deriving the stop reason only
from `turn/end`:

- A pre-turn `cancel()` cleared the queued prompt before any `turn/end` was
  logged, so the run settled `error` instead of `aborted`, violating the
  `SubagentRun.cancel()` contract. The driver now tracks that a cancel was
  requested and maps the no-turn case to `aborted`.
- A fork child whose own turn produced no `assistant/message` returned the
  SEEDED parent's last message as a `completed` success. `readResult` now scopes
  to the child's OWN events (after the seed prefix), so a message-less child
  yields empty output.

Both fixes carry a regression test proven to go red on the pre-fix driver.

Also: correct the `SubagentRun.id` / event-payload docs (it is the child AGENT
id, not a session id — the backend mints distinct tokens); refresh the stale
`coding-agent` welcome string (subagent is now a tool); and replace the stale
`TODO(sub-agents)` "deferred" prose in the Agent interface, core.md, and
architecture.md with an accurate pointer to the realized seam.
2026-06-22 06:47:20 +08:00
Tianyi Cui 07f4047ff0 Use explicit ts specifiers for declarations
Restore explicit .ts relative specifiers in source and enable rewriteRelativeImportExtensions so emitted JS uses .js while declarations keep explicit .ts specifiers. Add a NodeNext declaration-consumer gate to prevent extensionless declaration regressions.
2026-06-22 06:11:00 +08:00
Tianyi Cui 7aabd2a3df Add in-process subagent backends: spawn (fresh) and fork (seeded)
The second PR of the subagent seam: the two in-process backends that run a
child agent on the same cordis context, reusing the agent factory's quiescent
AgentHandle teardown. Both register on ctx.subagents (PR1's named-provider
registry) and share one run driver.

- dsh-subagent-spawn: a FRESH child via ctx.agents.create — own session, the
  parent's model by default (overridable), zero inherited conversation. Also
  exports the shared in-process run driver (startInProcessRun): mint ids, stamp
  cwd/parentSession-lineage/depth, drive the one-shot (send → whenIdle), read
  the last assistant/message + turn/end reason, dispose to quiescence.
- dsh-subagent-fork: a child SEEDED with the parent's balanced completed-turn
  prefix (the log up to and including its last turn/end), so the child inherits
  context. The in-flight unbalanced turn is excluded — a raw seed would fail the
  invariants replay. Proven: a regression test goes red if the boundary seeds
  the open turn.
- Seam extension: CreateAgentOptions.seed, threaded through AgentLoop.createAgent
  → ctx.sessions.prepare({ seed }) (the primitive resume already used). This is
  the fork-lineage path the TODO(sub-agents) markers anticipated.
- Depth: a merge-extensible AgentOptions.subagentDepth (0 top-level, parent+1 for
  a child); the depthLimit capability refuses a spawn past request.maxDepth.

Tests: real-loop unit tests for both backends (mock MODEL only, real loop +
invariants), a multi-subagent test (one parent drives a fork AND a spawn child
then keeps working), and a with-key e2e (a real parent delegates via the
`subagent` tool to a real child that writes a file on disk — world-verified).
100% per-file coverage. The coding-agent demo wires the spawn backend + tool.

Snapshot coverage of nested agents is deferred to a stacked follow-up
(TODO(subagent-snapshots)): dsh-llm-replay is a single global positional cursor
that cannot route calls to a parent vs. a child on one context. Recorded in the
RFC's deferrals and a new AGENTS.md rule: designing a subsystem must design its
test infrastructure END TO END up front, verifying the snapshot/e2e harness can
express the new shape — a gap this plan hit.
2026-06-22 05:58:40 +08:00
imccyu 88b75181ad fix: apply ts-build-config adjustment to new packages 2026-06-22 01:38:39 +08:00
imccyu 74cdd9a2e5 Merge remote-tracking branch 'origin/master' into feat/adr0016-type-build-check 2026-06-22 00:35:51 +08:00
Tianyi Cui f09a88ecee Merge branch 'worktree-simplify-agent-stop' into worktree-simplify-trace-events 2026-06-21 18:38:29 +08:00
Tianyi Cui 2eb6ad3260 fix review findings: sync stale v1/removed-event doc references
Codex's re-confirmation pass verified both blocker fixes correct but found
doc/comment drift the fix commit missed:

- session/index.ts + session/README.md: "minimal v1 header" → "minimal header
  (stamped with the current SESSION_FORMAT_VERSION)" — the version is 0, not 1.
- session/index.ts deriveMessages comment listed "usage, and errors" as trace
  data — those standalone events no longer exist; only boundaries + chunks are.
- session-persistence RFC: "no v1 migration" → the pinned-v0 pre-release stance.
- collapse-trace-only RFC format-version note: reframed off the "bump the
  version and reject" wording (which now reads as the OTHER AGENTS.md stance)
  onto the pinned-0 unstable stance the session log actually uses.
- agent-loop/loop.ts finishError JSDoc: "with a logged `error` event" → the
  failure is recorded on turn/end.reason (no standalone error event).
- acp/acp-feature-support.md (two spots): usage is recorded on assistant/message
  now, not as standalone internal usage events.
- Regenerate the cordis catalog (finishError JSDoc line shift).
2026-06-21 11:33:41 +08:00