fix(ask-user): restore dangling questions on startup opens

Merged
SU
3d ago
+104-3

Fixes #4764

Bug

A pending ask_user_question (waitForAnswer: true) killed mid-wait is re-asked when a running client resumes the session with RPC switch_session, but NOT when a restarted CLI opens the same session file with --session <file> — the path a user most often takes after a crash.

Root cause

The CLI's initial runtime passes no explicit session_start event, so it defaults to reason startup (packages/coding-agent/src/core/agent-session.ts). resumeDanglingQuestion (packages/coding-agent/src/core/extensions/builtin/ask-user/resume.ts) returned early unless the reason was resume or reload, so the restore never ran on the CLI restart path.

Change

  • resume.ts: also run on reason startup. This is safe: a fresh session has no branch entries, so findDanglingQuestions finds nothing there, while a reopened file restores exactly like the switch_session path (same resumed-entry guard prevents a second re-ask). --continue / --resume pickers that open an existing file go through the same initial runtime, so they are covered too.
  • resume.ts: diagnostic sizes-only startup (sizesOnly: true, which starts no turn) still restores nothing, matching the early returns in the other builtins.
  • test/suite/ask-user-resume.test.ts: regression test re-presenting a dangling question once on startup (mirrors the existing resume case, including the no-double-ask second start), plus a sizes-only startup test asserting no restore. The new tests live in the existing owner file rather than suite/regressions/ because the dangling-session harness (danglingSession/install/emitStart) is local to that file; happy to move them if preferred.

Local tests (real runs)

  • New regression test run against pre-fix code first: FAILED as expected (question called 0 times on startup).
  • After the fix: test/suite/ask-user-resume.test.ts — 13 passed (11 pre-existing + 2 new).
  • Related: test/suite/ask-user-extension.test.ts + test/characterization/crash-recovery-today.test.ts — 22 passed.
  • oxlint on both changed files: 0 warnings, 0 errors. bun fmt --check on both files: clean.

Not run (left for CI / follow-up)

  • Full bun run check and the senpi-qa real-CLI channel were not run in this session; CI covers them.
  • Issue #4764 also mentions updating the X13 characterization test and the EQ E25 golden together with the fix. E25 drives the pending-question kill through the switch_session path only, so its assertions are unaffected by this change; a real-CLI kill test that reopens with --session (startup path) is still missing and is follow-up, not claimed here.

Summary by cubic

Restores pending ask_user_question prompts when a restarted CLI opens an existing session file, since those opens arrive with session_start reason startup and the restore logic previously handled only resume and reload. Running restore on startup is safe because a fresh session has no branch entries, and diagnostic sizesOnly startup restores nothing since no turn starts.

The follow-up also makes headless startup skip restore entirely: print mode (-p / --mode json) has no question UI, so restoring there would settle the question as orphaned-after-restart, start an unasked model turn, and write the ask-user:resumed marker, blocking a later interactive re-ask. A CLI --fork <file> also arrives as startup with copied branch entries, so it re-presents the source's dangling question (unlike runtime /fork, excluded); the event carries no signal to tell a forked startup from a plain restart, so this divergence is documented, not filtered.

Bug Fixes

  • Includes startup in the reason guard in resume.ts, skips when sizesOnly is set, and skips startup restores without a UI.
  • Adds regression tests for startup restore, sizes-only no-restore on startup and resume, and headless no-restore.
  • Adds a changelog fragment documenting the fix and its rationale.