Commit Graph

102 Commits

Author SHA1 Message Date
Tianyi Cui
49bb6a88eb fix(tool-bash): handle unavailable spill paths 2026-06-19 01:54:57 +08:00
Tianyi Cui
7fa113be0e Merge remote-tracking branch 'origin/master' into codex/pr48-repo-hardening-rfcs
# Conflicts:
#	docs/adr/README.md
#	docs/rfc/009-session-persistence-and-resumability.md
#	docs/rfc/README.md
#	docs/rfc/implemented/2026-06-11-doc-sync-enforcement.md
#	docs/rfc/proposed/2026-06-14-acp-agent-client-protocol.md
#	examples/acp-agent/tests/acp.e2e.ts
#	packages/acp/README.md
#	packages/acp/src/index.ts
#	packages/acp/tests/stream-update.spec.ts
#	packages/agent-loop/src/loop.ts
#	packages/tools/src/index.ts
2026-06-18 23:41:14 +08:00
Tianyi Cui
9f6b96c555 fix(acp): address review of the terminal-card alignment (exit parse, background/error, capability snapshot)
Codex + an independent review pass found three real defects in the prior commit:

1. parseExitStatus could misreport a SUCCESSFUL command as a failure: a clean
   exit 0 appends no marker, so output ending in "[exit code: 5]" (no trailing
   newline) was read as the marker. Anchor the parse to a LEADING newline —
   renderResult always inserts one before a real marker, so a body that merely
   ends in marker-like text no longer matches. A narrow residual (a clean exit 0
   whose final line is exactly the marker) is inherent to the replay-only-sees-
   text design and documented; the complete fix (a structured exit on the event)
   is the RFC's named escape hatch.

2. A run_in_background start and an isError result were rendered as exited
   terminal cards with a false exit-0 pill. A background start returns a task-id
   ack (not a streamed terminal) and is no longer marked terminal; an isError
   result (spawn failure / abort) carries no exit pill.

3. The terminal capability was re-read live on the result path, so a second
   initialize between a call and its result could desync them (orphan
   terminal_output or clobbered card). Snapshot the capability per session at
   creation (SessionRecord.terminalEnabled) so call and result always agree.

Also reword the reference-parity claim: keeping the description as a content
block in terminal mode is a DELIBERATE divergence (claude-agent-acp drops it).
Tests added for each; with-key e2e still green.
2026-06-18 19:35:15 +08:00
Tianyi Cui
e51dabbb8b feat(acp): align bash terminal card with reference adapters (command title, description block, exit pill)
Match claude-agent-acp / codex-acp: the bash tool_call title IS the command
(an execute card hides rawInput), the model description rides as a content
text block above the card, and the completed card carries an exit-status pill
via _meta.terminal_exit.

Bridge fixes found in review of the prior terminal-card commit:
- tool_call_update.content is OMITTED in terminal mode (an ACP update.content
  REPLACES the call's content collection in Zed, so the fenced ```console block
  would clobber the terminal content block).
- terminal.output preserves RAW newlines (terminal renderers rely on exact
  bytes); only the fenced fallback trims trailing blank lines.
- a relative workdir is resolved against the session cwd for the card header,
  matching where the command actually ran.
- result-side terminal output is gated on the pending call having registered a
  terminal (no orphan _meta.terminal_output for a terminal Zed never made).

The exit pill is recovered by parsing renderResult's status markers (the pure
presentResult seam sees only content blocks); a round-trip test pins the parse
to the marker emission. Neutral ToolTerminal gains exitCode/signal; widened
ToolCallPresentation with a content block. Docs (RFC + 3 READMEs) updated;
with-key e2e verifies the card + exit pill against the real model.
2026-06-18 18:54:32 +08:00
Tianyi Cui
c8dbe6567a docs(acp): clarify _meta is a spec extensibility point; the terminal keys are the Zed convention 2026-06-18 17:34:50 +08:00
Tianyi Cui
149ab1bba4 feat(acp): render bash as a terminal card via the _meta convention
When the client advertises clientCapabilities._meta.terminal_output (Zed), a
bash tool call now renders as a real TERMINAL card — a cwd header + the command
+ its output — instead of the plain ```console text block. Keeps agent-side
dsh-bash execution; rejects the spec's client-side terminal/create (which would
bypass sandbox/env-scrub/ownership/cwd). Matches what claude-agent-acp and
codex-acp do; wire contract verified against Zed's source.

- dsh-tools: a provider-neutral ToolTerminal shape ({ cwd?, output? }) on
  ToolCallPresentation/ToolResultPresentation — a tool asks "render me as a
  terminal"; no ACP types leak in.
- dsh-tool-bash: bash presentCall marks terminal (cwd from an explicit absolute
  workdir, else left for the bridge to fill from the session cwd); presentResult
  carries the output alongside the ```console fallback.
