feat(own-loop): final-summary turn after a stuck-loop exit - #79
Merged
Conversation
When the own-loop exits because the model got stuck (doom-loop, no-text-loop, or text-loop), users currently see only the harness's own '[Stopped: ...]' notice and no real conclusion. The auto- continuation block was deliberately gated to skip these exit reasons (restarting a stuck model produces more of the same), but that left a gap: the model's tool calls produced real information that just never got written up. This change adds a single bounded no-tools streamText call after those exits. The model has no tool choice — it's forced to produce text. The injection tells it what to write: (1) what it was trying to do, (2) what it actually found, (3) what it would have done next. Bounds: 30s timeout, 800 max output tokens. Aborted on the outer signal. Wrapped in try/catch so failure here doesn't kill the already-finished turn. Skipped on: - abort (user asked to stop) - natural / step-limit / budget-limit exits (auto-continuation handles those or the model already wrapped up) - when the model already wrote >=200 chars of non-harness-notice text (no point asking for more) Decision logic extracted to src/final-summary-policy.ts as a pure function (shouldRunFinalSummary) plus a helper that strips the harness's own bracketed notices before measuring 'real' text length. ## Tests - 16 unit tests covering all branches of shouldRunFinalSummary and stripHarnessNotices: edge cases (abort, natural exit, custom threshold), happy paths (stuck exit + empty text → true), negative paths (model already wrote a conclusion). - All 38 existing compaction + prune + loop-detection tests still pass. Typecheck clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When the own-loop exits because the model got stuck (doom-loop, no-text-loop, or text-loop), run one more bounded streamText call with no tools to elicit a real conclusion from the model. Without this, users saw only the harness's own
[Stopped: ...]notice and no real answer — even though the prior tool calls had often produced useful information that just never got written up.Why
Reproduced today on Gemma 4 26B after PR #78 landed. Two sessions hit the same-tool repetition hard-break at step 10/13:
Both ran 10 productive codemode calls each — the model had real findings — but never converted them into a final text response. PR #78 stops the loop cleanly; this PR closes the loop on the user-visible side.
How
After the loop exits and
exitReasonis tagged, check if a stuck signal fired and the model never wrote a real conclusion. If so, run a singlestreamTextwith:tools: {}— empty, so the model can ONLY write textBounded and one-shot. Failure (timeout, network error, etc.) is non-fatal — the existing
[Stopped: ...]notice is already in the response.Skipped on
natural/step-limit/budget-limitexits (auto-continuation handles those, or the model already wrapped up naturally)The
<200 charsthreshold deliberately scrubs the harness's own bracketed notices before measuring, so a 60-char[Stopped: ...]doesn't count as "model wrote text".Tests
src/final-summary-policy.tsexports two pure functions:stripHarnessNotices(text)— removes[Stopped: ...],[Compacting context ...],[Loop detected ...]shouldRunFinalSummary(inputs)— the boolean decisionLinked
memory/learnings/dodo-orchestrator-model-matters.mdbeep-boop-🤖