Treat missing thread snapshots as terminal - #4235
Conversation
- Preserve authoritative thread-not-found snapshot failures - Mark missing threads deleted before opening socket subscriptions - Cover terminal missing-thread handling with sync tests - Document thread detail subscription reliability
- Delay draft detail loading until the server shell appears - Cover draft and normal thread subscription decisions - Document pre-creation draft synchronization behavior
- Verify decoded 404 and transient HTTP loader behavior - Cover draft detail subscription activation through the hook - Add structured metadata for terminal missing-thread cleanup
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Effect service review: one convention violation found. The new HTTP thread_not_found handling uses Effect.catchTag, but the conventions require Effect.catchTags for statically known tagged failures — including when handling a single tag.
Posted via Macroscope — Effect Service Conventions
ApprovabilityVerdict: Needs human review This PR changes how thread subscriptions handle missing resources, converting infinite retry loops to terminal states. It modifies core subscription lifecycle behavior across multiple packages with new error types, persistence locking, and state machine guards. The scope and runtime impact warrant human review. You can customize Macroscope's approvability policy. Learn more. |
- Match the convention for statically known tagged errors
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
- Gate direct route detail reads until local drafts have a shell - Interrupt subscribe input creation once thread state is deleted - Cover foreground resubscription and direct draft consumers
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop # Conflicts: # apps/web/src/routes/_chat.$environmentId.$threadId.tsx # apps/web/src/state/entities.ts
…d-subscription-loop
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2d25d20. Configure here.
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…ad-not-found-subscription-loop
…d-subscription-loop
There was a problem hiding this comment.
One finding: the new reason discriminator on the shared OrchestrationGetSnapshotError drives caller control flow and should be a distinct error class. The previously flagged catchTag usage in packages/client-runtime/src/state/threads.ts is now Effect.catchTags — thanks.
Posted via Macroscope — Effect Service Conventions
- Serialize snapshot persistence with cache removal - Model missing thread subscriptions with a dedicated RPC error
There was a problem hiding this comment.
Effect service conventions review of the changed TypeScript. One finding: the new OrchestrationThreadNotFoundError does not derive a message from its attributes, dropping the caller-visible message the previous OrchestrationGetSnapshotError sent over the WS RPC error channel. Everything else (the catchTags handling in threads.ts, the Semaphore-serialized persistence, the separate error tag, subpath namespace imports, and the Layer.effect/Context.Service shape of ThreadSnapshotLoader) matches the conventions.
Posted via Macroscope — Effect Service Conventions
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop # Conflicts: # packages/client-runtime/src/state/threadSnapshotHttp.ts # packages/client-runtime/src/state/threads.ts
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop
…d-subscription-loop

Summary
Treat authoritative missing-thread snapshot responses as terminal client state instead of falling through to a repeatedly failing WebSocket subscription, including after session replacement or application-foreground resubscription. Transient HTTP failures still retain the existing socket-snapshot fallback.
Route web detail synchronization through one canonical resolver so known local drafts wait until the server shell observes thread creation. The merged thread hook detects drafts automatically or honors an explicit shell-wait request, while direct detail and status consumers map their readiness through the same resolver. Draft workspace-mode changes preserve lookup through the reserved thread ref, maintaining the legitimate pre-creation flow without letting a missing-resource response delete the draft.
What Changed
thread_not_founderrors through the thread snapshot loader while mapping transient failures to socket fallback.OrchestrationThreadNotFoundErrorin thesubscribeThreadRPC error union instead of using a control-flow discriminator on the generic snapshot error.resolveThreadDetailRefthe canonical web subscription gate, with automatic draft-store detection and explicit shell-wait support inuseThread.BRANCH_DETAILS.md.Why
While investigating renderer memory growth from roughly 300 MB after launch to nearly 2 GB after extended use and idle time, stale thread details were observed being prewarmed after their server resources no longer existed. Each HTTP lookup returned authoritative
thread_not_found, but the client treated that as a transient miss, opened a WebSocket subscription that also failed, and retried the cycle every 250 ms. Multiple stale subscriptions made this a concrete source of sustained allocation, retained state, and background CPU pressure, though it should not be assumed to explain every byte of renderer memory growth by itself.The terminal handling stops that loop, evicts the stale persisted detail, prevents pending persistence work from restoring it, and prevents later session or foreground events from reopening the socket. Because a brand-new local draft also legitimately has no server thread before its first send, the canonical web resolver now makes automatic and explicit draft callers wait for the shell's creation signal so the fix does not turn that expected pre-creation 404 into a deleted draft.
Validation
src/relay/discovery.ts.Proof
No additional proof artifacts are required; the focused automated coverage and integrated browser flow exercise the affected lifecycle directly.
Note
Medium Risk
Changes core thread sync, persistence, and subscription lifecycle; incorrect not-found vs draft gating could mark live threads deleted or leave stale retries.
Overview
Authoritative thread not found responses now end thread-detail sync instead of falling through to a WebSocket path that retried every 250ms. HTTP
thread_not_foundstays in the error channel (transient HTTP failures still use socket fallback). The server exposesOrchestrationThreadNotFoundErroronsubscribeThreadwhen a snapshot is missing. The client marks the thread deleted, clears persisted detail under a lock so queued saves cannot resurrect it, and stops resubscribing on foreground wakeups or session replacement.On the web,
resolveThreadDetailRefgates detail subscriptions: local drafts and explicitwaitForShellwait for a server shell before subscribing, so expected pre-creation 404s do not delete drafts. The chat route uses readiness-aware detail/status hooks backed by the same resolver.Reviewed by Cursor Bugbot for commit 5b79dd2. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Treat missing thread snapshots as terminal by propagating not-found errors to subscriptions
OrchestrationThreadNotFoundErrorin orchestration.ts and adds it to thesubscribeThreadRPC error union so authoritative not-found signals are distinguishable from transient failures.ThreadSnapshotLoader.loadin threadSnapshotHttp.ts now fails the effect withEnvironmentResourceNotFoundErroron HTTP 404 thread-not-found instead of returningOption.none; other failures still yieldOption.nonefor socket fallback.makeEnvironmentThreadStatemachine in threads.ts marks a thread deleted and terminates its subscription on authoritative not-found from either HTTP or WebSocket, removes the cache entry under a persistence lock, and skips resubscription on foreground wakeups or session replacement.subscribeThreadWebSocket handler in ws.ts now returnsOrchestrationThreadNotFoundErrorinstead ofOrchestrationGetSnapshotErrorwhen a snapshot is missing.Macroscope summarized 5b79dd2.