Skip to content

fix(agent): make turn lifecycles explicit state machines - #2311

Merged
Makisuo merged 4 commits into
mainfrom
fix/agent-turn-lifecycles
Nov 6, 2026
Merged

Makisuo merged 4 commits into
mainfrom
fix/agent-turn-lifecycles

test(agent): name the socket heartbeat test for what it guards

12c1c94
Select commit
Loading
Failed to load commit list.
Maple Review Bot / Maple / review completed Nov 6, 2026 in 3m 47s

Confidence 6/10 · 1 issue to address

🟡 Confidence 6/10 · needs attention. Held at 6 because a correctness warning is open.
1 warning −1 · tests covered · risk high −1 · 1/1 new units observable

Reworks the autonomous turn into explicit settle/abort paths with coherent failure status, and makes the socket reconnect on a stalled handshake. The phase/settle logic reads correctly, but the new settle deadlines cannot cut a hung write.

  • runAutonomousTurn runs the phases, then settles once via settleTurn or aborted.
  • phase records each ending; the turn-end and PR failure reason derive from all of them.
  • Runner turn-ends now go through session.endTurn, gated by the turn slot.
  • ConnectorSocket gives up a dial that has not opened within OPEN_TIMEOUT_MS and reconnects.

Findings

🟠 Warning · F1 · Effect.timeout on the settle's submit cannot cut an uninterruptible write

correctness · apps/ai/src/chat/turn-runner.ts:792

submitReview runs inside runAutonomousTurn's uninterruptible settle, so Effect.timeout(SUBMIT_SAVED_TIMEOUT) cannot interrupt it: a hung submit holds the settle — and with it the session's turn slot, since endTurn is below it — for as long as the provider call runs. progress.ts:178 makes the write interruptible before its timeout for exactly this reason.

Thread `restore` from `runAutonomousTurn`'s mask into `settleTurn` and run each write as `restore(write).pipe(Effect.timeout(...))`, keeping only the bookkeeping uninterruptible, as `progress.ts` does.
What was checked
  • Abort path: failUnfinished skips superseded aborts and settle re-checks the slot, so an abort cannot overwrite a filed review.
  • phase only catches non-interrupt causes, so an abort still reaches the settler's interrupt path instead of being swallowed.
  • The saved-review submit now runs before endTime/finish, so it is not racing the review row's failure write.
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
Autonomous turn settle (runAutonomousTurn) background work yes Turn span re-annotated after settle (turn-runner.ts:816); maple.pr_review.failure_reason per row write (autonomous-settle.ts:54)

12c1c94 · Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

Annotations

Check warning on line 792 in apps/ai/src/chat/turn-runner.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: `Effect.timeout` on the settle's submit cannot cut an uninterruptible write

`submitReview` runs inside `runAutonomousTurn`'s uninterruptible settle, so `Effect.timeout(SUBMIT_SAVED_TIMEOUT)` cannot interrupt it: a hung submit holds the settle — and with it the session's turn slot, since `endTurn` is below it — for as long as the provider call runs. `progress.ts:178` makes the write interruptible before its timeout for exactly this reason.