Skip to content

[Bug]: OpenCode thread permanently wedged by "Insufficient context allowance" after a failed turn; /compact cannot recover it #19875

Open

Description

@lageorgem

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. On the OpenCode provider, run a long thread on one native session until T3 has stored a few hundred KB of history for it. Command output counts toward this. Here it was ~354 KB across 21 runs.
  2. Let one turn fail before the provider accepts it (ProviderAdapterTurnStartError, "The provider could not start this turn"). Here this followed a turn whose end could not be recorded (see Logs).
  3. Send any further message, for example "continue".
  4. Run /compact, then send another message.
  5. Switch the model (opus-5-5 → fable-5-1, both via opencode) and send again.

No provider or instance switch is involved; this is the same opencode instance and the same native session throughout.

Expected behavior

  • A ~1 KB replay of a few failed user messages should fit, or be dropped or deferred instead of failing the turn.
  • /compact, as the error message suggests, should let the next turn proceed.

Actual behavior

Every turn after step 2 fails within ~250 ms with:

Insufficient context allowance for the provider handoff. Compact the target conversation or use a larger-context model; the current request has not been truncated.

/compact completes, but the next turn fails the same way. Switching the model doesn't help either. The thread can't be recovered in the app: each failed message becomes another missed run, so the loop feeds itself.

Root cause, traced through statev2.sqlite, server.trace.ndjson* and the bundled server code:

  1. Failed runs that never reached the provider become missed runs. A failed run with no provider turn is added to missedRuns, so the next turn builds a delta_since_target_last_seen handoff to replay it. Here that was runs 15, 16, 18–21, about 1 KB in total: user messages plus the error items.
  2. The handoff budget is 0 because opencode reports no usage. The opencode provider turns report turnTokenUsage.usageStatus: "unavailable". As a result latestNativeContextUsage finds nothing and usedTokens is undefined. handoffBudget then uses nativeContextEstimate, which is the byte length of all stored T3 history for that native thread. That came to ~354 KB here, of which command output was 274 KB, and it is counted as if each byte were a token:
    max(0, min(16000, window − nativeEstimate − msg − max(16k, window/4)))
    
    This is 0 for any window under about 472k. At 0, not even the coverage text fits, so deliverContextHandoffs throws ContextHandoffBudgetError.
  3. Compaction can't move either input. /compact runs with deferInline, so it skips the handoff and succeeds. It doesn't clear missedRuns, and it doesn't change nativeContextEstimate, because that comes from T3's projection, not from the compacted native session. The compaction turn also reports usage as unavailable.

The advice in the error message ("Compact the target conversation") therefore cannot work for opencode threads.

Impact

Blocks work completely

Version or commit

0.0.46-nightly.20261015.3145

Environment

macOS 26.6.2 (arm64), T3 Code (Nightly) desktop app, opencode 1.19.0, models anthropic/claude-opus-5-5 and anthropic/claude-fable-5-1 through opencode, full-access runtime

Logs or stack traces

# Trigger: the end of the preceding long turn (run 14) could not be recorded
2026-10-15T11:36:34Z orchestrationV2.EventSink.write Failure
ProjectionStoreApplyEventError: Failed to apply orchestration projection event provider-turn.updated.
  [cause]: SqlError/UniqueViolation: UNIQUE constraint failed:
    orchestration_v2_projection_provider_turns.provider_thread_id, orchestration_v2_projection_provider_turns.ordinal
# -> provider turns for runs 12 and 14 are still status=running, completedAt=null, turnTokenUsage=null

# Run 15: ProviderAdapterTurnStartError ("The provider could not start this turn")
# Runs 16, 18, 19, 20, 21 (11:37:19 to 11:39:26Z), all in ~200 ms:
orchestrationV2.deliverContextHandoffs Failure
ContextHandoffBudgetError: Insufficient context allowance for the provider handoff. ...
    at orchestrationV2.deliverContextHandoffs (Nightly)
# Run 17 (/compact) completed in between; the provider turn has turnTokenUsage.usageStatus = "unavailable"

