fix(session): run-mode nudge without a clarification stop, a blocked exit, and the completion contract for run-mode agents

RA
5d ago
+680-65

Issue for this PR

Closes #1640

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Headless altimate-code run runs could end on a question to the user, who isn't there to answer. In an eight-pass headless evaluation, all six turn-ending questions came right after the post-compaction nudge, whose option (2) was "stop and ask for clarification". This PR fixes that, gives every run-mode agent the completion contract, keeps the completion token out of what readers see, and gives a blocked run an honest exit. Interactive sessions are unchanged.

1. The run-mode nudge has no clarification stop, and has a blocked exit (session/termination.ts).
COMPLETION_NUDGE is sent only when ALTIMATE_RUN_MODE is on. It now reads:

Context was compacted; the summary above is the record of the work so far. No one can answer questions during this run, so do not end your turn with a question. Choose exactly one: (1) if concrete next steps remain toward the original task, continue with them; if you are unsure between safe, reversible choices, pick the most reasonable interpretation, state your assumption, and continue; (2) if the deliverable is complete and verified, reply with DONE alone on the final line and stop; (3) if you cannot proceed because something you cannot obtain is missing, or the only way forward is a destructive or irreversible action on a guess, state what was completed, end with a final line BLOCKED: , and stop without DONE.

On an overflow compaction in run mode, the notice before it no longer says "ask the user to re-supply it" (it says no one can re-supply it during this run). The interactive continuation and the interactive overflow notice are byte-for-byte unchanged and pinned by tests.

2. Every run-mode agent except plan gets the completion contract (session/termination.ts, session/prompt.ts).
The builder-only allowlist is gone: custom agents and task subagents get the contract in run mode. plan is excluded (its prompt ends a turn with a question or plan_exit), and a structured-output turn gets none. The contract now reads: do not end the turn with a question; only when the assigned task is fully completed and verified, end with DONE on its own final line; if blocked as above, end with BLOCKED: <what blocked you> and no DONE.

3. The completion token stays out of the output (cli/cmd/run.ts, cli/cmd/run-output.ts, tool/task.ts).
In run mode a trailing DONE line (as detected by isExplicitDone, so fenced or quoted text is untouched) is removed from stdout, from JSON text events and from --output. done_reason is computed from the raw text, so it still reports explicit_done. The task tool removes the child's trailing DONE from <task_result>; when the child's last reply is a bare DONE (the nudge's acknowledgement after a compaction), the result is the child's latest earlier report in that prompt instead of a bare DONE. With ALTIMATE_RUN_MODE=0 nothing is stripped.

4. A blocked run is reported as blocked (cli/cmd/run-accounting.ts, cli/cmd/run.ts).
A stop whose final line is BLOCKED: <reason> gives done_reason=blocked (new value) in the termination record and JSON event, exit code 1, and a stderr line in text mode. Existing exit codes are unchanged for every other outcome. A blocked reply to the idle-done confirm challenge is not treated as a declined challenge.

How did you verify your code works?

New tests, each written first and failing before the fix:

  • test/cli/run/run-mode-output.test.ts (spawns the real run against the mock LLM): {"title":"x"}\n\nDONE gives {"title":"x"} on stdout and in --output; in --format json the text event carries {"title":"x"} and termination reports done_reason: explicit_done; a BLOCKED: reply gives done_reason: blocked and exit 1; with ALTIMATE_RUN_MODE=0 the text is written as given.
  • run-output.test.ts: OutputCollector strips a trailing DONE in run mode, keeps a fenced DONE, and leaves text alone otherwise.
  • run-accounting.test.ts: stop + BLOCKED: line → done_reason=blocked; BLOCKED followed by DONE is a completion; later text clears blocked.
  • task.test.ts: run mode strips the child's DONE from <task_result>; a bare DONE last reply returns the child's earlier report; outside run mode the text is unchanged.
  • prompt.test.ts (real prompt loop, mock LLM): a custom primary agent carries the contract in run mode; plan does not. The existing custom-subagent run/interactive cases still pass.
  • termination.test.ts: nudge has three options, no "clarification", the safe/reversible limit and the BLOCKED: ending; contract wording; plan exclusion; isBlocked and stripCompletionToken cases; run-mode overflow notice; interactive overflow notice pinned byte-for-byte.
  • compaction-summarizer-integrity.test.ts: a run-mode overflow compaction injects the run-mode notice without "ask the user"; an interactive overflow compaction injects the original notice followed by the original continuation, byte-for-byte.

No-regression check: bun test test/session test/tool test/agent test/cli on the branch head after merging main, before any change, and again after.

  • Before (3 fail): opencode mcp add (non-interactive subprocess) > adds a remote server with HTTP headers; opencode run: JSON output artifacts > --time-budget cuts off a running tool and finalizes once (no answer); opencode run: JSON output artifacts > --time-budget delivers one soft directive at the next slow-tool boundary.
  • After (2 fail, 3331 pass): the same two --time-budget cases (known timing-sensitive). The mcp add headers case passed on this run. No new failures.

Typecheck clean; script/check-tracker-leaks.ts and script/upstream/analyze.ts --markers --strict pass.

Gateway run: one headless run on the real gateway is described in this comment. It ran on a local branch that stacked this PR with #1631, #1632, #1639 and #1643, so it does not isolate this PR; it stopped at the token cap without finishing and did not exercise the DONE or blocked paths. It predates the blocked ending and the output stripping added here.

Cost side effects

Small increase per run-mode request for agents other than builder and plan: the completion contract adds a fixed paragraph (about 110 tokens) to the system prompt. It is constant for the session, so it is cached with the rest of the prefix. Builder's contract text changed, which changes its run-mode prefix once. Interactive sessions are unchanged. Runs that would have ended on an unanswerable question, or on a false DONE when blocked, should now finish or stop early with a clear reason.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code


Summary by cubic

Fixes headless altimate-code run sessions ending on questions nobody can answer, and makes a blocked run a first-class exit instead of a silent or false DONE.

  • The run-mode post-compaction nudge drops the "ask for clarification" option and now tells agents to continue (with explicit assumptions), reply DONE, or end with BLOCKED: <reason>.
  • The DONE completion contract now applies to every agent in run mode except plan; interactive sessions and structured-output turns still get none.
  • A trailing DONE is stripped from run-mode stdout, JSON text events, --output, and <task_result>; a bare-DONE acknowledgement falls back to the child's latest report.
  • A final BLOCKED: line yields done_reason=blocked, exit 1, and a stderr note in text mode; a blocked reply to the idle-done challenge is no longer treated as a decline.
  • The run-mode overflow notice no longer asks the absent user to re-supply anything; the interactive notice and continuation are unchanged.

Summary by CodeRabbit

  • New Features
    • Run mode now recognizes BLOCKED: <reason> responses, reports them as blocked, and exits with a failure status.
    • Completion guidance and follow-up prompts now distinguish completed work, blocked work, and work that should continue.
  • Bug Fixes
    • Run-mode output omits trailing DONE markers from displayed text and JSON text events while preserving them in output files as appropriate.
    • Task reports can fall back to an earlier assistant response when the latest response contains only a completion marker.