Problem
The Control chat route dies at its 800 second maxDuration. The reaper that marks stale ai_calls rows as failed uses a 30 minute threshold sized for /api/chat (1800 seconds). Between those two numbers the row sits in streaming, the UI shows the "Saving the coordinator handoff" spinner, and the user cannot tell the turn is dead.
The reaper itself is correct. It only runs from Trigger.dev. The Vercel cron fallback route exists but is not scheduled, so if the Trigger deploy is stale nothing reaps at all.
Where
app/api/control/chat/route.ts:8: maxDuration = 800.
lib/interactive-runs.ts:30: ACTIVE_CHAT_STALE_THRESHOLD_MS = 30 * 60 * 1000, applied at :372-378 for every type === "chat" call.
lib/zombies/zombie-reaper-ai-calls.ts (reaper logic), scheduled at trigger/zombie-row-reaper.ts:44.
app/api/cron/zombie-row-reaper/route.ts exists, is auth-gated, and is absent from vercel.json crons.
Fix
- Split the constant:
ACTIVE_CONTROL_CHAT_STALE_THRESHOLD_MS = 800_000 + 60_000 (route lifetime plus a minute of grace) and select it when the call is a Control chat. Find how Control calls are distinguished from /api/chat calls in ai_calls.metadata and branch on that, not on a string in the prompt.
- Add
{ "path": "/api/cron/zombie-row-reaper", "schedule": "*/5 * * * *" } to vercel.json.
- Add
export const maxDuration = 300 to the cron route so it matches the Trigger task's 240 second budget with headroom.
Exit criteria
Out of scope
Making the UI detect a dead stream client-side before the reaper runs.
https://claude.ai/code/session_01SQ4nS96XYRztd5w9QkubPf
Problem
The Control chat route dies at its 800 second
maxDuration. The reaper that marks staleai_callsrows as failed uses a 30 minute threshold sized for/api/chat(1800 seconds). Between those two numbers the row sits instreaming, the UI shows the "Saving the coordinator handoff" spinner, and the user cannot tell the turn is dead.The reaper itself is correct. It only runs from Trigger.dev. The Vercel cron fallback route exists but is not scheduled, so if the Trigger deploy is stale nothing reaps at all.
Where
app/api/control/chat/route.ts:8:maxDuration = 800.lib/interactive-runs.ts:30:ACTIVE_CHAT_STALE_THRESHOLD_MS = 30 * 60 * 1000, applied at:372-378for everytype === "chat"call.lib/zombies/zombie-reaper-ai-calls.ts(reaper logic), scheduled attrigger/zombie-row-reaper.ts:44.app/api/cron/zombie-row-reaper/route.tsexists, is auth-gated, and is absent fromvercel.jsoncrons.Fix
ACTIVE_CONTROL_CHAT_STALE_THRESHOLD_MS = 800_000 + 60_000(route lifetime plus a minute of grace) and select it when the call is a Control chat. Find how Control calls are distinguished from/api/chatcalls inai_calls.metadataand branch on that, not on a string in the prompt.{ "path": "/api/cron/zombie-row-reaper", "schedule": "*/5 * * * *" }tovercel.json.export const maxDuration = 300to the cron route so it matches the Trigger task's 240 second budget with headroom.Exit criteria
lib/for the staleness check: a Control chat call started 15 minutes ago is stale, a/api/chatcall started 15 minutes ago is not. Must fail onmain.vercel.jsonlists the reaper cron. Confirm it appears in the Vercel dashboard crons after the deploy.pnpm lint,pnpm typecheck,pnpm testpass.Out of scope
Making the UI detect a dead stream client-side before the reaper runs.
https://claude.ai/code/session_01SQ4nS96XYRztd5w9QkubPf