Skip to content

fix: forks overflow the model's window and then stay stuck on "Prompt is too long" - #381

Merged
saucam merged 1 commit into
mainfrom
fix/fork-seed-overflow
Oct 15, 2026
Merged

saucam merged 1 commit into
mainfrom
fix/fork-seed-overflow

Conversation

@saucam

@saucam saucam commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

What happened

The interop-testing (fork) session failed on its very first message with "Prompt is too long", and then on every message after that.

When you fork a session, codeoid copies the parent's conversation into the new session's first prompt. It decides how much fits by assuming 3.5 characters per token. This conversation was mostly code and tool output, which comes out closer to 2.25. So a history sized to fit a 1M-token window (2.36M characters) was really about 1.09M tokens, and the request was rejected.

The session then couldn't recover. Each later message resumed the same backing Claude session, which still held the oversized prompt. /rotate, the usual way out, refused to run because the session had "only 2 turns".

What changes

  • The carried-over history is sized realistically. The estimate is now 2 characters per token. On a 1M window that caps the history at about 1.4M characters, leaving room for the system prompt and tools.
  • An overflowing fork retries itself. If the first turn after a fork or backend switch is rejected as too long, the session starts a fresh backing session, rebuilds the history at half the size, and re-runs the user's message as the same turn, without posting it twice. It tries up to 3 times and posts a note each time. This is safe because nothing runs on a rejected request. If all attempts fail, the error says to use /rotate.
  • /rotate works on a stuck session. It skips its minimum-turn check when the last turn overflowed or the session is in an error state. This also holds after a daemon restart, so existing stuck sessions can be rotated once this is deployed.
  • The sizing is configurable. New session settings, also shown under advanced in the settings UI:
Key Env var Default
seedBudgetChars CODEOID_SEED_BUDGET_CHARS 0 (worked out from the window)
seedCharsPerToken CODEOID_SEED_CHARS_PER_TOKEN 2
seedWindowFraction CODEOID_SEED_WINDOW_FRACTION 0.7

CODEOID_SEED_BUDGET_CHARS already existed as a .env-only setting. It is now a config key too, and the env var still takes precedence.

Testing

  • New src/tests/session-seed-overflow.test.ts (9 tests): the retry with halving, giving up after 3 attempts, /rotate after an overflow (including after a restore), /rotate still refused on a healthy young session, no retry when no history was carried over, and the config settings.
  • Typecheck and lint are clean. Full suite on top of current main: 3127 passed, 0 failed.
For agents
  • src/daemon/providers/context-windows.ts: SEED_CHARS_PER_TOKEN changed from 3.5 to 2. seedBudgetChars() takes SeedBudgetOptions (capChars, charsPerToken, windowFraction). Precedence: CODEOID_SEED_BUDGET_CHARS at call time, then opts.capChars, then the computed value. Adds isPromptTooLongError().
  • src/daemon/session.ts:
    • #seedProviderFromHistory(retry?) records #pendingSeed (historyLength, budgetChars, attempts), but only for transcript seeds a warm backend actually stashed (outcome.truncation is set).
    • #sendInner stamps prompt, sender and turn (id/text/at) on the pending seed when the first fresh turn starts.
    • In the turn_done handler: an error that is prompt-too-long (from errorMessage, or this turn's committed assistant text) with a pending seed and attempts < SEED_OVERFLOW_MAX_RETRIES sets #seedOverflowRetry and skips the error message and #lastTurnError. The finally in #consumeEvents then calls #scheduleSeedOverflowRetry().
    • The retry is chained on #sendChain. It tears the provider down, calls resetToNewSession and persists the new backing id, truncates the accumulator to historyLength, and re-seeds at half the budget. It then restores #currentTurnId to the original turn (already indexed) and calls runTurn with the original prompt.
    • A successful turn clears #pendingSeed.
    • manualRotate bypasses min-turns when #promptTooLong is set or status is error. restoreScrollback sets #promptTooLong when the last assistant row is the overflow.
  • src/config.ts: adds session.seedBudgetChars / seedCharsPerToken / seedWindowFraction to the schema, the type and ENV_OVERRIDES.
  • src/daemon/settings/manifest.ts: the env-only CODEOID_SEED_BUDGET_CHARS field is replaced by three config-backed fields.

🤖 Generated with Claude Code

… is too long"

A fork carries the parent's conversation into its first prompt, sized at 3.5
chars/token. Code and tool output run ~2.25, so a 2.36M-char seed came to
~1.09M tokens against a 1M window. Every later send resumed the backing
session holding that prompt and failed the same way, and /rotate refused to
run because the session had too few turns.

- Size the seed at 2 chars/token, configurable via session.seedBudgetChars /
  seedCharsPerToken / seedWindowFraction (and CODEOID_SEED_* env vars).
- When a seeded first turn overflows, retry it on a fresh backing session
  with a halved seed (up to 3 times) without re-posting the user's message.
- Let /rotate skip its min-turns guard after an overflow or in error state,
  including after a daemon restart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@saucam

saucam commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator Author

pr-shepherd: CI is green at 4138401 (daemon-check, web-check, Socket Security x2 all SUCCESS). Branch is current with main. No auto-fixes were needed, nothing pushed. Ready for human review.

@saucam
saucam merged commit 4da1cdf into main Oct 15, 2026
4 checks passed
saucam added a commit that referenced this pull request Oct 17, 2026
…on Claude and Codex (#388)

* feat: forks, switches and rewinds continue natively on Claude and Codex

A fork, backend switch or rewind used to hand Claude, Codex and pi a text
transcript in the first prompt (tool I/O cut to 2,000 chars, reasoning
dropped), even when nothing changed backend. Now the session asks the
backend to continue natively first (#387):

- same backend: the backend forks its own conversation at the turn
  (Claude Agent SDK forkSession; Codex thread/fork), from a native point
  every turn now records (NormalizedTurnResult.nativeRef, stamped on the
  canonical turn) — nothing converted or cut, reasoning kept;
- another backend: native history built from the canonical turns — a
  Claude session file with real tool_use/tool_result blocks, verified with
  the SDK's own reader before use, or Codex items via thread/inject_items.

The transcript is the fallback (pi, the Gemini CLI, any failure), the
session says which way it went, and session.nativeContinuation /
CODEOID_NATIVE_CONTINUATION turns it off.

Also fixes Codex never saving its thread id, which silently emptied every
Codex conversation on a daemon restart, and marks a switched-to backend
fresh until its first prompt so a restart re-seeds it.

Closes #387

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: native continuation round 1 — never fork past a redaction, keep restored turns quoted, pin and validate fork points

Security review: a native fork copies the backend's own log, which still
holds what a tool_result hook redacted from codeoid's record, so a fork is
not used once any carried tool call was redacted (codeoid's record is
rebuilt natively instead). Turns rebuilt from a displayed transcript (an
import) stay a quoted transcript rather than native history in the
assistant's voice. Native refs are shape-checked, and Claude's fork lookup
is pinned to the source session's project dir. The session file is written
with wx into an owner-only dir and its temp file removed on failure. The
Codex fallback notice no longer forwards backend error text to clients.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: native continuation round 1 — identity on native forks, no fork into an overflow, recovery reseeds

Correctness review:
- A fork that adopted a native backing counted as having queried, so its
  ZeroID agent identity was never registered and model calls fell back to
  the shared badge. Identity now registers on the first send after any
  native continuation.
- No native fork from an errored, interrupted or budget-stopped turn (no
  nativeRef is stamped), nor when the source's last context was within 85%
  of the target's window: a fork stays the way out of an overflowed session
  (#381), with budgeted native history instead.
- A Claude fork is read back before it is adopted, and Claude's
  'No conversation found' recovery now reseeds the new session from the
  conversation instead of silently keeping only the latest prompt.
- Stale turn anchors are cleared on a session reset or loop rebuild; a
  rewind to the start clears the native flag; long workdir paths are named
  exactly as the SDK names them; Codex anchors only completed turns, forks
  with excludeTurns, and can't adopt a thread after a teardown.
- Settings UI exposes session.nativeContinuation; docs cover qwen and how
  this sits with CODEOID_CONTEXT_STRATEGY.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: native continuation round 2 — redaction sticks to the backing, durably; imported sessions may still fork

- A tool_result redaction now marks the BACKING, not just the call: every
  later turn of it carries native.redacted, the set is written to the
  session meta the moment a redaction happens (so a crash before the turn
  ends can't forget it), restored on resume, and inherited by forks. A
  partial restart load, log compaction or a squeezed giant turn can no
  longer hide the redaction from the fork guard.
- Turns rebuilt from a displayed transcript block only BUILDING native
  history (allowHistory: false); a fork of the backend's own record is still
  allowed.
- With no usage on record, whether a fork fits is estimated from the
  history instead of assumed.
- Claude recovery: a transcript seed it applies records the prompt it rides
  (so an overflow retries there), and adopting a backing doesn't re-arm
  recovery.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: native continuation round 3 — backing recovery can't loop, redaction flag can't leak into the next turn

- Claude's 'No conversation found' recovery latch no longer clears when the
  backing is reset (recovery resets it itself, then may adopt a resumable
  native backing): it clears only after a turn on the new backing succeeds.
  If that backing is missing too, the turn now fails instead of recovering
  in a loop. (The round-2 guard in #adoptBacking was dead: the reset ran
  before it.)
- The per-turn redaction flag is cleared when a new turn starts, so a
  redaction in a turn that never reached turn_done can't mark the next
  turn's backing.
- A late tool result can't rewrite a destroyed session's meta.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* fix: test that backing recovery re-arms after a successful turn, and keep the zero-turn docs on their tests

Round-4 audit: clean. Folds its two cheap lows.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* docs: native continuation final-audit pass — docs and comments match the code

providers-pi.md no longer says Claude can't take native history; FEATURES
states the imported-turns rule as it is (never built into native history,
a later real turn may still fork) and only claims a notice on fork/switch;
COMPARISON and CHANGELOG name every transcript backend and the recovery
fix; #seedProviderFromHistory and the Codex header describe the native
path. On a switch the continuation notice now follows the switch line,
and its transcript wording no longer says the backend can't (the feature
may be off). Codex's thread config is one helper; small type and naming
tidy-ups.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
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.

2 participants