# Projection state for the thread
provider_turns (ordinal, status, run):
  9  running    run 12   (run failed)
  10 completed  run 13   usageStatus=unavailable
  11 running    run 14   (run failed; this is the update that hit the unique violation)
  12 completed  run 17   (/compact) usageStatus=unavailable
runs 15, 16, 18-21: status=failed, no provider turn  -> missedRuns
provider_threads.contextUsage = null
Sum of historicalMessage() bytes for the thread: 354,570 (command_execution 274,308; assistant_message 77,026)

Suggested fixes (independent of each other):

  • Don't throw for an optional replay. When the only pending handoff is a delta_since_target_last_seen of failed runs that never reached the provider, drop or defer it (or send only the coverage pointer to t3_thread_read) rather than failing the turn.
  • Reset or ignore the estimate after compaction. After a successful compactThread on a native thread, stop using byte-summed T3 history as the occupancy estimate, or treat it as 0 until real usage is reported.
  • Use opencode's real numbers. Map opencode token usage to usedTokens/maxTokens, or at least don't count raw bytes as tokens.
  • Fix the ordinal collision. Investigate the provider_turns (provider_thread_id, ordinal) unique violation, which leaves turns stuck as running.

Related: #15997 (same wedge, triggered by an Antigravity instance switch) and #12931 (Pi; the same 128k-default and byte-estimate budget collapse).

Workaround

Fork the thread. The new native session starts with nativeContextEstimate = 0, so the budget is the full 16,000 and the handoff succeeds. T3CODE_CONTEXT_HANDOFF_TOKEN_CAP does not help, because the budget is clamped to 0 by the window calculation before the cap applies.

