From fc7d7f894ea233db114041a15b59e72c9154d92b Mon Sep 17 00:00:00 2001 From: Yasyf Mohamedali Date: Tue, 6 Oct 2026 08:34:17 -0700 Subject: [PATCH] =?UTF-8?q?sessions:=20=F0=9F=90=9B=20Let=20the=20root=20i?= =?UTF-8?q?nterrupt=20its=20own=20Orca=20lane?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: At 8:29 AM on October 6, 2026, the release-v3 drive root ran `orca terminal send --terminal term_4137f7d4-… --text 'STOP THE ROLLBACK. …' --interrupt` against its own incident lane, dispatch `ctx_83a95c65e319` in Run `run_7715a23a5657`. The lane was mid-turn preparing a rollback the owner had just forbidden. `orca_send_ends_session` blocked the call with "interrupts or exits the agent in another terminal, which ends that session … leave interrupts and exits to the owner." Ordinary text would only have queued until the lane's turn ended. The owner asked for the hook to be fixed. Summary: `orca_send_ends_session` now allows `--interrupt` when the send names one literal `--terminal` and the caller is the main session whose `ORCA_TERMINAL_HANDLE` coordinates the Run of that terminal's dispatch. The guard finds the dispatch with `worker_of`, the `worker-list` scan the settled-close lift uses, and checks the coordinator with `coordinates()`, the same check the 12.88.15 `worker-stop` lift relies on. Exit text such as `exit`, `/quit`, `logout`, or a control byte stays blocked for everyone with the original message. An interrupt from a lane terminal, an in-process subagent, another Run's coordinator, or a session with no readable worker stays blocked, as does a handle or text named at run time. Motivation: An `--interrupt` stops the lane's current turn and delivers the text; it does not end the session. The root already owns the Run and may stop its lanes outright under cc-notes answer edfa70e. Making it wait for the owner to interrupt a lane is a round trip with no safety gained, and here it let a forbidden rollback keep running. Details: `interrupts_own_lane` reuses `lone_target` and `spelled` from the `worker-stop` lift; `own_lane` sits beside `own_dispatch` in `_sessions.py`. `orca_send_verdict` now takes the scan and event so it can probe. The blocked-interrupt message keeps the original first sentence and names the allowed route. `inline_worker` gained a `run` parameter for the other-Run test. The inline tests pin the 8:29 AM command verbatim, a send piped to `tail` followed by a `cci post`, as an Allow for the coordinating root, plus Blocks for no coordinator handle, a lane terminal, a subagent, another Run, no worker row, `--text exit --interrupt`, `/quit`, a `"$T"` handle, and `"$MSG"` text. Claude-Session-Id: 900424b6-7393-480c-a26a-f1bd21da6e57 --- CHANGELOG.md | 7 +++ .../builtin_packs/general/hooks/_sessions.py | 9 ++- .../builtin_packs/general/hooks/sessions.py | 58 +++++++++++++++++-- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14cfc7c8..6b73d83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **The drive root can interrupt its own Orca lane.** `orca_send_ends_session` blocked every + `orca terminal send --interrupt`, so a root could not stop a lane mid-turn and redirect it; + ordinary text only queued until the lane's turn ended. A send with `--interrupt` and one literal + `--terminal` now passes when the caller is the main session whose `ORCA_TERMINAL_HANDLE` + coordinates the Run of that terminal's dispatch. Lanes, subagents, other Runs, handles named at + run time, and exit text such as `exit` or `/quit` stay blocked. + - **The drive root can stop a done Orca lane in its own Run.** `orca_ends_session` blocked every `orca orchestration worker-stop`, so a root that had decided a lane was done had to message the lane to call `worker_done`. One literal `worker-stop --dispatch ` diff --git a/captain_hook/builtin_packs/general/hooks/_sessions.py b/captain_hook/builtin_packs/general/hooks/_sessions.py index 147c20a1..bda8f580 100644 --- a/captain_hook/builtin_packs/general/hooks/_sessions.py +++ b/captain_hook/builtin_packs/general/hooks/_sessions.py @@ -314,10 +314,10 @@ def inline_workers(*handles: str) -> str: return json.dumps({"ok": True, "result": {"workers": rows, "page": {}, "scope": {"source": "all"}}}) -def inline_worker(handle: str, status: str, stage: str = "settled") -> str: +def inline_worker(handle: str, status: str, stage: str = "settled", run: str = "run_inline") -> str: row = { "dispatchId": "ctx_settled", - "runId": "run_inline", + "runId": run, "dispatchStatus": status, "agentTerminalHandle": handle, "projection": {"stage": {"detail": stage}}, @@ -1216,6 +1216,11 @@ def own_dispatch(evt: BaseHookEvent, dispatch: str) -> bool: return isinstance(run, str) and coordinates(evt, run) +def own_lane(evt: BaseHookEvent, handle: str) -> bool: + worker = worker_of(handle) + return isinstance(worker, dict) and coordinates(evt, worker.get("runId")) + + def still_settled(action: Proposal) -> bool: worker = settled_worker(action.payload["terminal"]) return worker is not None and worker["dispatchId"] == action.payload["dispatch"] diff --git a/captain_hook/builtin_packs/general/hooks/sessions.py b/captain_hook/builtin_packs/general/hooks/sessions.py index aa71259b..99a95fe0 100644 --- a/captain_hook/builtin_packs/general/hooks/sessions.py +++ b/captain_hook/builtin_packs/general/hooks/sessions.py @@ -59,6 +59,7 @@ lone_pane, nested, own_dispatch, + own_lane, owner_lift, pid_verdict, process_class, @@ -216,6 +217,15 @@ settling = partial(guarded, env={"ORCA_TERMINAL_HANDLE": "term_root"}) ORCA_GC = ".agents/skills/orca/scripts/orca-gc" STOPPED = "ctx_a0f447e7e42c" +INTERRUPTED = "term_4137f7d4-c749-445f-bb84-7dbd162da898" +OWN_LANE = {**SETTLED_COMMANDS, "orca orchestration worker-list": inline_worker(INTERRUPTED, "running", "working")} +ROLLBACK_STOP = ( + f"orca terminal send --terminal {INTERRUPTED} --text 'STOP THE ROLLBACK. OWNER 8:25 AM: do not roll back " + "executor, fix forward only. Evidence #28631: not executor; one team queue at its concurrency cap 55/55 with " + "executors idle. Re-read the OWNER OVERRIDE at the end of your brief.' --interrupt 2>&1 | tail -2; " + "~/.local/bin/cci post --drive release-v3 --lane root --kind hold --to merge-walker-r2,incident-run-backlog-0711 " + "--text 'OWNER 8:25 AM: NO executor rollback.' 2>&1 | tail -1" +) @guard( @@ -1263,19 +1273,27 @@ def orca_ends_session(evt: ToolRewriteEvent) -> HookResult | None: return block_first(evt, (orca_ending_verdict(call, scan, evt) for call in scan.literal_calls)) -def orca_send_verdict(call: Call) -> str | None: +def interrupts_own_lane(call: Call, scan: Scan, evt: ToolRewriteEvent, arguments: Arguments) -> bool: + handle = lone_target(call, arguments, ("terminal", "send"), "terminal") + return handle is not None and spelled(call, scan) and own_lane(evt, handle) + + +def orca_send_verdict(call: Call, scan: Scan, evt: ToolRewriteEvent) -> str | None: if call.name != "orca" or orca_command(arguments := ORCA.bind(call)) != ("terminal", "send"): return None spelling = spell(call) values, words = arguments.values, arguments.words text = values.get("text", ()) - if "interrupt" in values or any( - str(payload).strip().casefold() in END_OF_SESSION for payload in text if payload is not None - ): + if any(str(payload).strip().casefold() in END_OF_SESSION for payload in text if payload is not None): return ( f"BLOCKED: `{spelling}` interrupts or exits the agent in another terminal, which ends that session. Send " "only ordinary text, and leave interrupts and exits to the owner." ) + if "interrupt" in values and not interrupts_own_lane(call, scan, evt, arguments): + return ( + f"BLOCKED: `{spelling}` interrupts or exits the agent in another terminal, which ends that session. Send " + "only ordinary text; only the root coordinating that lane's Orca Run may `--interrupt` it." + ) if None in text: return ( f"BLOCKED: `{spelling}` sends text built at run time " @@ -1362,10 +1380,40 @@ def orca_send_verdict(call: Call) -> str | None: guarded(command='orca terminal send --terminal "$ORCA_TERMINAL_HANDLE" --text "note to self" --enter'): Allow(), guarded(command='orca terminal send --worktree "$w" --text hi --enter'): Allow(), guarded(command='orca terminal send --terminal t --text "ship it" --enter'): Allow(), + settling(command=ROLLBACK_STOP, commands=OWN_LANE): Allow(), + settling(command=f"orca terminal send --terminal {INTERRUPTED} --interrupt", commands=OWN_LANE): Allow(), + guarded(command=ROLLBACK_STOP, commands=OWN_LANE): Block(pattern="only the root coordinating"), + settling(command=ROLLBACK_STOP, commands=OWN_LANE, env={"ORCA_TERMINAL_HANDLE": "term_lane"}): Block( + pattern="only the root coordinating" + ), + settling(command=ROLLBACK_STOP, commands=OWN_LANE, agent_id="sibling-lane"): Block( + pattern="only the root coordinating" + ), + settling( + command=ROLLBACK_STOP, + commands={ + **SETTLED_COMMANDS, + "orca orchestration worker-list": inline_worker(INTERRUPTED, "running", "working", "run_other"), + }, + ): Block(pattern="only the root coordinating"), + settling(command=ROLLBACK_STOP, commands=SETTLED_COMMANDS): Block(pattern="only the root coordinating"), + settling(command=f"orca terminal send --terminal {INTERRUPTED} --text exit --interrupt", commands=OWN_LANE): ( + Block(pattern="leave interrupts and exits to the owner") + ), + settling(command=f"orca terminal send --terminal {INTERRUPTED} --text /quit --enter", commands=OWN_LANE): ( + Block(pattern="leave interrupts and exits to the owner") + ), + settling(command='orca terminal send --terminal "$T" --text hi --interrupt', commands=OWN_LANE): Block( + pattern="only the root coordinating" + ), + settling( + command=f'orca terminal send --terminal {INTERRUPTED} --text "$MSG" --interrupt', commands=OWN_LANE + ): Block(pattern="interrupts or exits"), } ) def orca_send_ends_session(evt: ToolRewriteEvent) -> HookResult | None: - return block_first(evt, map(orca_send_verdict, Scan.of(evt).literal_calls)) + scan = Scan.of(evt) + return block_first(evt, (orca_send_verdict(call, scan, evt) for call in scan.literal_calls)) def ends_session_key(key: str) -> bool: