Repository navigation
session: permanently stuck on HTTP 400 after a failed step; compaction also fails #52463
Description
Summary
A session became permanently unusable: every provider request returns HTTP 400 (provider.invalid-request) before any model output, and this includes a manual compaction request (POST /api/session/{id}/compact), which is documented as a recovery path ("Manual compaction always works"). The same history works when forked through opencode run --fork, but an API fork of the same history (POST /api/session/{id}/fork) fails identically. The provider's error body is never surfaced, so the trigger cannot be determined from OpenCode alone.
Environment
- opencode version: 2.0.14 (OpenCode Desktop and bundled
opencode-cli.exe) - OS: Windows 11 Home Single Language 10.0.26200
- Terminal: OpenCode Desktop app (CLI diagnostics were run from a PowerShell agent shell;
TERM=xterm-256color) - Shell: C:\WINDOWS\system32\cmd.exe (PowerShell 5.1)
- Install/channel: release build (
%LOCALAPPDATA%\Programs\@opencodedesktop) - Active plugins: none found in configuration
Model/provider: opencode-go/deepseek-v4.1-flash, variant max (variant high also fails), baseURL https://opencode.ai/zen/go/v1.
Reproduction
- Use a long-running session until its context is ~517 messages / ~880k prompt tokens.
- A mid-work provider request fails once with HTTP 400 (first seen 2026-09-30T21:54:54Z).
- Send any further message in that session. It fails with
Provider request failed with HTTP 400before producing output. 10+ attempts over ~10 hours, across 3 background service restarts, all fail. - Request manual compaction:
POST /api/session/ses_f26f850aaffeaF1ziNoRxSiSDb/compact. The compaction message is stored withstatus: failedand the same HTTP 400 error. - Switch the session model from
#maxto#highand retry. Still HTTP 400.
Expected Behavior
The session should recover via manual compaction, or at minimum the provider's actual error response should be surfaced so the user can diagnose it.
Actual Behavior
Every request for this session fails with provider.invalid-request / Provider request failed with HTTP 400, with no model output. Manual compaction is rejected the same way. The stored error is only:
{"type":"provider.invalid-request","message":"Provider request failed with HTTP 400","status":400}Server log line:
level=ERROR message="Failed to drain Session" cause="AI.Error: Provider request failed with HTTP 400 ... [cause]: AI.Error.InvalidRequest: Provider request failed with HTTP 400" role=server sessionID=ses_f26f850aaffeaF1ziNoRxSiSDb
Additional Context
- Not a context-size problem: a fork of the same session successfully completed requests with
input=883,497prompt tokens (larger than the original's last successful request at 879,704), and continued successfully for ~9 more requests up to ~898k-token prompts. - Not a model/service-wide problem: brand-new sessions on the same model work, and manual compaction works on a brand-new session.
- Fork behavior is inconsistent:
opencode run --standalone --session <id> --fork --model "opencode-go/deepseek-v4.1-flash#max" "<message>"works and resumed the work.POST /api/session/{id}/fork(withbeforeset to a message ID) fails on the first prompt exactly like the original.
- Difference observed: the working
run --forkflow appendsagent-switchedplus a fresh environmentsystemmessage before the first user message, while the API fork only copies the projected history, which ends withidle/ emptyerrorassistant messages immediately before the new user message. - Stuck session shape: 517 messages (449 assistant, 32 user, 31 idle, 3 system, 2 model-switched) with 9 empty assistant messages (
finish: "error"). Earlier HTTP 400s at 2026-09-30T12:55/12:58 were followed by successful recovery, so the permanent state began after 2026-09-30T21:54. - The AI layer stores only the generic message; the provider's
responseBody/datanever reaches the session error or the log, blocking self-diagnosis.
Workaround: continue in the fork created via opencode run --session <id> --fork.
Activity
github-actions commentedon Oct 1, 2026
This issue might be a duplicate of existing issues. Please check:
opencode-go/deepseek-v4.1-flashlong-session HTTP 400 with an opaque provider response and compaction recovery not engaging.opencode-gogateway HTTP 400 in long sessions, with the provider response body not surfaced and recovery unavailable.Quantum-Quasar commentedon Oct 10, 2026
Same symptom class here on opencode 2.0.26 (Linux,
opencodeprovider) — permanently stuck session, manual compaction fails the same way, model switch doesn't help — but in my case the provider body was surfaced, so I can add the trigger this issue is missing.Exact error (also in server log as
Failed to drain Session ... AI.Error):provider.invalid-request400:`name` must match ^[a-zA-Z0-9_.-]+$Compaction request stores:
{"status":"failed","reason":"manual","error":{"type":"provider.invalid-request",...}}Root cause (diagnosed from
session_messagerows): the model (step-5-preview-free) hallucinated tool calls namedash:edit,ash:shell,ash:python,ash:execute. The:violates the OpenAI-compatible toolnameregex. They fail at execution (No tool named "ash:..." is currently available) but are persisted in history, so every latercontinueand everycompactre-sends the poisoned history and gets the same 400 — on any model (reproduced onmuse-spark-1.3-contributor-freetoo). Forks inherit it (fork is a full deep copy ofsession_message; verified 0 shared IDs, same 6 bad entries).Minimal repro:
namecontains:(e.g. model hallucinatesash:shell).continue→ 400name must match..., forever./compact→ same 400, compaction recorded as failed.Suggestion: validate/sanitize tool
nameagainst^[a-zA-Z0-9_.-]+$at persistence time and/or strip-or-rename invalid tool-call parts when building the provider request (continue / compact / summarization), so one hallucinated call can't brick a session permanently.r266-tech commentedon Oct 10, 2026
I checked this trigger on current v2 (
4b6fea2875e4f2e3e8a8fee181803e54f7056845) with synthetic persisted messages and the actual OpenAI Chat request compiler; no provider/model calls.An errored
ash:shell/ash:edittool survivesSessionMessagedecoding →toLLMMessages→compileRequest(LLM.request(...))unchanged asmessages[].tool_calls[].function.name. Its error result still pairs with the same call ID. Six controls (two invalid names and validbash, each with same/different history target-provider refs) confirm this; provider switching drops incompatible provider metadata but does not remove the name.Both normal execution and compaction preparation use
baseTranscriptand the same lowering. That explains why the failure can recur; the complete/compact/fork workflow and live 400 were not independently reproduced here.A regression should cover existing errored history at the request boundary, paired call/result handling, and a valid-name control. Validating only newly generated calls would leave already persisted sessions poisoned. I have not opened a competing implementation PR; sharing this for the assigned fix. AI-generated investigation.
Quantum-Quasar commentedon Oct 10, 2026
Thanks for tracing this — your analysis matches my live case exactly, and my report is the live repro you didn't run.
Confirming your findings from a real session (
ses_edd90d634ffe1MTsEt6p5Tlbel, opencode 2.0.26):ash:edit,ash:shell,ash:python,ash:execute(allexecuted: false,state.status:error,No tool named "ash:..." is currently available`).continueand every manual/compactfails with the 400. Confirmed on two models (step-5-preview-freeandmuse-spark-1.3-contributor-free) — consistent with your point that provider switching drops incompatible metadata but keeps the name.ses_edad1b600ffeA2ugmIegQBxzAEis a full deep copy ofsession_message(0 shared message IDs with the parent) and carries the same 6 bad entries, so it fails identically. So this is a history-poisoning bug, not a provider/model quirk.On the fix: agreed — validating only newly generated tool calls leaves already-persisted sessions bricked, which is exactly the state I hit. Sanitizing at the request boundary (
toLLMMessages/compileRequest) is the right repair: rename or drop tool-call parts whosenamefails^[a-zA-Z0-9_.-]+$when building the provider request. A fix there would have recovered my session with no manual work — instead I had to do SQLite surgery on the live DB (rename the rows, restart the service) to get it usable again.A regression test over existing errored history at the request boundary, paired call/result handling, and a valid-name control sounds right to me too. Happy to test a build if you or the assignee want a live verification of the
/compact+ fork workflow on the original poisoned session.opencodejr commentedon Oct 28, 2026
Thanks for the detailed report and for comparing
run --forkwith the API fork, that helps a lot.We can't tell from this what the provider is rejecting, because in 2.0.14 the provider's error body was dropped unless it had a recognised
messagefield. Since v2.0.20 OpenCode adds the provider's raw response body to the error (Provider request failed with HTTP 400: …). Several compaction and step-error fixes have also landed since 2.0.14.Could you:
opencode --version.ses_f26f850aaffeaF1ziNoRxSiSDb), send a message and also try a manual compaction.HTTP 400:. Please remove anything sensitive first.If it now works, please let us know that too.
You can reply here with the details or edit the issue. If you edit it, please also leave a comment saying you've updated it so we know you're done.