fix(mothership): keep Chat stream legs alive without a wall clock - #8463
Conversation
…wall clock - End the replay GET at its cap without a terminal event, so the client re-attaches from its cursor instead of ending a live turn with resume_timeout. - Replace the per-leg worker SSE wall clock with an idle timeout (WORKER_STREAM_IDLE_TIMEOUT_MS, 120 s) that fails a silent leg as a retryable interruption; a caller-set timeout still applies. - Let StreamRetryWindow run without a deadline by default and replenish its reachable budget only after five minutes of healthy streaming, never per event. - Split the tool watchdog, permission wait, client tool wait, and delegation TTL onto their own constants and remove ORCHESTRATION_TIMEOUT_MS. - Refresh the replay buffer TTLs from the chat-lock heartbeat, and report a replay gap for a cursor ahead of a buffer whose numbering restarted. - Document why maxDuration stays on the chat POST and execute routes.
…s events A reconnect attempt whose re-attached tail delivered new events now restarts the retry budget at the base delay, so separate network drops hours apart in a long turn no longer add up to the ten-attempt exhaustion.
… never revive a closed buffer - The chat-lock heartbeat now slides the owner byte counter's TTL along with the replay buffer's. Otherwise, after a park longer than an hour, the counter expired while the ring survived: the ring could grow to about twice its target, and its refunds drained the user counter. - Scheduling a finished stream's cleanup now marks it closed, and the refresh leaves a closed stream alone. A heartbeat still in flight at teardown can no longer re-extend a buffer whose cleanup was already scheduled. - Describe the worker idle timeout in terms of network intermediaries, and state that the delegation TTL reuses the long-running tool watchdog's cap.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
There was a problem hiding this comment.
1 issue found across 22 files
Confidence score: 5/5
- In
apps/sim/lib/mothership/request/session/buffer-ttl.integration.ts, Test 1 parks longer than the 2-second stream TTL, so the keys may expire before the test completes and make its result unreliable; align the park duration with the TTL or refresh the keys.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/lib/mothership/request/session/buffer-ttl.integration.ts">
<violation number="1" location="apps/sim/lib/mothership/request/session/buffer-ttl.integration.ts:13">
P3: Test 1's park is much longer in real time than the stream TTL it compresses. `COPILOT_STREAM_TTL_SECONDS` is 2, so the events/seq/owner-counter keys expire 2 real-time seconds after their last refresh, but each tick of the loop also spends a real `await sleep(500)` — 10 ticks ≈ 5s of real time. The assertions only pass because the abort poller's heartbeat lands `refreshBufferTtl` every ~500ms real; on a stalled CI runner a single >2s gap between heartbeats expires the keys and fails the final `getLatestSeq === 1` / `appendText === 2` / counter equality asserts. Widen the real-time margin by raising the TTL (e.g. 10-20s) while keeping the 21s fake-time jump so the park still outlasts it.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
… only on real progress - A non-OK worker response's body is read under the same idle bound as the leg, so a stalled error body can no longer block the turn forever. - The reachable retry budget refills only when a leg's delivered events span five minutes, from its first event to its latest. A leg that delivered one event and then only kept alive has made no progress and no longer refills it.
… a capped replay as ended - scheduleBufferCleanup sets the closed marker before shortening the TTLs, so a heartbeat refresh that lands mid-pipeline is either overridden or sees the marker. - A reconnect that ends at its cap is reported as ended without a terminal, not as a client disconnect: the outcome is read before the route closes its own stream. - The buffer TTL integration test keeps a 5 s TTL against a 12 s park, so a slow runner cannot expire the buffer between heartbeats.
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
Summary
Sim no longer ends a Chat run's stream at 60 minutes. The worker's own run deadline still applies until its flag is turned off, so on its own this PR moves the limit from Sim to the worker. It has no worker dependency.
GET /api/copilot/chat/stream) no longer ends a live turn withresume_timeoutat its cap. It closes without a terminal event, and the client re-attaches from its cursor.ORCHESTRATION_TIMEOUT_MSis removed.replay_gapinstead of waiting for events that never come.Known limits
The usage analytics settle window (
USAGE_SETTLE_MS) still assumes a stream of about an hour. It only affects cached analytics hours, not invoices or the usage gate; revisit before the worker deadline is turned off.Worker deadline: the worker's run deadline still ends runs until its flag is turned off.
replay_gapstill ends the view: a reconnect the replay ring can't serve still ends with areplay_gaperror. The follow-up PR re-syncs it from the worker's durable log.Inbox runs: Chat runs started from the inbox still run in a Trigger.dev task with a 90-minute
maxDuration. That is tracked separately.maxDuration = 3600on the POST and execute routes: it stays, because it only takes effect on serverless hosts.Test plan
buffer-ttl.integration.ts(real Redis): a park longer than the buffer and counter TTLs; no revival after cleanup is scheduled; a cursor ahead of a restarted buffer.bun run lint,bun run type-check,bun run check:audits, fullapps/simvitest.