Skip to content

feat(own-loop): final-summary turn after a stuck-loop exit - #79

Merged
jonnyparris merged 1 commit into
mainfrom
fix/final-summary-after-hardbreak
May 25, 2026
Merged

feat(own-loop): final-summary turn after a stuck-loop exit#79
jonnyparris merged 1 commit into
mainfrom
fix/final-summary-after-hardbreak

Conversation

@jonnyparris

Copy link
Copy Markdown
Owner

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:

session 9f37534e (auto-nudge): hard-break, tokenInput=262k, content="[Stopped: codemode called 10 times in a row...]"
session fd8c6595 (skills UI):  hard-break, tokenInput=339k, content="[Stopped: codemode called 10 times in a row...]"

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 exitReason is tagged, check if a stuck signal fired and the model never wrote a real conclusion. If so, run a single streamText with:

  • tools: {} — empty, so the model can ONLY write text
  • A pure-text system injection: "write your final answer now, summarise what you were trying to do, what you found, what you'd do next"
  • 30s timeout via a chained AbortController
  • 800 max output tokens

Bounded and one-shot. Failure (timeout, network error, etc.) is non-fatal — the existing [Stopped: ...] notice is already in the response.

Skipped on

  • Abort (user asked to stop)
  • natural / step-limit / budget-limit exits (auto-continuation handles those, or the model already wrapped up naturally)
  • When the model already wrote >=200 chars of non-harness-notice text (no point asking for more)

The <200 chars threshold deliberately scrubs the harness's own bracketed notices before measuring, so a 60-char [Stopped: ...] doesn't count as "model wrote text".

Tests

  • New src/final-summary-policy.ts exports two pure functions:
    • stripHarnessNotices(text) — removes [Stopped: ...], [Compacting context ...], [Loop detected ...]
    • shouldRunFinalSummary(inputs) — the boolean decision
  • 16 unit tests cover all branches: abort, natural exit, custom threshold, harness-notice scrubbing, stuck exit with empty/short/long text
  • All 38 existing tests still pass; typecheck clean

Linked

beep-boop-🤖

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.
@jonnyparris
jonnyparris merged commit 50f2c71 into main May 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant