8bf97911d5
* fix(orchestrator): plug zombie turn leak pinning sessions to RUNNING Two independent code paths lazily created turns without sharing state. service.CreateMessage (DB-based) created a turn for inbound user messages; PromptTask then unconditionally created another via startTurnForSession (in-memory + DB). The first turn was never tracked by the orchestrator, so its completed_at never got set and the session state machine kept the session pinned to RUNNING forever — the "agent is processing" spinner that wouldn't clear. - startTurnForSession is now idempotent: adopts an existing open turn from the in-memory map or the DB before creating a new one. - completeTurnForSession is now DB-authoritative: the in-memory map is a hint, the DB is truth. Loops to mop up multiple pre-existing zombies with a sanity bound. - CancelAgent captures the turn ID before agentManager.CancelAgent so the cancel message can't race the agent's complete event into a phantom turn. - service.GetActiveTurn returns (nil, nil) for "no active turn" instead of leaking sql.ErrNoRows. Adds five regression tests covering adoption, idempotency, untracked close, multi-zombie sweep, and double-complete idempotency. * fix(orchestrator): address PR review feedback on turn lifecycle - Log DB read errors in startTurnForSession adoption path so transient failures aren't silently swallowed (Claude, CodeRabbit, Greptile). - completeTurnForSession probes once after the iteration cap and only warns when a turn is *still* observed — closing exactly maxIterations is no longer a false alarm (CodeRabbit). - Clarify the CompleteTurn-failure log: returning early defers cleanup to the next sweep rather than silently dropping zombies (Greptile). - Route getOrStartTurn through GetActiveTurn so ErrNoRows normalization is the single contract (Claude). Adds TestCompleteTurnRespectsIterationCap to lock the cap behavior. * fix(orchestrator): align getOrStartTurn observability with adoption path Log DB read errors in getOrStartTurn before falling through to StartTurn — same observability contract as the orchestrator's startTurnForSession (Claude review on the previous fix-up). Also add a comment in turn_lifecycle_test.go explaining the hardcoded "task1" TaskID in repoTurnService matches seedSession, so future test authors reusing the helper know the coupling. --------- Co-authored-by: Kandev Agent <agent@kandev.dev>