fix: make harness interruption and shutdown reliable - #300
Conversation
Requeue unapplied Codex steers after interruption so queued follow-ups continue with their original identity. Interrupt live command processes without recycling the app-server, and add bounded adapter and TUI shutdown cleanup. Cover Queue admission races, lifecycle teardown, signal handling, and Codex process cleanup with regression tests and document the ownership policy.
| const target = this.options.resolveTarget(harnessTargetId); | ||
| if (!target) return false; | ||
| return this.bindingFor(MAIN_LANE_ID, target.id).adapter.cancelPendingSteers !== "requeue"; | ||
| } |
There was a problem hiding this comment.
🤖 devloop code-review · seed-2.1-pro
The preservesPendingSteers method calls bindingFor() which creates a new HarnessBinding and starts it via created.start() when no binding exists yet. This is a side-effectful operation for what should be a read-only query — it can trigger a harness cold start (adapter.open(), spawning processes, etc.) from the TUI projection path.
Since preservesPendingSteers is called from the TUI state projection to determine whether pending steers can still be owned by the native harness queue, the correct behavior when no binding exists is to return false: if there's no binding, there's no active native turn, so there are no pending steers to preserve.
The fix is to check the existing bindings map first without creating a new binding:
ccr:fp=712626fbc933
| try { | ||
| this.reclaimPendingSteers(active); | ||
| await active.binding.adapter.cancel(active.binding.ref); | ||
| } catch (error) { |
There was a problem hiding this comment.
🤖 devloop code-review · seed-2.1-pro
When interruptRecord runs, it calls reclaimPendingSteers synchronously before await adapter.cancel(). However, a concurrent sendTurn call may have already passed claimFirstForSteer and be waiting on harnessHooks.send to return. If the receipt arrives after reclaimPendingSteers has already scanned active.steers but before cancel has completed, the new steer is accepted and pushed to active.steers. Since cancel hasn't completed yet, active.status is still "active", so the race-condition check at sendTurn line 632 doesn't fire.
When cancel completes and the turn is finalized, finishRun marks this steer as "interrupted" instead of requeuing it. For adapters with cancelPendingSteers === "requeue", this means the steer is lost — it should have been returned to Baton's queue for re-processing as a follow-up turn but is instead treated as terminated with its parent turn.
ccr:fp=6cdd4b4742be
|
🤖 devloop code-review · 2 finding(s)(2 条已作为独立 review thread 发布) |
Requeue unapplied Codex steers after interruption so queued follow-ups continue
with their original identity. Interrupt live command processes without recycling
the app-server, and add bounded adapter and TUI shutdown cleanup.
Cover Queue admission races, lifecycle teardown, signal handling, and Codex
process cleanup with regression tests and document the ownership policy.