refactor(harness): DI-seam runner interrupt/stop — collapse 16 handlers (PR 1.6) - #3568
Conversation
…rs (PR 1.6) Route the runner's native interrupt / stop dispatch through a dependency-injected NativeInterruptRunner instead of 16 per-harness closures plus two hardcoded `if _harness == "<x>-native"` chains in the /events handler. Mirrors the CodexGoalRunner DI precedent (omnigent/runner/codex/goal.py): app-scope state (AP client, resource registry, event publisher, sub-agent wake plumbing, codex bridge-state resolver) is injected at construction, typed via Protocol. - New omnigent/runner/native/interrupt.py: the 9 uniform interrupt and 7 uniform stop handlers collapse to two descriptor-driven methods (_UNIFORM_INTERRUPT / _UNIFORM_STOP); claude interrupt (bridge-id) and codex interrupt (MCP-startup + turn/interrupt) keep dedicated methods, moved verbatim. interrupt()/stop() return None for handler-less harnesses so the caller falls through to the in-process cancel. - app.py: the two dispatch chains become one runner.interrupt()/.stop() call + fall-through; the 16 closures are deleted (net app.py -470). Local `from omnigent.<x>_native_bridge import` stays at call time so bridge-module monkeypatches keep resolving (no test repoints). - 12 new unit tests for NativeInterruptRunner. - Doc: add 1.6 ledger row (gap-fill deferred); flip stale 1.5c row to landed. Scope: migration-only, behavior-preserving. The antigravity/opencode coverage gap (no interrupt/stop handler; they fall through to _cancel_inprocess_turn) is left unchanged and pinned by a no-handler test; wiring agy interrupt_turn() / opencode client.abort() is a deferred follow-up. Co-authored-by: Isaac Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
|
/review |
|
Address Polly non-blocking doc nit: the module comments said 'nine uniform interrupt' and 'seven uniform stop', but _UNIFORM_INTERRUPT has seven entries and _UNIFORM_STOP six (claude/codex interrupt and claude stop are special-cased; codex/pi alias stop to interrupt). Clarify uniform-vs-total counts. Doc-only. Co-authored-by: Isaac Signed-off-by: Pat Sukprasert <pattara.sk127@gmail.com>
|
Thanks Polly — approve confirmed. On the notes:
|
|
🏷️ Doc impact: Internal refactor extracting native-harness interrupt/stop closures into a NativeInterruptRunner class with added unit tests; behavior is unchanged and no user-facing surface, integration, or default was modified. Auto-classified on merge. Set the label manually before merging to override. · run |
Syncs 4 upstream commits. No conflicts. One of them (omnigent-ai#3568, the DI-seam interrupt/stop refactor) edits `omnigent/runner/app.py`, the only file it shares with this branch, but the two changes occupy disjoint regions: the refactor collapses the interrupt / stop handlers and ends around line 6533, while this branch's sub-agent bundle-dir resolution starts at 6960 and its remaining edits sit in the session-init, turn-dispatch and harness-config regions the refactor does not touch. Git merged them without a conflict, and nothing here was resolved by hand. The other three upstream commits (Windows orphan-reaper guard, Anthropic thinking-mode capability selection, web lint baseline) touch no file in this branch's diff. Reviewed invariants were fingerprinted before the merge and re-checked after — source_rel_dir, the widened ResolvedSpec.workdir, the resolver and its three helpers, all four `_resolved_workdir_for_spec` sites, both terminal-ensure arms, and the AGENTSPEC/validator prose — all intact, with no `_find_spec_by_name` reintroduced on the runner's sub-agent path.
Related issue
N/A — part of the modular native-harness registry workstream (
designs/harness-modular-registry-proposal.md).Summary
PR 1.6 of the modular native-harness registry refactor: routes the runner's native
interrupt / stop dispatch through a dependency-injected runner class instead of 16 per-harness
closures + two hardcoded
if _harness == "<x>-native"chains increate_session_terminal'ssibling event handler.
NativeInterruptRunnerinomnigent/runner/native/interrupt.py— mirrors theexisting
CodexGoalRunnerDI precedent (omnigent/runner/codex/goal.py): app-scope state(AP client, resource registry, event publisher, sub-agent wake plumbing, codex bridge-state
resolver) is injected at construction, typed via
Protocol.interrupt(harness, conv_id)/stop(harness, conv_id)resolve the harness by key and returnNonefor harnesses with nohandler so the caller falls through to the in-process turn cancel — unchanged.
module, control-fn name, and error label) collapse to two descriptor-driven methods
(
_UNIFORM_INTERRUPT/_UNIFORM_STOP). claude interrupt (bridge-id resolution) and codexinterrupt (MCP-startup + app-server
turn/interrupt) keep dedicated methods, moved verbatim.app.py: the two dispatch chains becomeawait _native_interrupt_runner.interrupt(...)/.stop(...)+ fall-through. Net app.py −470. Localfrom omnigent.<x>_native_bridge importimports are preserved at call time so existing bridge-module monkeypatches keep resolving.
Scope: migration-only
This PR is behavior-preserving. The doc's 1.6 line also mentions "fill the coverage gaps
so every native has both paths" — antigravity-native and opencode-native have no interrupt/stop
handler and fall through to
_cancel_inprocess_turn. Wiring their native cancel (agyinterrupt_turn()/ opencodeclient.abort()) is net-new behavior and is deferred to afollow-up PR; this PR leaves that fall-through exactly as today (pinned by a new test).
ELI5
Sixteen near-identical "tell the vendor CLI to stop" functions lived inside the giant runner
file, each closing over runner state. This moves them into one small class that's handed the
state it needs, and collapses the copy-paste into two table-driven methods. Two harnesses that
never had a stop button still don't — that's a separate follow-up.
Test Plan
tests/runner/test_native_interrupt_runner.py) driving the runnerdirectly: uniform interrupt (+ pi's enqueue/no-timeout variant), uniform stop (kill +
teardown + idle + wake), 503 on bridge failure (no idle / no wake), codex no-bridge-state
no-op, claude bridge-id path, codex/pi stop→interrupt aliasing, and the no-handler → None
contract for antigravity/opencode.
test_app_sessions_native_events_lifecycle.py,test_app_sessions_native_supervision.py) POST to/eventsand patch bridge-modulefunctions — they pass untouched (call-time imports preserve the patch point), so no
monkeypatch repoints were needed.
Demo
N/A — internal runner refactor, no user-facing or UI change.
Type of change
Test coverage
Coverage notes
Behavior-preserving refactor covered by the existing HTTP-path interrupt/stop tests plus 12 new
NativeInterruptRunnerunit tests. Manual verification:tests/runner/local run is greenexcept for 9 pre-existing codex-native failures (settings/model-options/interrupt) that
reproduce identically on clean
main— they need the codex app-server to boot / modelcatalog discovery, unavailable in this local env (CI has both). Confirmed the codex interrupt
tests fail with the same "no bridge state → skipped" mode on my branch as on main, i.e. the
refactor preserves that path.
Behavior-preservation notes for reviewers:
"<x>_native_interrupt_failed"/"_stop_failed") — asserted by tests; nodisplay_namedrift on the wire.log_on_errordescriptor flag (theTUI harnesses returned 503 without logging; pi logged).
_cancel_inprocess_turn; pinned bytest_no_handler_harnesses_return_none.Workstream progress (
designs/harness-modular-registry-proposal.md, ~14 PRs):Phase 1 — 1.1 (#3239), 1.2 (#3244), 1.3 (#3314), 1.5a (#3495), 1.5b-i (#3500), 1.5b-ii
(#3501), 1.5c (#3543) landed; 1.6 is this PR. Remaining: 1.7 (seeding loop), 1.8
(enumerations); Phase 2 — 2.1–2.4. The runner interrupt/stop surface is now behind a DI seam;
the antigravity/opencode gap-fill is the one deferred follow-up. (Also flips the stale 1.5c
ledger row to
landed.)