ref(chat): unify turn lifecycle - #1567
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
a7c647a to
aa1a216
Compare
aa1a216 to
dccb17d
Compare
dccb17d to
c7f8406
Compare
a67b1f9 to
d561e31
Compare
| }); | ||
| await args.lifecycle.complete({ | ||
| conversationId: args.conversationId, | ||
| createdAtMs: Date.now(), |
There was a problem hiding this comment.
Cancelled web turn leaks active cancellation entry if cleanup throws
Removing the try/finally around turn cleanup allows finish() to be skipped when any step throws, leaving the conversation blocked in the active cancellation map and preventing future turns.
Evidence
createWebTurnCancellationstores entries in a localMap;finish()is the only code path that removes them.begin()returnsundefinedwhen an entry already exists, permanently blocking new turns for that conversation.- The old code wrapped cleanup in
try/finallysofinish()always ran; the refactored code removed the try/finally entirely. - If
abandonTurnRecord,deleteWebAuthorization,persistThreadStateById, orlifecycle.completethrows, the function propagates the error without ever reachingfinish(), leaking the active entry. - In the old code,
finish()ran insidefinally; in the new code it also runs afteracknowledge(), so anacknowledge()throw also skips cleanup.
Identified by Warden · code-review · ZR5-CNH
There was a problem hiding this comment.
Fix attempt detected (commit 88b347d)
The commit clearly attempts to release failed cancellations by finishing in the cleanup catch, but an error from acknowledge() still occurs before the final finish() call and can leak the active cancellation entry.
The original issue appears unresolved. Please review and try again.
Evaluated by Warden
There was a problem hiding this comment.
Fix attempt detected (commit 6f01b93)
The commit directly refactors cancellation cleanup but still calls finish() only after acknowledge() and without a try/finally, so any cleanup or acknowledgement error can still leak the active cancellation entry.
The original issue appears unresolved. Please review and try again.
Evaluated by Warden
88b347d to
41d06f7
Compare
| // The reply and completed conversation state are already durable. A later | ||
| // lifecycle write failure must not rewrite them as a failed Turn. | ||
| if (completedStateCommitted) { |
There was a problem hiding this comment.
completedStateCommitted set too late allows committed state overwrite on checkpoint failure
commitResult persists the completed conversation state before saveTurnCheckpoint, but completedStateCommitted is only set to true after the checkpoint succeeds. If checkpointing fails and lifecycle.fail() also throws, the catch block overwrites the already-durable completed state with a failed turn because the guard evaluates to false.
Evidence
commitResultcallspersistThreadStateByIdto make the completed conversation state durable (~line 324), then proceeds tosaveTurnCheckpoint(~line 329).completedStateCommitted = trueis assigned only aftersaveTurnCheckpointreturns successfully (~line 338).- If
saveTurnCheckpointthrows,executeTurnsetscommitFailure, callslifecycle.fail(), and if that call also throws,executeTurnthrows into the local catch block. - At that point
completedStateCommittedis stillfalseeven thoughpersistThreadStateByIdalready wrote the completed state. - The
if (completedStateCommitted)guard at line 496 is intended to prevent rewriting a durable completed state, but it fails here because the flag is set too late than the persistence point. - The catch block then calls
markTurnFailedandpersistThreadStateById, overwriting the committed completed state with a failed turn state.
Identified by Warden · code-review · 5A6-NWZ
| } catch (persistenceError) { | ||
| captureWebBoundaryFailure({ | ||
| conversationId: context.conversationId, | ||
| error: persistenceError, | ||
| failureCode: "persistence_failed", | ||
| runId: currentRunId, | ||
| turnId, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Web failure path writes terminal lifecycle event even when turn-state persistence fails
If persistThreadStateById throws after markTurnFailed mutates the conversation, the catch block swallows the error and lifecycle.fail is still called. The task then acknowledges and returns "completed", leaving the DB snapshot stale while the event log records a terminal failure.
Evidence
markTurnFailedclearsactiveTurnIdand setslastCompletedAtMson the in-memoryconversationobject.persistThreadStateByIdis the DB call that makes those mutations durable.- When it throws, the
catchat line 670 logs the secondary error but does not rethrow or return. - Execution falls through to
lifecycle.failat line 680, which appends a terminalturn_failedevent. - The worker then returns
{ status: "completed" }, so the task framework will not retry. - Callers such as
chat/runtime/slack-turn.ts:614andchat/task-execution/paused-turn.ts:451readactiveTurnIdfrom the persisted DB snapshot, not the event log, so they will observe stale active-turn state.
Identified by Warden · code-review · WMP-5SV
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes 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 9490a09. Configure here.
9490a09 to
fa96a62
Compare

Conversation Turns now use one shared
executeTurnboundary for AgentRunner completion, failure fallback, result commit, and terminal lifecycle across Slack, web, local, dispatch, and agent invocation paths. Source-specific code still owns input durability, Actor, Source, Destination, authorization, delivery, and persistence, and starts each Turn only after its input is durable.The old
api-turnsloop is removed. Web ingress and authorization now live with API conversations, web mailbox work delegates to the shared runtime, and the former reply executor is a Slack-owned Turn boundary. Fresh mailbox work routes from its exact Source; empty resume wakes use durable dispatch, invocation, or Turn ownership instead of a fallback chain.This intentionally makes a hard cutover from the internal
api_turnandapi-runnames toweb_messageandweb-run; there are no compatibility aliases. The main review focus is Turn start/terminal commit ordering and the accepted-output path, where persistence failure must not cause duplicate delivery.Validated with the full Junior suite (2,657 tests), workspace typechecking, the Junior build, eval harness tests, dependency checks, architecture checks, and the complete pre-push lint suite.
Fixes #1563