[Bug]: context-overflow recovery latches for the whole session — every later turn is rejected locally with action:"none", tokensBefore==tokensAfter, killing goal continuation
Prerequisites
- I will write this issue in English
- I have searched existing issues to avoid duplicates (closest: #7975, #7497, #4701 — see "Relationship to existing issues" below)
- I am using the latest version (
omo 5.0.0-0.beta.67, enginesenpi 2026.9.16-3) - I have read the documentation and could not find the answer
Bug Description
Once a session hits a context overflow that the single compact-and-retry attempt cannot fix, the session enters a permanently terminal state. Every subsequent turn — including every goal-continuation wake and every background-task delivery — is rejected locally, before any compaction is attempted and before the provider is ever contacted again.
The gate is _overflowRecoveryAttempted in packages/coding-agent/src/core/agent-session.ts (~L6626):
if (this._overflowRecoveryAttempted) {
const errorMessage =
"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.";
this._emit({ type: "compaction_end", reason: "overflow", result: undefined, aborted: false, willRetry: false, errorMessage });
if (inlineReason === "pre_prompt") { throw new Error(errorMessage); }
return false;
}The flag has reset sites elsewhere in the file (L2439, L2513, L6802, L6810, L7017, L7073, L7107), but all of them sit downstream of a turn that makes progress. A wedged session never completes a turn, so the budget is never replenished. The result is not "one retry per overflow" — it is "one retry per session, ever", and the session cannot self-heal because no compaction is allowed to run.
My logs show this is not transient. Four consecutive compaction_decision records, spanning 13 hours and multiple session wake-ups, are byte-identical:
action: "none" disposition: "skipped" tokensBefore: 486755 == tokensAfter: 486755action: "none" means no compaction ran; therefore tokensBefore can never move; therefore the next turn takes the same branch. It is a closed loop. There is also no provider_error after the first one — the session never reaches the provider again, so the "Try reducing context or switching to a larger-context model" advice is unreachable from inside the session: switching the model does not clear the latch.
User-visible symptom (what actually ended my session):
Warning: Goal continuation blocked: context overflow ended the turn (compaction did not recover). Send any message to resume.
Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.
Runtime error (send_message): Context overflow recovery failed after one compact-and-retry attempt. ..."Send any message to resume" is misleading — sending a message re-enters the same latched branch and fails identically.
Frequency on this machine: 14 occurrences across 8 distinct days (2026-09-05, 09-07, 09-08, 09-09, 09-10, 09-11, 09-16, 09-17), all on claude-sdk-oauth / claude-opus-5, each immediately preceded by a claude_sdk_oauth_session_* event (session_close, or continuity query_failed / assistant_rewritten / resume_initialization_aborted / prefix_matched).
Steps to Reproduce
- Run a long OMO Native session on
claude-sdk-oauth(claude-opus-5) with an active Goal, so goal continuation keeps waking the session. - Let the session grow until the dispatched request crosses the provider limit. The provider returns
Prompt is too long · the request is ~1010873 tokens (limit 1000000) but this conversation is only ~502132 tokens .... - The first compact-and-retry runs and fails.
_overflowRecoveryAttemptedlatches. - Send any further message, or let goal continuation wake the session.
- Every turn from now on fails instantly with
Context overflow recovery failed after one compact-and-retry attempt, logged asaction: "none",disposition: "skipped",tokensBefore == tokensAfter. No compaction is attempted. This persists across idle-TTL provider-session closes and across many hours.
Expected Behavior
A failed recovery attempt should be a per-overflow budget, not a per-session death sentence:
- The retry budget should be replenished on a new turn / new user message / goal-continuation wake, so each fresh overflow gets its own compact-and-retry.
- A latched session should still be able to attempt compaction.
action: "none"on every subsequent turn guarantees the state can never improve. - If the state really is unrecoverable, the message should say so and offer an actionable escape (e.g. explicit
/compact, fork the session, or drop the oldest N turns) instead of suggesting a model switch that the latch makes ineffective. - Goal continuation should not be silently killed by a latch the user cannot clear from inside the session.
Actual Behavior
The session is permanently dead for all model traffic. Every turn short-circuits locally at the _overflowRecoveryAttempted gate, emits compaction_end with the fatal message, and returns false without running compaction or contacting the provider. The only recovery is to abandon the session and start a new one — which is not discoverable from the error text.
Doctor Output
$ omo doctor
PASS plugin manifest: plugin/package.json
PASS extension: plugin/extensions/omo.js
PASS lsp-daemon runtime: plugin/runtime/lsp-daemon/dist/cli.js
PASS senpi CLI: /home/.../node_modules/@code-yeongyu/senpi/dist/cli.js
PASS senpi version 2026.9.16-3
INFO omo · Edition: Native · Installed: 5.0.0-0.beta.67 (engine: senpi 2026.9.16-3) · Latest: 5.0.0-0.beta.68
INFO Update: bun add --cwd '.../node_modules/omo-ai' -g omo-ai@beta
WARN engine pid <n> (age 2-16:53:37, tty pts/7) started before this payload was installed; it still runs the previous plugin copy
... (10 such warnings for long-running engines; unrelated to this report)
INFO restart those sessions to pick up the installed payload; a running engine is never rewritten in place
INFO memory identities: 72 durable, 3 transient (no repo/); transient run roots: 0Error Logs
From ~/.omo/agent/logs/session.log — the full lifecycle of one wedged session. Note the single provider_error, then four identical local rejections over 13 hours with no further provider contact:
{"ts":"2026-09-16T17:22:34.740Z","event":"provider_error","error":"Prompt is too long · the request is ~1010873 tokens (limit 1000000) but this conversation is only ~502132 toke..."}
{"ts":"2026-09-16T17:28:17.237Z","event":"compaction_decision","reason":"overflow","mode":"auto","action":"none","disposition":"skipped","accepted":false,"skipped":true,"willRetry":false,"tokensBefore":486755,"tokensAfter":486755,"error":"Context overflow recovery failed after one compact-and-retry attempt. ..."}
{"ts":"2026-09-16T17:28:41.668Z","event":"claude_sdk_oauth_session_close","reason":"idle_ttl"}
{"ts":"2026-09-16T17:36:11.071Z","event":"claude_sdk_oauth_session_close","reason":"idle_ttl"}
{"ts":"2026-09-16T17:59:14.966Z","event":"compaction_decision","reason":"overflow","mode":"auto","action":"none","disposition":"skipped","accepted":false,"skipped":true,"willRetry":false,"tokensBefore":486755,"tokensAfter":486755,"error":"Context overflow recovery failed after one compact-and-retry attempt. ..."}
{"ts":"2026-09-17T06:15:54.372Z","event":"compaction_decision","reason":"overflow","mode":"auto","action":"none","disposition":"skipped","accepted":false,"skipped":true,"willRetry":false,"tokensBefore":486755,"tokensAfter":486755,"error":"Context overflow recovery failed after one compact-and-retry attempt. ..."}
{"ts":"2026-09-17T06:15:57.965Z","event":"compaction_decision","reason":"overflow","mode":"auto","action":"none","disposition":"skipped","accepted":false,"skipped":true,"willRetry":false,"tokensBefore":486755,"tokensAfter":486755,"error":"Context overflow recovery failed after one compact-and-retry attempt. ..."}Earlier incidents, same shape, always tokensBefore == tokensAfter and action: "none":
{"ts":"2026-09-05T04:19:59.436Z", ... "tokensBefore":437881,"tokensAfter":437881, ...}
{"ts":"2026-09-05T04:37:15.788Z", ... "tokensBefore":437881,"tokensAfter":437881, ...} <- 17 min later, unchanged
{"ts":"2026-09-07T05:17:51.255Z", ... "tokensBefore":397551,"tokensAfter":397551, ...}
{"ts":"2026-09-07T05:43:49.088Z", ... "tokensBefore":397551,"tokensAfter":397551, ...} <- 26 min later, unchanged
{"ts":"2026-09-09T00:25:02.522Z", ... "tokensBefore":452857,"tokensAfter":452857, ...}
{"ts":"2026-09-09T00:40:43.983Z", ... "tokensBefore":452857,"tokensAfter":452857, ...} <- 15 min later, unchanged
{"ts":"2026-09-10T17:53:20.124Z","event":"provider_error","error":"Prompt is too long (invalid_request)"}
{"ts":"2026-09-11T00:51:18.603Z", ... "tokensBefore":377544,"tokensAfter":377544, ...} <- 7 hours later, unchanged
{"ts":"2026-09-11T05:55:54.862Z","event":"provider_error","error":"Prompt is too long (invalid_request)"}
{"ts":"2026-09-11T12:46:25.155Z", ... "tokensBefore":471894,"tokensAfter":471894, ...} <- 7 hours later, unchangedAdditional Context
Relationship to existing issues. #7975 and #7497 report the accounting half — OMO's injected system prompt / tool schemas / skills catalogue roughly double the request, so the session believes it is at ~486k/1M (safe, below any compaction threshold) while the provider receives ~1,010,873. My data reproduces that exactly (486,755 measured vs ~1,010,873 dispatched = 2.08x), and I am not re-filing it here.
This issue is the recovery half, which I could not find reported: even after the overflow is known, the runtime refuses to attempt compaction again for the rest of the session's life. Fixing the accounting would reduce how often sessions reach this state; it would not stop a session that reaches it from becoming permanently unusable. They are independently fixable and, in my opinion, the latch is the cheaper and more valuable fix — a session that can still compact can still recover, even with bad accounting.
Suggested direction. Scope the retry budget to the overflow event rather than the session: reset _overflowRecoveryAttempted at turn start (or on new user input / goal-continuation wake), and keep a separate guard against tight in-turn loops. A same-turn loop guard is what the current flag appears to have been written for; latching it for the session's lifetime looks like a wider blast radius than intended.
Environment notes. Provider is claude-sdk-oauth with claude-opus-5. All 14 occurrences are on that provider; I have not tried to reproduce on another provider, so I cannot say whether the latch is provider-specific (it reads as provider-neutral in the source).
Operating System
Linux
OpenCode Version
N/A — OMO Native edition (omo 5.0.0-0.beta.67, engine senpi 2026.9.16-3)
Source: code-yeongyu/oh-my-openagent