Repository navigation
fix(session): run-mode nudge without a clarification stop, a blocked exit, and the completion contract for run-mode agents #1641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
da56132
9ec28fe
2760562
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -651,8 +651,10 @@ You are speaking to a non-technical business executive. Follow these rules stric | |
| - "Query Duration" not "total_elapsed_time" — "Data Processed" not "bytes_scanned" — "Monthly Cost" not "credits_used * 3.00"` | ||
|
|
||
| async function execute(sdk: OpencodeClient) { | ||
| // altimate_change start — collect completed assistant text for run output | ||
| const outputParts = new OutputCollector() | ||
| // altimate_change start — collect completed assistant text for run output; in run mode the | ||
| // completion token is stripped at every output boundary | ||
| const stripDone = Flag.ALTIMATE_RUN_MODE | ||
| const outputParts = new OutputCollector({ stripCompletionToken: stripDone }) | ||
| // altimate_change end | ||
| // altimate_change start — a turn must end with text (#1334). Track whether | ||
| // the assistant said anything at all, and the last tool failure, so a | ||
|
|
@@ -1224,15 +1226,18 @@ You are speaking to a non-technical business executive. Follow these rules stric | |
| } | ||
| // altimate_change end | ||
| // altimate_change start — upstream_fix: --output collects text in JSON mode too | ||
| const text = part.text.trim() | ||
| if (args.output && text) { | ||
| outputParts.text(text, { | ||
| if (args.output && part.text.trim()) { | ||
| outputParts.text(part.text, { | ||
| partID: part.id, | ||
| synthetic: part.synthetic === true, | ||
| compaction: accounting.isCompactionStep(part.messageID), | ||
| }) | ||
| } | ||
| if (emit("text", { part })) continue | ||
| // Run mode: stdout and JSON text events carry the answer without the | ||
| // completion token; done_reason still records it. | ||
| const shown = stripDone ? SessionTermination.stripCompletionToken(part.text) : part.text | ||
| if (emit("text", { part: shown === part.text ? part : { ...part, text: shown } })) continue | ||
|
Comment on lines
+1238
to
+1239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const text = shown.trim() | ||
| if (!text) continue | ||
| // altimate_change end | ||
| if (!process.stdout.isTTY) { | ||
|
|
@@ -2006,7 +2011,10 @@ You are speaking to a non-technical business executive. Follow these rules stric | |
| // branch with a normal text-only stop. That has already returned from | ||
| // SessionPrompt.loop, so enqueue one explicit continuation turn instead | ||
| // of silently finalizing the run at rc 0 with done_reason=none. | ||
| if (!accounting.fatal && !challengeConfirmed) { | ||
| // altimate_change start — a blocked reply to the challenge is a final report, not a decline | ||
| const challengeBlocked = accounting.termination().done_reason === "blocked" | ||
| if (!accounting.fatal && !challengeConfirmed && !challengeBlocked) { | ||
| // altimate_change end | ||
| NudgeArbiter.register(sessionID, { | ||
| source: "termination_challenge", | ||
| kind: "continue_after_decline", | ||
|
|
@@ -2232,6 +2240,15 @@ You are speaking to a non-technical business executive. Follow these rules stric | |
| // (not process.exit) so pending stdout/trace writes still flush. | ||
| if (accounting.fatal) process.exitCode = 1 | ||
| // altimate_change end | ||
| // altimate_change start — a blocked run did not do its task: exit nonzero and say why | ||
| // (done_reason=blocked is also in the termination record/JSON event). | ||
| if (!accounting.fatal && termination.done_reason === "blocked") { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This exit path ignores the Prompt for AI agents |
||
| process.exitCode = 1 | ||
|
Comment on lines
+2245
to
+2246
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (args.format !== "json") { | ||
| process.stderr.write("\n! run blocked: the model reported it cannot proceed (done_reason=blocked)\n") | ||
|
Comment on lines
+2243
to
+2248
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
| // altimate_change end | ||
| } | ||
|
|
||
| if (args.attach) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1765,16 +1765,18 @@ When constructing the summary, try to stick to this template: | |
| : "" | ||
| // altimate_change end | ||
| // altimate_change start — completion-aware termination path: | ||
| // (b) the continue message carries the three-option completion-aware nudge | ||
| // (continue / ask for clarification / assert DONE), giving a finished | ||
| // session a termination path. Delivered via the NudgeArbiter (Global | ||
| // (b) the run-mode continue message carries the completion-aware nudge | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The run-mode compaction nudge is not gated on the Prompt for AI agents |
||
| // (continue / assert DONE / report BLOCKED — no clarification option, | ||
| // since nobody can answer in a headless run), giving a finished or | ||
| // blocked session a termination path. Delivered via the NudgeArbiter (Global | ||
| // rule 5): this injection point registers its candidate and takes the | ||
| // single winner, so pending lower-precedence directives (starvation | ||
| // breaker, budget reminder) are consumed here and the injected turn | ||
| // never carries two system-authored directive blocks. The termination | ||
| // nudge has top precedence, so it always wins at this site. | ||
| // (d) the overflow notice is mechanism-accurate — the old text falsely | ||
| // blamed "large media attachments" (see SessionTermination.OVERFLOW_NOTICE). | ||
| // blamed "large media attachments" (see SessionTermination.OVERFLOW_NOTICE); | ||
| // in run mode it does not ask the absent user to re-supply anything. | ||
| let continuation = | ||
| "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." | ||
| if (Flag.ALTIMATE_RUN_MODE) { | ||
|
|
@@ -1791,7 +1793,9 @@ When constructing the summary, try to stick to this template: | |
| NudgeArbiter.take(input.sessionID, input.nudgeGeneration)?.text ?? SessionTermination.COMPLETION_NUDGE | ||
| } | ||
| const text = | ||
| (input.overflow ? SessionTermination.OVERFLOW_NOTICE + "\n\n" : "") + | ||
| (input.overflow | ||
| ? SessionTermination.overflowNotice({ runMode: Flag.ALTIMATE_RUN_MODE }) + "\n\n" | ||
| : "") + | ||
| continuation + | ||
| // altimate_change end | ||
| // altimate_change start — state ledger | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Attached sessions can strip a remote interactive response's literal
DONEwhen the local environment hasALTIMATE_RUN_MODE=1. Exclude--attachfrom this local output transformation.Prompt for AI agents