Repository navigation
Conversation
…ead's later work claimNext marked an outbox row running under the worker's lease before decoding its payload. A payload this build could not decode (an effect type from a newer build after a downgrade, or a damaged row) failed the claim and left the row running, so every later effect on that thread stayed blocked, including across restarts. claimNext now decodes before registering the cancellation signal. On a decode failure it logs a warning, settles the row as failed with a last_error through the existing fail path, and claims again so the thread's next effect runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — Unreadable effects now fail with a recorded reason rather than blocking later work on their thread. Normal claim ordering remains unchanged, and failed payloads are retained for investigation. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
|
Note Written by Verified in a real dev environmentonly neighboring behavior is verified; the unreadable-effect recovery is not verified in the running app. checked exact head
main, normal codex turn: pr, matching normal codex turn: pr, real provider interruption (playback and seeking checked): Not verifiedthe original unreadable-row bug was not reproduced on main, and the new failure-and-continue branch was not reached in the running app. these exact builds have identical effect request schemas, and enqueue encodes with the same schema claimNext decodes. switching between them cannot generate an incompatible row. producing the trigger would require external corruption, an incompatible writer, or database fault injection; the task prohibits fabricated fixtures. no database rows were inserted or damaged for this verification. recovery of already-running unreadable rows is outside this pr's stated scope. no native desktop, ios/android, other providers, or actual downgrade from an incompatible build was exercised. no code changes were made. this is partial runtime evidence, not verification of the fix itself. |
|
Note 🤖 Opus 5.5 replying on behalf of Victor. Thank you, @maria-rcks, for running this head next to main with real Codex turns. Your run shows the change leaves normal work alone. On this branch one thread took a follow-up, a stopped You're right that switching between main and this PR can't reach the new branch. Both builds use the same schema to encode and decode, so neither writes a row the other can't read. The rows this fixes are ones an older build can't decode: an effect type a newer build added, or a damaged The gaps you listed stay open: the failure-and-continue branch in a running app, a real downgrade between two builds, other providers and native clients. Recovering rows an older build already left |
|
Note 🤖 Opus 5.5 replying on behalf of Victor. Thanks, @maria-rcks, for naming exactly what your run could not reach. A new run covers two of your items, the failure-and-continue branch in a running app and a real downgrade: I left a newer effect type pending by real use on main, then opened copies of that T3 home with |


Problem
EffectOutbox.claimNextmarks an outbox rowrunningunder the worker's lease, then decodes itspayload_json. If this build cannot decode the payload, the claim fails and the row staysrunning.claimableCandidatePredicatethen holds back later effects in the same thread's effect lane: turn starts, interrupts, checkpoints and delegated stops.nextClaimableAtreports no work for it.reconcileAfterProcessLossonly touches effect types this build knows, so restarting the same build does not clear an unknown effect type. The thread looks dead with no error in the UI. The worker logs one failed claim and the row staysrunningunder its lease; recovery needs database repair or a build that understands and reconciles that effect type.How a row this build can't read gets there:
Migrations.tslogs "Database migration history diverges from this build" and starts anyway. The effect request union has grown since:delegated-tasks.stop(fix(server): Stop also stops delegated tasks and pull request watches #16002),preview.cleanup(fix(server): environment-hosted browser tabs behave like a normal browser #16963) andscheduled-tasks.cleanup(fix(server): delete thread-bound automations with their thread #22070) are all newer variants. A thread with one of those still pending or running when you roll back a nightly, or open the same T3 home from an older desktop build, is stuck on the older build.payload_json.A same-build round trip is safe, because encode and decode share one schema.
Change
claimNextnow decodes the claimed row before it registers the cancellation signal. If decoding fails, it logs a warning with the effect id and type. Then it settles the row through the existingfailpath:failed, lease cleared,last_error"This server build cannot decode the<type>effect.", settled waiters signalled and claimers woken. Then it claims again in the same call, so the thread's next effect runs anddrainand recovery don't stop early.failed(notcancelled) keeps the row as evidence. There is no new status, contract change or migration.EffectOutboxErrors fromfailpass through unwrapped, aslistByCommandIdandnextClaimableAtalready do.Scope and approval
This is a small, focused fix for an obvious bug: one function in
EffectOutbox.tsplus one test. It changes no product behavior. A row that could never run now fails visibly instead of holding its thread forever. It does not touch the claim SQL orclaimableCandidatePredicate, so it does not overlap #30669 (open) or #26272 (now on main), and it merges cleanly with current main. Recovering rows that an older build already leftrunningis a separate change and is not part of this PR.Prior PRs: none. This change does not reuse an earlier patch; #30669 and #26272 are separate changes checked for overlap.
Verification
I made the row by real use on today's main (
cd8fc91a57), which knows the newer typeprovider-session.apply-model-selection(#34563), and opened two copies of that T3 home with builds that don't. Input went through the web app in T3's Browser panel. Claude Haiku 5.5 was signed in by a freshclaude auth login(subscription) in an isolated config dir.On main, mid-turn, I paused the Claude CLI (
SIGSTOP), pressed Stop and changed effort while the interrupt waited, then killed the server. That left the interruptrunningand theprovider-session.apply-model-selectioneffectpending. On each copy I sent "Reply with the single word: ready." in that thread.Before,
a02bc75737(the parent of #34563, without this fix): one failed claim at startup, and 95 seconds after the send the turn had not started.After, this PR's head
05893b779e: the row fails, then the recovery and the new turn run.Outbox rows (read-only) and server log lines
Main, rows after the kill:
Before,
a02bc75737, the server log lines naming the failure:After,
05893b779e, the server log lines naming the failure, then the rows:The merge with today's main (
4e3498f643, clean) knows the type and runs the effect, so it can't show this; there the new test fails with main'sEffectOutbox.tsand passes with this PR's (whole file: 61 passed).Not checked: released builds (both ran from source), and a crash between the claim and the
failupdate.Generated with Claude Opus 5.5 and GPT-6.1 Sol in T3 Code Nightly