- dsh-acp: initialize reads/remembers the _meta.terminal_output capability;
  streamSessionEventUpdate maps a terminal presentation to
  content:[{type:'terminal',terminalId}] + _meta.terminal_info on the call and
  _meta.terminal_output on the update WHEN capable — else the unchanged text
  path. terminalId is the callId; cwd defaults to the session header. The pure
  translator gained a TerminalRendering {enabled,cwd} param (off by default).

Tests via the REAL tool-bash + bash-local: capability ON -> terminal content +
_meta; OFF -> no _meta (text path). The with-key e2e adds a real-model terminal
card case (echo over ACP with the capability on). 773 tests, 100% coverage.

The exit-status pill (_meta.terminal_exit), live streaming
(_meta.terminal_output_delta), and command classification are RFC follow-ups.
2026-06-18 17:25:09 +08:00
Tianyi Cui
386ee14af3 docs(acp): RFC for the terminal-card rendering (implemented design)
Records the verified design before implementing: keep dsh-bash agent-side
execution and render Zed's terminal tool-call card via the `_meta` convention
(terminal_info/terminal_output/terminal_exit), capability-gated on
clientCapabilities._meta.terminal_output, with the ```console text block as the
no-capability fallback. Rejects the spec's client-side terminal/create path (it
would bypass dsh-bash's sandbox/env-scrub/ownership/cwd). Studied
claude-agent-acp, codex-acp, and Zed's renderer to ground the wire contract.
Live streaming and command classification are noted as separate follow-ups.
2026-06-18 14:36:02 +08:00
Tianyi Cui
8acafe918f feat(acp): show the command in execute titles; test via the real bash tool; RFC for terminal rendering
- bash presentCall title is now "description — command" (e.g. "List files in
  src — ls -la src"). An execute-kind ACP card HIDES rawInput (Zed renders it
  only for non-terminal tools), so the command must ride in the always-visible
  title to be seen — matching how claude-agent-acp/codex-acp title execute
  tools. The command stays in rawInput too for non-execute UIs that show it.
- Rework the acp tool-call presentation tests (turns + load replay) to drive the
  REAL dsh-tool-bash + dsh-bash-local via a new makeBridgeHarness({ withBash })
  option, running an actual `echo` — instead of an inline fake bash tool. The
  mock MODEL still scripts the call (deterministic, no key), but the tool and
  executor are real, so the test verifies the shipping presentCall/presentResult.
- AGENTS.md: add the principle "prefer the REAL implementation over a mock/
  stand-in in tests" (mock only the expensive/non-deterministic boundary).
- RFC (proposed): the ACP terminal sub-protocol + command classification — the
  capability-gated rich rendering (live cwd-header terminal card, classify a
  `cat` as a read / `grep` as a search) that the reference adapters do; the
  fenced ```console text block stays the no-capability baseline. Studied
  codex-acp, claude-agent-acp, and Zed's renderer to ground it.
2026-06-18 11:23:12 +08:00
Tianyi Cui
7803c38824 feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.

dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).

dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.

dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.

Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
Tianyi Cui
49bec650b8 Merge branch 'feat/acp-3-multi-session' into feat/acp-4-session-cwd
# Conflicts:
#	docs/rfc/proposed/2026-06-14-acp-agent-client-protocol.md
#	packages/acp/src/index.ts
2026-06-18 04:07:55 +08:00
Tianyi Cui
c6a7a4e462 Merge branch 'feat/acp-2-bridge' into feat/acp-3-multi-session
# Conflicts:
#	docs/rfc/proposed/2026-06-14-acp-multi-session.md
#	packages/acp/src/index.ts
2026-06-18 03:55:08 +08:00
Tianyi Cui
7c168fca34 fix(acp): address Codex review — strict ctx.get, correct fiber-ownership doc + test
- AgentLoop.resume uses `this.ctx.get('sessionPersistence')` (strict) instead
  of the `, false` overload: still topology-independent, but an inactive/
  absent backend reads as undefined (rejected by the existing guard) rather
  than being handed back mid-teardown.
- Correct the bridge teardown comment: an ACP-created agent's registry entry
  binds to the BRIDGE fiber (the factory is reached through the bridge's
  traceable proxy, so AgentLoop.start's `this.ctx.effect` registration uses the
  caller context), not the AgentLoop fiber — so an ACP-only HMR dispose
  reclaims it. Add a regression test pinning that ownership.
- Sync the ctx.get guidance in the post-mortem, packages/AGENTS.md, and the
  dsh-code-review skill to the strict form.
2026-06-18 03:48:33 +08:00
Tianyi Cui
86ec067bff Merge remote-tracking branch 'origin/master' into feat/acp-2-bridge
# Conflicts:
#	.agents/skills/dsh-code-review/SKILL.md
#	AGENTS.md
#	docs/rfc/proposed/2026-06-14-acp-agent-client-protocol.md
2026-06-18 03:31:04 +08:00
Tianyi Cui
6d37b6c33d fix(acp): server crashed on connect — drop export default, read optional service cwd-independently
Two independent bugs made the ACP server crash the moment an editor (Zed)
connected, despite 178 green unit tests at 100% coverage:

1. `session/new` threw `cannot get property "agents" without inject`. Root
   cause: a stray `export default apply` made the cordis Loader's
   `unwrapExports` (`exports.default ?? exports`) collapse the module to the
   bare `apply` function, discarding the sibling `inject`/`name`/`Config`
   named exports. The plugin fiber was built with empty `inject`, so every
   `ctx.<service>` read in `apply` threw at load. Fix: remove the default
   export so the Loader uses the namespace.

2. `session/load` threw `cannot get property "sessionPersistence" without
   inject`. `AgentLoop.resume` read `this.ctx.sessionPersistence` (a service
   it deliberately does NOT inject); the property proxy's ancestor-only fiber
   walk fails through the bridge's traceable shadow. Fix: read it via
   `this.ctx.get('sessionPersistence', false)`, the topology-independent
   global-store lookup.

Why the suite missed both: every test mounted the plugin by hand
(`ctx.plugin({name,inject,apply})`), bypassing `unwrapExports` entirely, and
the only test driving these RPCs was key-gated (skipped in CI). Added a no-key
`session/new` e2e that boots the real example through the real Loader — it
fails loudly on bug #1 without an API key. Set `TSX_TSCONFIG_PATH` in the e2e
spawn so the subprocess resolves workspace `paths` from a temp cwd (it was
silently falling back to a stale built `lib/`).

Docs: post-mortem 0001; AGENTS.md "line coverage is not behavior coverage" +
with-key/smoke-test philosophy; packages/AGENTS.md plugin-export-shape and
ctx.get rules; dsh-code-review SKILL checks.
2026-06-18 03:12:37 +08:00
Tianyi Cui
27f5f84e3b docs: address Codex review of the RFC reorg
- Fix two root-AGENTS.md cross-links that the depth bump left pointing at the
  new docs/AGENTS.md instead of the root file they cite (capability-seams,
  optional-code-mode). These resolved on disk so verify-md-links passed — the
  gate checks existence, not which file you meant; corrected to ../../../.
- Broaden verify-md-links scope to .agents/skills/**/*.md: this PR rewrote the
  dsh-code-review skill's links into the RFC tree, but the skill dir was outside
  the gate, so a broken skill link would have passed silently.
- Percent-decode the path component before the existence check, so a valid
  encoded relative target (My%20File.md) is not falsely reported broken; a
  malformed escape (%zz) is reported broken rather than crashing the gate.
- Drop the merged property-testing RFC's "nightly CI job 100x" claim: that line
  came from the original proposal, not the accepted decision, and CI has only
  push/pull_request triggers — note it as possible future work instead.

doc-sync (incl. verify-md-links over 58 files), doc-typecheck, lint pass.
2026-06-18 02:41:19 +08:00
Tianyi Cui
7c400e9c02 docs: unify ADR/RFC trees into one lifecycle-organized RFC tree
Collapse docs/adr/ and docs/rfc/ into a single docs/rfc/ with proposed/,
implemented/, and rejected/ subfolders. Every file is renamed to
yyyy-mm-dd-topic-title.md, where the date is when the topic was first
proposed (from git history). ADRs and RFCs that covered exactly the same
topic are merged (property-based testing, session persistence); the
umbrella RFC 005 stays split across its three implemented decisions, and
RFC 006's deferred part-3 (API extractor reports) splits into its own
proposed RFC. All cross-references become machine-checkable relative
links instead of bare "ADR NNNN" / "RFC NNN" prose.

Add a verify-md-links doc-sync gate (scripts/verify-md-links.ts) that
checks every relative Markdown cross-link resolves, wired into doc-sync
alongside verify-md-wrap. This makes the reorganization self-verifying:
the same change that rewrote ~forty inter-doc links adds the check that
proves none dangle. Document the cross-link convention in a new
docs/AGENTS.md and record the gate as an implemented RFC.

doc-sync, typecheck, lint, and the full test suite (667) all pass.
2026-06-18 02:18:24 +08:00
Tianyi Cui
7a5886da2d docs(rfc): record follow-up cleanup seams 2026-06-17 21:26:57 +08:00
Tianyi Cui
513e16f9dc fix(dev): make hooks and bash seams safer 2026-06-17 21:26:44 +08:00
Tianyi Cui
b920239389 fix(acp): align prompt and workspace contracts 2026-06-17 21:26:31 +08:00
Tianyi Cui
f860474f8b Merge branch 'feat/acp-3-multi-session' into feat/acp-4-session-cwd 2026-06-17 15:27:16 +08:00
Tianyi Cui
ff82254fca Merge branch 'feat/acp-2-bridge' into feat/acp-3-multi-session 2026-06-17 15:24:09 +08:00
Tianyi Cui
a9d5a5ba68 Merge branch 'feat/acp-1-max-tokens-turn-end' into feat/acp-2-bridge 2026-06-17 15:20:36 +08:00
Tianyi Cui
977c23d31f Merge remote-tracking branch 'origin/master' into feat/acp-1-max-tokens-turn-end
# Conflicts:
#	AGENTS.md
2026-06-17 15:16:26 +08:00
Tianyi Cui
c870c8fcb1 Merge remote-tracking branch 'origin/split/agent-factory' into split/session-persistence-sqlite 2026-06-17 15:05:30 +08:00
Tianyi Cui
cae1517407 Merge remote-tracking branch 'origin/master' into split/agent-factory 2026-06-17 15:03:51 +08:00
Tianyi Cui
edd6eb28dd Merge pull request #33 from deepseek-ai/split/session-persistence
feat(session-persistence): abstract seam + JSONL backend + wiring (split 3/5)
2026-06-17 15:01:52 +08:00
Tianyi Cui
f3906af225 feat(acp): honor per-session cwd — run each ACP session in its own workspace
Lifts the RFC 010 § Deferred restriction that the server had to launch in the
workspace ("cwd must equal the launch directory"). An editor can now open any
project folder, and N concurrent sessions over one connection can each target a
different directory.

- packages/acp: drop the `cwd === process.cwd()` guard in validateWorkspaceParams
  (keep "must be absolute" — the cwd becomes the session header / bash workdir),
  and drop the persisted-cwd-vs-launch-dir check in session/load (a resumed
  session keeps its original header.cwd, so its bash tools run in its workspace).
- packages/tool-bash: the missing link — default the bash workdir to the calling
  agent's session cwd (`exec.agent.session.header.cwd`) via a new resolveWorkdir
  helper. An explicit model `workdir` still wins; a relative one resolves against
  the session cwd. This is the only correct spot for multi-session: N sessions
  share one ctx.bash executor, so the workdir must come per-call from exec.agent,
  not executor config. Falls back to the executor default when no session cwd is
  available (preserves non-ACP behavior).
