mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
* fix(codex): install Codex's Interrupt hook so an Esc-cancelled turn settles Codex 0.150+ fires an Interrupt hook when the user presses Esc on an approval prompt or mid-tool, and nothing else. Orca did not install it, so the pane stayed blocked/working until the next prompt. - Add Interrupt to the managed Codex events and label maps, written with Codex's 3s cap (a larger value triggers a startup clamp warning). - Hash the timeout Codex hashes (Interrupt is clamped to [1,3], default 1) so self-computed trust matches Codex; pinned against a real 0.159.3 hash. - Map a root Interrupt to the existing cancelled-turn record (markCodexLeadTurnInterrupted), keeping child work in the fold; a child-scoped Interrupt is ignored. Relayed rows take the same path. * refactor(codex): let the hook builder own Codex's per-event timeout The managed hook's timeout is now Codex's own normalization of the shared budget, and every installer derives its trust entry from the hook it wrote, so no installer repeats the Interrupt special case. Claude-Session: codex-interrupt-hook review * refactor(codex): route Interrupt through the Stop lead update with an outcome Interrupt now writes the lead record through the same setCodexMainAgentTurnState call as Stop, so markCodexLeadTurnInterrupted keeps its original signature. Drops the child-scoped Interrupt guard: Codex never runs Interrupt hooks for subagents and its input schema has no agent_id. Claude-Session: codex-interrupt-hook review * fix(codex): ignore an Interrupt from an earlier turn once the next turn has started A replayed or late Interrupt carries the old turn's turn_id; matching it against the turn_id from the running turn's UserPromptSubmit keeps it from cancelling the new turn. Missing ids still cancel. * revert(codex): drop the Interrupt turn-id guard; delivery is already ordered Hook events reach Orca in order: Codex waits for the Interrupt hook before the next turn, and the restart spool is one append-only file per pane, replayed in order before live events. The guard protected an unreachable case and could drop a real cancel if the ids ever differed.
55 lines
2.7 KiB
TypeScript
55 lines
2.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { CODEX_EVENT_LABEL } from './codex-hook-definition'
|
|
import { getCodexHookTrustSignature } from './codex-hook-identity'
|
|
import { parseCodexTrustKey } from './codex-trust-identity'
|
|
import { computeTrustedHash, type CodexTrustEntry } from './config-toml-trust'
|
|
|
|
// Why: Orca's frozen form-1 POSIX command and the hash codex-cli 0.159.3 `hooks/list` reported
|
|
// for it as an Interrupt hook with `timeout: 3`; fails loudly if Codex's normalization drifts.
|
|
const ORCA_FORM_1_COMMAND =
|
|
': orca-agent-hook-form=1; if [ -n "${ORCA_PANE_KEY-}" ] && [ -n "${ORCA_AGENT_HOOK_ROOT-}" ] && [ -f "${ORCA_AGENT_HOOK_ROOT-}/agent-hooks/codex-hook.sh" ]; then /bin/sh "${ORCA_AGENT_HOOK_ROOT-}/agent-hooks/codex-hook.sh" || :; elif [ -z "${ORCA_AGENT_HOOK_ROOT-}" ] && [ -n "${ORCA_PANE_KEY-}" ] && [ -n "${ORCA_AGENT_HOOK_PORT-}" ] && [ -f "${HOME-}/.orca/agent-hooks/codex-hook.sh" ]; then /bin/sh "${HOME-}/.orca/agent-hooks/codex-hook.sh" || :; else { command -p cat 2>/dev/null || cat; } >/dev/null 2>&1 || :; fi'
|
|
const REAL_INTERRUPT_HASH =
|
|
'sha256:78b430d1d7794c49e6d83a36964d4082d05ec72023ccc38244e97b8e08d054d2'
|
|
|
|
const INTERRUPT_ENTRY: CodexTrustEntry = {
|
|
sourcePath: '/home/dev/.codex/hooks.json',
|
|
eventLabel: 'interrupt',
|
|
groupIndex: 0,
|
|
handlerIndex: 0,
|
|
command: ORCA_FORM_1_COMMAND,
|
|
timeoutSec: 3
|
|
}
|
|
|
|
describe('the managed Codex Interrupt hook', () => {
|
|
it('uses the trust key label Codex uses', () => {
|
|
expect(CODEX_EVENT_LABEL.Interrupt).toBe('interrupt')
|
|
expect(parseCodexTrustKey('/home/dev/.codex/hooks.json:interrupt:0:0')).toMatchObject({
|
|
eventLabel: 'interrupt'
|
|
})
|
|
})
|
|
|
|
it('reproduces the hash real Codex computed for the Interrupt hook', () => {
|
|
expect(computeTrustedHash(INTERRUPT_ENTRY)).toBe(REAL_INTERRUPT_HASH)
|
|
})
|
|
|
|
it('hashes the timeout Codex clamps to, so an over-cap entry stays trusted', () => {
|
|
// Why: Codex clamps Interrupt to [1, 3] and defaults it to 1 before hashing.
|
|
expect(computeTrustedHash({ ...INTERRUPT_ENTRY, timeoutSec: 10 })).toBe(REAL_INTERRUPT_HASH)
|
|
expect(computeTrustedHash({ ...INTERRUPT_ENTRY, timeoutSec: undefined })).toBe(
|
|
computeTrustedHash({ ...INTERRUPT_ENTRY, timeoutSec: 1 })
|
|
)
|
|
expect(getCodexHookTrustSignature({ ...INTERRUPT_ENTRY, timeoutSec: 10 })).toBe(
|
|
getCodexHookTrustSignature(INTERRUPT_ENTRY)
|
|
)
|
|
expect(computeTrustedHash({ ...INTERRUPT_ENTRY, eventLabel: 'stop', timeoutSec: 10 })).not.toBe(
|
|
computeTrustedHash({ ...INTERRUPT_ENTRY, eventLabel: 'stop', timeoutSec: 3 })
|
|
)
|
|
})
|
|
|
|
it('ignores a matcher, as Codex does for Interrupt', () => {
|
|
expect(computeTrustedHash({ ...INTERRUPT_ENTRY, matcher: 'anything' })).toBe(
|
|
REAL_INTERRUPT_HASH
|
|
)
|
|
})
|
|
})
|