Skip to content

ref(chat): unify turn lifecycle - #1567

Open
dcramer wants to merge 20 commits into
mainfrom
codex/unify-turn-lifecycle
Open

ref(chat): unify turn lifecycle#1567
dcramer wants to merge 20 commits into
mainfrom
codex/unify-turn-lifecycle

Conversation

@dcramer

@dcramer dcramer commented Aug 14, 2026

Copy link
Copy Markdown
Member

Conversation Turns now use one shared executeTurn boundary 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-turns loop 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_turn and api-run names to web_message and web-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

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
junior-docs Ready Ready Preview Aug 17, 2026 1:52am

Request Review

Comment thread packages/junior/src/chat/task-execution/web-work.ts
Comment thread packages/junior/src/chat/agent-invocations/work.ts
Comment thread packages/junior/src/chat/runtime/slack-resume.ts Outdated
Comment thread packages/junior/src/chat/runtime/turn-execution.ts Outdated
@dcramer
dcramer marked this pull request as ready for review August 14, 2026 18:51
@github-actions github-actions Bot added the risk: high PR risk score: high label Aug 14, 2026
@dcramer
dcramer force-pushed the codex/unify-turn-lifecycle branch from a7c647a to aa1a216 Compare August 14, 2026 19:08
Comment thread packages/junior/src/chat/task-execution/web-work.ts Outdated
Comment thread packages/junior/src/chat/task-execution/web-work.ts
Comment thread packages/junior/src/chat/agent-invocations/work.ts
Comment thread packages/junior/src/chat/task-execution/web-work.ts
Comment thread packages/junior/src/chat/local/runner.ts
Comment thread packages/junior/src/api/conversations/web.ts Outdated
Comment thread packages/junior/src/chat/runtime/slack-resume.ts
Comment thread packages/junior/src/chat/task-execution/web-work.ts
Comment thread packages/junior/src/api/conversations/pending-messages.ts
Comment thread packages/junior/src/chat/task-execution/web-cancellation.ts
Comment thread packages/junior/src/chat/task-execution/web-cancellation.ts Outdated
Comment thread packages/junior/src/chat/local/runner.ts
Comment thread packages/junior/src/chat/task-execution/web-work.ts
Comment thread packages/junior/src/chat/task-execution/web-work.ts
Comment thread packages/junior/src/chat/task-execution/web-work.ts
@@ -905,17 +911,6 @@ async function resumeSlackTurnInContext(
}

if (postDeliveryCommitError) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed turnLifecycle.fail fallback leaves thrown commit errors unterminated

When the commitResult callback throws before returning a failure — for example if status.clear() or onPostDeliveryCommitFailure throws — executeTurn does not write a terminal event, and the removed fallback no longer ensures the turn reaches a failed state. This leaves the turn incomplete in the event store with no terminal lifecycle.

Evidence
  • commitResult callback sets runResultHandled = true first, then calls await status.clear() outside the try/catch at line ~701, so a Slack status-write error propagates out of the callback.
  • Inside the callback catch block, await runArgs.onPostDeliveryCommitFailure?.(error) at line 762 is also unguarded and can throw.
  • executeTurn contract explicitly states: "Throw when the source still owns recovery; this helper does not write a terminal event for a thrown commit error."
  • When the callback throws, outer catch at line ~879 sees runResultHandled === true and sets postDeliveryCommitError, then the function throws at line 913 without calling turnLifecycle.fail.
  • ConversationTurnLifecycleService.fail uses an idempotency key, so the old duplicate call was harmless; removing it entirely breaks the thrown-error path.

Identified by Warden · code-review · 7PG-PYS

Comment on lines +221 to +223
if (mailboxBatch) {
const firstMessageId = mailboxBatch[0]!.metadata.messageId;
const terminalInputMessageIds = await getTerminalTurnInputMessageIds(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty mailbox batch crashes on truthy array guard

if (mailboxBatch) treats an empty array as truthy, allowing a subsequent mailboxBatch[0]!.metadata.messageId access to throw at runtime when the batch is empty.

Evidence
  • mailboxBatch is initialized from resolved.batch and can be an empty array when resolved.kind === "mailbox".
  • if (mailboxBatch) evaluates to true for [], so the block executes despite there being no messages.
  • mailboxBatch[0]! evaluates to undefined at runtime (the non-null assertion is erased), and accessing .metadata.messageId throws.
  • The same hunk later uses mailboxBatch?.length at line 390 to explicitly guard against empty batches, demonstrating the codebase expects them.

Identified by Warden · code-review · P2R-URT

Comment on lines +638 to +639

if (cancellationSignal?.aborted && outcome.status !== "completed") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancellation after accepted output still rewrites non-completed turn state

The post-executeTurn cancellation check calls completeCancelledTurn for awaiting_auth and suspended outcomes even when assistant output was already accepted, violating the accepted-output boundary established in the agentRunner wrapper.

Evidence
  • The agentRunner.run wrapper inside the executeTurn options guards cancellation with if (!assistantMessageAccepted) and documents: "Active cancellation wins until assistant output is accepted. After that point, cancellation must not rewrite visible work."
  • The agent can return status: "awaiting_auth" or status: "suspended" after already calling delivery and setting assistantMessageAccepted = true.
  • The outer code at line 638 checks only cancellationSignal?.aborted && outcome.status !== "completed", ignoring whether assistantMessageAccepted is true.
  • When this condition fires for a non-completed outcome after accepted output, completeCancelledTurn() is called, which invokes completeCancelledWebTurn to abandon the turn record, clear pending auth, mark the user message as skipped, and write a turn_completed event with outcome: "cancelled" — rewriting the visible turn state that should have been protected.

Identified by Warden · code-review · M2C-Z6Q

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c9d1014. Configure here.

Comment thread packages/junior/src/chat/task-execution/web-work.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unify turn lifecycle outside api-turns

1 participant