- Trust: the cwd originates from the ACP client (the user's editor) at
  session/new — same trust level as the old launch dir; no new untrusted-input
  path. `additionalDirectories` (scope widening / sandbox) stays rejected.
- Tests: bridge accepts any absolute cwd + records it on the header; session/load
  honors the persisted cwd; bash defaults to / resolves relative against the
  session cwd; two sessions with different cwds each run bash in their own dir;
  non-absolute cwd still rejected. 100% per-file coverage maintained.
- Docs: RFC 010 status + § Deferred cwd bullet marked RESOLVED; acp README adds a
  Per-session cwd section; tool-bash + example READMEs and e2e comments updated.
2026-06-17 10:53:40 +08:00
Tianyi Cui
ffc107aa57 docs: record verify-md-wrap in the doc-sync source-of-truth docs
Adding verify-md-wrap to the shared doc-sync gate left its defining docs
stale (Codex review):
- ADR 0014 described doc-sync as two gates; add a dated amendment for the
  third (verify-md-wrap) and drop the "two checkable classes" wording.
- CI step label/comment said "doc code blocks + event taxonomy"; include
  the markdown wrap check.
2026-06-17 10:50:58 +08:00
Tianyi Cui
dbaef018cc Merge remote-tracking branch 'origin/split/agent-factory' into split/session-persistence-sqlite
# Conflicts:
#	docs/adr/0018-session-persistence.md
2026-06-16 23:53:54 +08:00
Tianyi Cui
d3c9f6ca1e Merge remote-tracking branch 'origin/split/session-persistence' into split/agent-factory 2026-06-16 23:51:16 +08:00
Tianyi Cui
2bae18f811 fix(session): synthesize tool results for interrupted tool calls on crash recovery (review #33)
The loop logs the assistant/message (carrying tool-call blocks) BEFORE running
the tools, so a crash mid-tool leaves durable tool calls with no matching
tool/result. interruptedTurnClosers only added step/end + turn/end, so a resumed
session's deriveMessages() replayed a dangling assistant tool-call — which every
provider rejects as an invalid transcript on the next request.

interruptedTurnClosers now scans the interrupted turn for tool-call blocks
without a matching tool/result and synthesizes an error tool/result for each
(before the step/end), so the rehydrated history is a valid transcript. Adds a
dedicated repair.spec.ts and a shared-contract case proving both backends pair
every orphaned call with a result. Docs (ADR 0018, both persistence READMEs,
load() JSDoc) updated.

Also fixes the echo-agent README session-cleanup path: demo:echo runs from the
repo root, so sessions land in <repo-root>/.sessions/_no-cwd/, not
examples/echo-agent/.sessions/ (review #33).
2026-06-16 23:50:52 +08:00
Tianyi Cui
5e3df1b2f5 Merge branch 'feat/acp-2-bridge' into feat/acp-3-multi-session 2026-06-16 23:44:27 +08:00
Tianyi Cui
c2f8af30da Merge branch 'feat/acp-1-max-tokens-turn-end' into feat/acp-2-bridge
# Conflicts:
#	AGENTS.md
#	docs/cookbook/extension-cookbook.md
#	yarn.lock
2026-06-16 23:40:23 +08:00
Tianyi Cui
840dfbab84 Merge remote-tracking branch 'origin/master' into worktree-md-wrap-guard
# Conflicts:
#	AGENTS.md
2026-06-16 23:38:58 +08:00
Tianyi Cui
63425a2b87 refactor: detect md hard-wraps via mdast AST, not regex
Per review feedback (use a real markdown parser with an AST linked to
source positions), rewrite verify-md-wrap to parse each file with
mdast-util-from-markdown (the CommonMark parser behind remark) + the GFM
extension, then flag any `paragraph` node whose source span covers more
than one line.

Why a parser over the hand-rolled line scanner:
- It is a checker, not a formatter — it reports and never rewrites, so
  zero cosmetic churn (no emphasis-marker or table-delimiter
  normalization, which is why Prettier was rejected for this).
- The AST owns every structural exemption (fenced code of any fence
  length, tables, lists, blockquotes, HTML, headings, reference defs),
  fixing both bugs the regex version had: it now catches wrapped
  list-item / blockquote prose (a `paragraph` inside those nodes) and no
  longer false-positives on a longer ```` fence wrapping an inner ```.

Also unwrap two pre-existing hard-wrapped blockquotes (architecture.md,
adding-a-tool.md) that the stricter AST check correctly surfaced.
2026-06-16 23:35:33 +08:00
Tianyi Cui
ddf121320b Merge remote-tracking branch 'origin/split/session-persistence-sqlite' into feat/acp-1-max-tokens-turn-end
# Conflicts:
#	packages/session/src/types.ts
2026-06-16 23:31:14 +08:00
Tianyi Cui
973399e03c Merge pull request #45 from deepseek-ai/worktree-docs+todo-markers
docs(development): document FIXME/TODO/XXX markers
2026-06-16 23:21:37 +08:00
Tianyi Cui
9fb12740c7 Merge remote-tracking branch 'origin/split/agent-factory' into split/session-persistence-sqlite 2026-06-16 23:20:29 +08:00
Tianyi Cui
16e7bbc7c6 Merge remote-tracking branch 'origin/split/session-persistence' into split/agent-factory 2026-06-16 23:19:34 +08:00
Tianyi Cui
16e75c2631 Merge remote-tracking branch 'origin/master' into split/session-persistence 2026-06-16 23:18:06 +08:00
Tianyi Cui
c2dc63f2ea Merge pull request #46 from deepseek-ai/split/turn-enclosure
feat(agent-loop): turn-enclosure invariant + post-turn error model (split 2/5, re-land)
2026-06-16 23:16:13 +08:00
Tianyi Cui
326b026ab3 docs(development): document FIXME/TODO/XXX markers
Define the three issue-urgency tags so contributors can flag a release
blocker, a soon-to-fix item, and a someday-maybe consistently.
2026-06-16 22:45:19 +08:00
Tianyi Cui
f0d9383f49 Merge branch 'split/agent-factory' into split/session-persistence-sqlite
# Conflicts:
#	docs/adr/0018-session-persistence.md
#	docs/architecture.md
2026-06-16 22:32:49 +08:00
Tianyi Cui
8511b9639c Merge branch 'split/session-persistence' into split/agent-factory
# Conflicts:
#	docs/adr/0017-turn-enclosure-invariant.md
#	docs/architecture.md
2026-06-16 22:27:51 +08:00
Tianyi Cui
67447fcdc3 feat: enforce merge-commit policy and markdown wrap convention
- AGENTS.md: require merging PRs with a merge commit (gh pr merge
  --merge), never squash/rebase — the per-PR commit history (review-fix
  and regression-test commits) is intentional record.
- Add scripts/verify-md-wrap.ts: a doc-sync gate that fails on
  hard-wrapped prose paragraphs (one physical line per paragraph), with
  smart exemptions for fenced code, tables, lists, blockquotes, headings,
  HTML comments, hrs, and reference defs. Scope covers README.md,
  docs/**/*.md, packages/*/README.md, plus AGENTS.md / packages/AGENTS.md
  (the files doc-sync did not previously cover). Folded into doc-sync so
  it rides the existing pre-push and CI gates.
- Sync AGENTS.md and docs/development.md doc-sync descriptions and
  command lists to include verify-md-wrap.
2026-06-16 22:14:25 +08:00
Tianyi Cui
efee449cfe feat(session-persistence): preserve interrupted turns on crash; don't truncate (review #33)
A crash can leave a durable log whose final turn never closed. The old
behavior truncated everything after the last turn/end as a "crash tail".
But a single turn can be HUGE in a long-horizon task (many steps, large
tool output), so truncating it silently destroys real, durably-written
work — truncating a turn is wrong.

New crash recovery (ADR 0018): load() PRESERVES the interrupted turn's
events and CLOSES the orphaned turn by durably appending synthetic
boundary events — a step/end if a step was open, then a turn/end carrying
the new merge-extensible TurnEndReason {kind:'interrupted'}. load()
returns the balanced log, so a resumed session is immediately usable. Only
a never-fully-written TORN tail fragment is discarded; corruption in the
committed region is still unloadable.

- dsh-session: TurnEndReason {kind:'interrupted'} + shared
  interruptedTurnClosers() repair helper.
- JSONL backend: scanLog preserves the longest contiguous prefix
  (including a partial final turn); loadCore truncates a torn fragment and
  durably writes the closers, returning the balanced log.
- runPersistenceContract gains a crash-recovery test (both backends + mock).
- Docs: ADR 0018/0017, architecture.md, package READMEs.

Also (review #33): RFC 013 records the "move event vocabulary to Zod"
question (merge-extensible maps → runtime schema registry) + blast radius;
deferred, not done here.
2026-06-16 21:27:50 +08:00
Tianyi Cui
8e6fd91e15 docs: document module-graph gate; unwrap generated prose
Codex review follow-up:
- Update docs/development.md: add verify-module-graph to the pre-push and
  CI gate lists and gen/verify-module-graph to the daily commands, so the
  contributor guide matches the new freshness gate.
- Emit the module-graph.md intro paragraph as a single line (repo Markdown
  convention: one line per paragraph), since regenerating would otherwise
  reintroduce hard wrapping.
2026-06-16 21:21:21 +08:00
Tianyi Cui
b33668ef05 refactor: order module-graph table topologically (low→high level)
Replace the alphabetical package ordering with a Kahn-style topological
sort (alphabetical tiebreak within each layer to stay deterministic), so
the table and graph list leaf interfaces first and dependents last.
2026-06-16 21:11:09 +08:00
Tianyi Cui
4c8c1da8b3 feat: generate module dependency graph with freshness gate
Add scripts/gen-module-graph.ts, which derives the inter-package
dependency graph from each package's @deepseek-ai/dsh-* peerDependencies
and renders docs/module-graph.md (a GitHub-native Mermaid graph plus a
dependency table). Output is deterministic so a regenerate-and-diff
check is stable.

Wire a freshness gate the same way doc-sync is wired (ADR 0007: hooks
and CI run the same package.json scripts): verify-module-graph runs in
pre-push (lefthook) and as a CI step. It fails if the committed file
drifts from what the generator would produce.
2026-06-16 21:00:24 +08:00
Tianyi Cui
b3ea13749c feat(acp): multiplex N concurrent ACP sessions + bash task ownership (RFC 011)
Lifts the RFC 010 single-session-per-connection cap: the bridge now runs N
concurrent sessions over one connection, each mapped to its own LoopAgent.

- packages/acp: live sessions held in a Map<sessionId, SessionRecord> with an
  agent→sessionId reverse WeakMap so agent/* events (which carry only the
  Agent) demux in O(1). Every session/event and agent/status is routed strictly
  to its owning record — concurrent sessions never cross-settle or interleave
  their session/update notifications. Per-session state: one in-flight prompt
  each, session/cancel aborts+settles only its own agent/prompt, session/load
  reserves a per-id load slot (distinct ids load concurrently; re-loading a live
  id is rejected), and disposal drains every live session in parallel to
  quiescence.
- packages/tool-bash: record each background task's owning agent at spawn and
  keep it for the executor's lifetime (NOT cleared on completion).
  bash_output/bash_kill reject a task owned by a different agent (a task with no
  owner is open; a no-agent caller can't access an owned task). Task ids are
  global and predictable, so this is the fence that stops one session's agent
  from reading/killing another session's background task.
- Per-session permission ownership and a per-agent disposer seam stay deferred
  (depend on the deferred permission gate); the reverse map the gate will route
  through is in place. RFC 011 stays `proposed`.
- Tests: two sessions stream concurrently without interleave; cross-session
  cancel isolation; per-session in-flight enforcement; dispose-all-to-quiescence;
  bash cross-session read/kill rejected (+ no-agent and unowned-task cases).
- Docs: RFC 011 implementation-status note; acp + tool-bash READMEs; example
  MVP-limitations updated. 100% per-file coverage maintained.
2026-06-16 19:47:13 +08:00