Activity

  1. juliusmarminge commented on Oct 15, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the detailed trace. The wedge itself is what open PR #19414 fixes: an oversized context handoff is deferred instead of raising ContextHandoffBudgetError, so the turn proceeds and the handoff is retried once it fits. I've linked this issue to that PR so it closes when it lands. #16147 is the same failure on OpenCode with a different trigger (unknown model window after a delegate_task result).

    The budget being 0 because OpenCode reports usage as unavailable (so the byte-length estimate stands in for tokens) is a separate accuracy problem. If the thread still misbehaves once #19414 is in a nightly, please comment here and we'll reopen that part.

  2. lageorgem commented on Oct 15, 2026

    @lageorgem
    Author

    @juliusmarminge

    Update: the trigger is resuming an opencode subagent, and it reproduces in a fresh fork

    I forked the thread to recover it. The fork failed the same way within its first turn. Both failures share one trigger.

    Trigger: the lead agent resumes an existing subagent session by passing opencode's Task tool task_id. This sends a second prompt into the same child session. T3 then writes a provider-turn.updated for the child's second turn using ordinal 1 again. That collides with the child's first turn on UNIQUE(provider_thread_id, ordinal). The EventSink.write failure then fails the parent turn ("Provider turn failed.").

    Parent thread Child native session (subagent "Wire bounded window into host") Child's existing provider turn
    Original 4c5716b5…, run 14 ses_ec0a70275ffemMJ0KJEwxTU6UM ordinal 1, completed 11:36:13
    Fork f4bc3d47…, run 1 ses_ec09b890bffes49xaBlE4s3vnY ordinal 1, completed 11:48:55
    2026-10-15T11:36:34Z orchestrationV2.EventSink.write thread_id=thread:provider:opencode:native-thread:ses_ec0a70275ffemMJ0KJEwxTU6UM
    2026-10-15T11:49:09Z orchestrationV2.EventSink.write thread_id=thread:provider:opencode:native-thread:ses_ec09b890bffes49xaBlE4s3vnY
      ProjectionStoreApplyEventError: Failed to apply orchestration projection event provider-turn.updated.
        UNIQUE constraint failed: orchestration_v2_projection_provider_turns.provider_thread_id, orchestration_v2_projection_provider_turns.ordinal

    What follows:

    1. T3 marks the parent run as failed, and its provider turn stays running. opencode keeps executing the turn. The provider event log shows the resumed subagent still editing files (file.edited at 11:50:03, 11:50:43 and 11:51:37) after T3 reported the failure at 11:49:09. This is unobserved work changing the workspace.
    2. The next user message arrives while the native session is still busy. It fails with ProviderAdapterTurnStartError ("The provider could not start this turn").
    3. That failed run never reached the provider, so it becomes a missed run. On the original thread this fed the budget loop described above. The fork's native history is still small (budget 16000), so the fork can recover once opencode goes idle. The original could not.

    Likely fix: derive child provider-turn ordinals from the existing max for that provider_thread_id, or key them by native turn/message ID, so that a resumed subagent session appends a turn instead of colliding. Separately, a projection write failure for a child thread should not fail the parent turn while the native turn keeps running.

    Workaround: don't resume opencode subagents by task_id; dispatch a fresh subagent instead.

  3. lageorgem commented on Oct 15, 2026

    @lageorgem
    Author

    Thanks. Agreed that #19414 covers the budget wedge here. The trigger described in my update above (a resumed opencode subagent reuses provider-turn ordinal 1, hits a UNIQUE violation and fails the parent turn while opencode keeps running) is a separate defect that #19414 does not touch. I filed it as #19883 so this issue can close with #19414.

  4. ashleysommer commented on Oct 15, 2026

    @ashleysommer

    I'm hitting this too. Another datapoint from me:


    We hit what looks like the same wedge as this issue, and kept the thread around in its wedged state long enough to measure it. Posting the quantitative details in case they help pin the fix.

    Thread profile. Native T3 thread (not imported), single OpenCode native session throughout, same model the whole time (GLM-5.3-Flash , context window 512,000). 35+ runs over two days; the session legitimately filled up. No subagent or delegate used.

    The state when the wedge began. The last successful provider turn completed at usedTokens: 458161 / maxTokens: 512000. After that, every send failed within ~250 ms with ContextHandoffBudgetError ("Insufficient context allowance…"), and the run was rejected before any provider turn was created.

    Why the budget is zero — the reserve, not the cap. From handoffBudget():

    budget = max(0, min(tokenCap /* 16k */, HANDOFF_BYTE_CAP /* 64k */,
                         window − usedTokens − currentBytes − max(16_000, window/4)))
    

    At 458,161/512,000 the reserve term alone (max(16000, ceil(512000/4)) = 128,000) drives the budget negative — the wedge begins at 75% of the context window, well before the provider is actually out of room. Worth noting in case the reserve is intentional but the failure mode (permanent wedge, see below) is not.

    Why /compact cannot recover it. /compact does compact the provider session (OpenCode records a completed manual compaction with a summary), but the compaction turn completes without reporting token usage — its provider-turn payload has tokenUsage: {}. The handoff budget only reads contextUsage from the last provider turn that reported usage, so the stored snapshot stays frozen at the pre-compaction 458k. Every subsequent turn dies at deliverContextHandoffs before a provider turn runs, so nothing ever refreshes the number. Circular deadlock: compaction fixes the real context but cannot fix the snapshot, and the snapshot is all the budget trusts. We confirmed both halves: provider-thread.contextUsage remained {usedTokens: 458161} for hours after compaction (through app restarts and an OpenCode v2.0.29 update), and the newest rejected-dispatch receipts kept logging the same failure.

    Model switching does not help: switching models reuses the same provider thread and the same cached snapshot.

    What recovered the thread. Manually correcting the stored contextUsage snapshot (DB-level; set it to a plausible post-compaction value) — the next send then passed the budget check, delivered the handoff, and the first successful provider turn reported fresh usage, after which the thread stayed healthy. So the minimal fix is "get one honest usage report after the compaction", but nothing in the app triggers one while wedged.

    Suggestions:

    1. After any compaction event on the bound native session, invalidate or re-poll contextUsage before computing handoff budgets (the stale snapshot is the wedge).
    2. When preparing a handoff, prefer a live provider-side context measurement over the cached snapshot — the cached value can be arbitrarily stale exactly when the thread is fullest.
    3. PR fix(server): a handoff too large for the context window no longer wedges the thread #19414's deferred-handoff approach also resolves this instance's wedge (the oversized handoff is skipped, not fatal), but the budget would still compute 0 on a freshly-restarted app until usage refreshes, so the stale-snapshot half may deserve its own fix.

    Happy to provide the DB snapshots (wedged + recovered) or specific row dumps if useful.

  5. topitopongsala commented on Oct 16, 2026

    @topitopongsala

    Same wedge on the Cursor provider (Grok 4.7 through @cursor/sdk), on 0.0.46-nightly.20261011.2994, Linux x86_64. Cursor hits it through the same two gaps that are described here for OpenCode.

    Why the budget is 0 on Cursor (traced in the bundled server code and statev2.sqlite):

    • The Cursor adapter defines no getModelContextWindow, so knownModelWindow is undefined.
    • Cursor provider turns store no tokenUsage, and the Cursor provider thread has no contextUsage. As a result, handoffUsage is null and handoffBudget falls back to window = 128000.
    • native is then nativeContextEstimate: the byte length of all stored history for the native thread. For one affected thread this was 264,726 bytes, of which 225,761 bytes were command output.
    • max(0, min(16000, 128000 - 264726 - message - 32000)) = 0.

    The live catalog shows that Cursor's real window for grok-4.7 is 256K or 500K (the contextWindow option, default 500k), so this thread was far from full. The run's modelSelection does not carry the contextWindow option, and the budget never reads it.

    Two ways in, both seen on 2026-10-15 (6 Cursor threads, 34 failed turns, one thread with 24):

    1. A failed turn, as described in this issue. One turn fails with The provider could not start this turn (ProviderAdapterTurnStartError). Every later turn then needs a delta_since_target_last_seen replay for the missed run, gets budget 0, and fails in about 100 ms before any run.start reaches Cursor. Each failure becomes one more missed run.
    2. Moving a thread to Cursor a second time. A thread that already had a Cursor provider thread from an earlier provider switch went back to Cursor with a provider handoff. The first turn on the move failed at once with the same error, because the old native thread's stored bytes count against the budget.

    What recovers it: switching the thread to a Claude instance. Claude reports real usedTokens with a 1M window, and the first turn after the switch completed in each case. /compact cannot help, because getTurnStartHistory returns every stored item with no compaction boundary, so the byte estimate does not shrink.

    Suggested fixes, for Cursor in particular:

    • Give the Cursor adapter getModelContextWindow, read from the selected (or default) contextWindow option.
    • Do not count stored bytes as tokens when the provider reports no usage. At least divide by a bytes-per-token ratio, or leave out command output that the native session already holds.
    • When the only pending handoff is a missed-run replay and it does not fit, drop or defer it rather than fail the turn.

    In our case the first failed turn came while T3 had marked the previous run cancelled but never sent run.cancel to Cursor. I will post that part on #15447.

  6. kodaZD commented on Oct 20, 2026

    @kodaZD

    Additional OpenCode/T3 v0.1.0 datapoint on Linux.

    I hit the permanent Insufficient context allowance for the provider handoff wedge on a parent OpenCode thread after heavy delegated work.

    Environment:

    • T3 Code v0.1.0
    • OpenCode 1.19.3
    • Provider instance: opencode
    • Model: sunsilk/gpt-5.6-terra
    • Configured model context: 1,050,000 tokens

    Timeline:

    1. Parent run dispatched several T3-owned delegate_task children, then a provider-native OpenCode QC child.
    2. The native QC child failed. T3 marked the parent run failed, but the parent provider_turn remained running with no completedAt.
    3. The following ordinary continue failed before a new provider turn was created with: Insufficient context allowance for the provider handoff...
    4. /compact then succeeded. The server trace records a successful opencode.session.summarize taking 62.8 seconds, and T3 records Context compacted.
    5. A later continue still failed at the same handoff budget gate, before reaching OpenCode.

    Read-only projection observations:

    • Parent provider thread had contextUsage: null.
    • The thread accumulated 28 context-handoff records, mostly manual_context subagent-result handoffs.
    • Many of those records had no delivery receipt.
    • The two rejected turns were fast (about 27–30 ms), consistent with rejection in deliverContextHandoffs, not an upstream model failure.

    The project work itself was recovered into a fresh thread. I did not mutate statev2.sqlite.

    This appears covered by PR #19414's defer-instead-of-throw behavior. One additional concern is that successful /compact did not clear or refresh the state that made later handoff-budget calculations reject the thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions