qwen-code/packages/cli/src/services
ChiGao 02d303f849
feat(serve): persist prompt terminal ledger for cold-load reconciliation (#9426)
* feat(serve): persist prompt terminal ledger for cold-load reconciliation

Turn terminal events (turn_complete / turn_error) were synthesized by the ACP bridge and published over SSE only, so a prompt that was in flight when the daemon died could never be resolved after a restart: the cold load replay emits transcript chunks and carries no terminal evidence, leaving promptId-keyed orchestrators stuck on "unknown".

Each session now owns an append-only sidecar ledger next to its transcript. The bridge appends one in_flight record at prompt admission and one terminal record at the single publishPromptTerminal exit (covering the close/kill/channel-crash/daemon-shutdown flushes) through an injected synchronous sink. Ledger writes are best-effort and never block prompt execution or teardown, and records carry only ids, states, and timestamps — no prompt text, user content, or paths.

On a cold session load the serve layer reconciles prompts left dangling by a dead daemon: it classifies the transcript tail with the existing turn-interruption detector and appends a completed (stop reason reconstructed_from_transcript) or interrupted (code daemon_lost) verdict, guarded by an attribution check so an unattributable tail stays unknown (fail-closed). The load response gains an optional promptTerminals field with the trailing 64 terminal records, omitted entirely when the ledger holds no terminal evidence, and archive/unarchive move the sidecar alongside the transcript so evidence survives storage lifecycle.

Design: docs/design/2026-08-19-prompt-terminal-ledger-design.md

* fix(serve): tighten ledger reconciliation fail-closed semantics and complete sidecar lifecycle

Address review findings on the prompt terminal ledger:

- reconcile: fail closed on multiple dangling prompts (no synthesized
  terminal for the newest either); attribute the oldest dangling prompt
  only when the attribution guard skips settled admissions (fixes the
  [A if, B if, B cancelled] misattribution veto), the transcript's last
  write postdates the admission (temporal evidence), and a clean
  verdict is upgraded to interrupted when the model tail holds any
  functionCall part, id or not (id-less tool-call guard covering the
  detectTurnInterruption wire-pairing blind spot)
- lifecycle: removeSessionFiles deletes the ledger in both archive
  states; archive/unarchive move it through a single
  getPromptLedgerPathForState helper with merge semantics when the
  destination already exists (append-and-unlink instead of a permanent
  split); move warnings carry full source and destination paths in
  both directions
- scans: DataProcessor.scanChatFiles and
  usageHistoryService.rebuildFromSessionJsonl exclude .ledger.jsonl
  sidecars (the ledger is not a transcript)
- writer: appendPromptLedgerRecord seals a torn tail before appending
  so a torn fragment cannot fuse with (and destroy) the next record
- tests: pin the new behavior across multi-dangling fail-closed,
  settled-then-queued attribution, valid interleave migration,
  temporal veto, id-less tool-call guard, sidecar lifecycle
  (move/merge/warn-only delete), torn-tail sealing, queued-admission
  flush on shutdown, active-prompt and resume load contracts, and
  ledger exclusion from insight scans
- docs: sync the design doc's reconciliation algorithm, lifecycle, and
  fail-closed invariants

* perf(serve): read only the ledger tail for load-response promptTerminals

readRecentPromptTerminals ran on every POST /session/:id/load (including
attached hot loads) and synchronously read and JSON-parsed the entire
ledger — a multi-megabyte event-loop stall for long sessions on the
per-request hot path.

Add a tailBytes option to readPromptLedgerRecords that reads a trailing
byte window (the first window line is always dropped: the window start
can tear a line in half). The load path now reads a 256 KiB window,
which holds hundreds of ~150-byte records against the 64-terminal
response cap; sessions whose ledger outgrows the window return a
best-effort trailing subset, which the response contract already allows.

* fix(serve): close wrong-terminal attribution classes in cold-load reconciliation

Strengthen the reconcile attribution evidence per review round 2:
measure the temporal evidence on the same api-history projection the
verdict uses, fail closed on a compression checkpoint written after the
target's admission, and require the visible tail to postdate every other
prompt's settled terminal (FIFO evidence). Also fix a TS18048 narrowing
gap in the window test, make the seal test assert the raw file layout,
and restructure the window test so the call-site tailBytes wiring is
actually observable.

* fix(serve): keep ChatRecord import inline so lint-staged cannot merge it into a type-only import

* fix(serve): fail-closed reconciliation on millisecond clock equality and deadline-overlapped turns

* fix(serve): TOCTOU fence before ledger append and documented residual attribution risk

* test(serve): pin the ledger race fixture on the transcript timeline

* feat(serve): bind cold-load evidence to the admission via a dispatch marker

* test(core): pin the ledger sidecar exclusion in usage rebuild

* fix(serve): create ledger sidecar owner-only and fence marker-era compression by position

Round-7 review Criticals:
- appendPromptLedgerRecord created the sidecar with umask-default
  permissions (0o644) while the adjacent transcript is owner-only; the
  ledger now follows the 0o600 convention at creation time.
- Marker-bearing admissions fence post-admission compression by marker
  position instead of wall clock, so a backward clock step cannot hide
  a compression reset that voids the evidence chain.
- Design doc: the residual-risk claim is corrected — the dispatch
  marker binds ordering, not ownership; the two ownership classes that
  survive it (recordless predecessor with continued writes, ledger-less
  cross-client writer) are documented, pending writer identity on
  transcript records (#9483).

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-08-20 07:27:49 +00:00
..
insight feat(serve): persist prompt terminal ledger for cold-load reconciliation (#9426) 2026-08-20 07:27:49 +00:00
prompt-processors fix(core): report signal-terminated shell commands as errors (#8501) 2026-08-07 06:21:41 +00:00
test-commands feat: add extension for gemini and claude 2026-01-07 11:06:17 +08:00
tips feat(tui): Ctrl+O frozen transcript view and unified tool output rendering (#5666) 2026-07-09 23:40:29 +00:00
BuiltinCommandLoader.test.ts feat(cli): add /advisor command for second-opinion conversation review (#7567) 2026-08-17 08:54:18 +00:00
BuiltinCommandLoader.ts feat(cli): add /advisor command for second-opinion conversation review (#7567) 2026-08-17 08:54:18 +00:00
BundledSkillLoader.test.ts feat(review): Include CLI version in attribution (#8294) 2026-08-01 09:40:57 +00:00
BundledSkillLoader.ts feat(review): Include CLI version in attribution (#8294) 2026-08-01 09:40:57 +00:00
command-factory.test.ts feat(cli): core built-in i18n coverage (#3871) 2026-05-10 22:35:03 +08:00
command-factory.ts refactor(cli): revert dynamic slash command LLM translation (#4145) 2026-05-15 16:01:16 +08:00
command-migration-tool.test.ts fix(core): serialize migrated command descriptions as YAML (#5321) 2026-06-19 03:36:59 +08:00
command-migration-tool.ts fix(cli): replace all emoji with Unicode text symbols in TUI rendering (#5999) 2026-06-30 15:18:01 +00:00
commandMetadata.test.ts feat(workflows): finish Dynamic Workflows port — resume, saved workflows, keyword trigger, notifications (#4721) (#5600) 2026-06-22 19:13:14 +08:00
commandMetadata.ts feat(workflows): finish Dynamic Workflows port — resume, saved workflows, keyword trigger, notifications (#4721) (#5600) 2026-06-22 19:13:14 +08:00
CommandService.test.ts feat(skills): support user-invocable frontmatter (#5037) 2026-06-13 06:09:23 +08:00
CommandService.ts feat(skills): support user-invocable frontmatter (#5037) 2026-06-13 06:09:23 +08:00
commandUtils.test.ts feat(cli): Phase 2 — slash command multi-mode expansion, ACP fixes, and UX improvements (#3377) 2026-04-22 19:12:44 +08:00
commandUtils.ts feat(cli): Phase 2 — slash command multi-mode expansion, ACP fixes, and UX improvements (#3377) 2026-04-22 19:12:44 +08:00
FileCommandLoader-extension.test.ts feat(cli): core built-in i18n coverage (#3871) 2026-05-10 22:35:03 +08:00
FileCommandLoader-markdown.test.ts feat(cli): add argument-hint support for slash commands (#3593) 2026-04-27 08:29:50 +08:00
FileCommandLoader.test.ts feat(extensions): support Agent Plugins v1 (#8834) 2026-08-11 19:45:11 +00:00
FileCommandLoader.ts feat(extensions): support Agent Plugins v1 (#8834) 2026-08-11 19:45:11 +00:00
markdown-command-parser.test.ts feat(cli): add argument-hint support for slash commands (#3593) 2026-04-27 08:29:50 +08:00
markdown-command-parser.ts feat(cli): add argument-hint support for slash commands (#3593) 2026-04-27 08:29:50 +08:00
McpPromptLoader.test.ts fix(cli): MCP prompt completion no longer blocks Enter for optional params (#7995) 2026-07-29 13:00:37 +00:00
McpPromptLoader.ts fix(cli): MCP prompt completion no longer blocks Enter for optional params (#7995) 2026-07-29 13:00:37 +00:00
notificationService.test.ts feat(hooks): support terminal sequence notifications (#4895) 2026-06-10 11:25:38 +08:00
notificationService.ts feat(cli): add OSC notification support for iTerm2, Kitty, and Ghostty (#3562) 2026-04-27 20:28:07 +08:00
prompt-stash.test.ts feat(cli): add project-scoped prompt stash (#6709) 2026-07-11 10:36:06 +00:00
prompt-stash.ts feat(cli): add project-scoped prompt stash (#6709) 2026-07-11 10:36:06 +00:00
review-worktree-lease.test.ts fix(review): lock the PR review worktree lease against concurrent sessions (#9211) 2026-08-17 00:48:00 +00:00
review-worktree-lease.ts fix(review): lock the PR review worktree lease against concurrent sessions (#9211) 2026-08-17 00:48:00 +00:00
saved-workflow-loader.test.ts feat(serve): Add daemon workspace voice and control APIs (#5765) 2026-06-24 23:48:57 +00:00
saved-workflow-loader.ts feat(workflows): finish Dynamic Workflows port — resume, saved workflows, keyword trigger, notifications (#4721) (#5600) 2026-06-22 19:13:14 +08:00
setup-github.test.ts feat(serve): Hot-reload workspace trust changes (#7268) 2026-07-25 08:43:07 +00:00
setup-github.ts feat(serve): Hot-reload workspace trust changes (#7268) 2026-07-25 08:43:07 +00:00
skill-args-file.test.ts fix: make the test suite portable on Windows (#8050) 2026-08-01 05:16:58 +00:00
skill-args-file.ts fix: make the test suite portable on Windows (#8050) 2026-08-01 05:16:58 +00:00
SkillCommandLoader.test.ts feat(skills): add auto-skill curator (#7846) 2026-08-01 07:37:20 +00:00
SkillCommandLoader.ts feat(skills): add auto-skill curator (#7846) 2026-08-01 07:37:20 +00:00
types.ts Explict imports & exports with type modifier (#3774) 2025-08-25 22:04:53 +00:00
voice-keyterms.ts feat(cli): support a user-configurable keyterms file for voice dictation (#5817) 2026-06-25 08:32:04 +00:00
voice-model.ts feat(core): add configurable image generation models (#7607) 2026-07-24 09:03:18 +00:00
voice-service.test.ts feat(voice): support trusted private ASR base URLs (#8350) 2026-08-06 14:04:57 +00:00
voice-service.ts feat(voice): support trusted private ASR base URLs (#8350) 2026-08-06 14:04:57 +00:00
voice-settings.ts feat(serve): Add daemon workspace voice and control APIs (#5765) 2026-06-24 23:48:57 +00:00
voice-transcriber.ts feat(voice): support trusted private ASR base URLs (#8350) 2026-08-06 14:04:57 +00:00