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: