Skip to content

fix: make harness interruption and shutdown reliable - #300

Merged
qiankunli merged 1 commit into
mainfrom
fix/graceful-shutdown
Aug 14, 2026
Merged

fix: make harness interruption and shutdown reliable#300
qiankunli merged 1 commit into
mainfrom
fix/graceful-shutdown

Conversation

@qiankunli

Copy link
Copy Markdown
Collaborator

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.

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.
@qiankunli
qiankunli merged commit 36a02ef into main Aug 14, 2026
Comment thread src/controller/index.ts
const target = this.options.resolveTarget(harnessTargetId);
if (!target) return false;
return this.bindingFor(MAIN_LANE_ID, target.id).adapter.cancelPendingSteers !== "requeue";
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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

Comment thread src/controller/index.ts
try {
this.reclaimPendingSteers(active);
await active.binding.adapter.cancel(active.binding.ref);
} catch (error) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 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

@qiankunli

Copy link
Copy Markdown
Collaborator Author

🤖 devloop code-review · origin/main..HEAD · 5f4b99d89

2 finding(s)(2 条已作为独立 review thread 发布)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants