Docs/Reference

Reference

Cancellation, judge requests and hook supervision

How a dispatch the common hook runtime runs is cancelled, how a judge request shares its adapter with other hooks and waits only while Brama serves, and how each hook's processes are owned and ended.

How a dispatch the common hook runtime runs is cancelled, how a judge request shares its adapter with other hooks and waits only while Brama serves, and how each hook's processes are owned and ended.

Cancellation, judge progress and abandoned children

A dispatch requires a nonempty string id. To abandon it, send {"id":"cancel-event","op":"cancel","dispatchId":"event"}. The cancellation response identifies the dispatch and whether it was still owned by this core. The original dispatch answers ok:false with its cancellation reason. Cancellation never supplies a policy verdict. Closing the core's stdin also relinquishes ownership of every active hook, including a dispatch whose payload contains no session id.

If a tool call's dispatch is cancelled, the adapter refuses that tool call with the cancellation cause. It neither spends a single-use grant nor admits the call because the abandoned dispatch returned no blocking-hook record.

The before-provider text gate likewise requires an explicit pass; a cancelled assessment is not permission to send prose. A failed background refresh records its actual cause for the same session and work generation instead of retaining an earlier pass. A request whose tools cannot be required (an Anthropic Messages request) is sent unchanged, so its model can still write prose. In a session with a window the adapter watches the streamed answer: the first non-blank text it streams while the gate's newest answer for the work generation is not a pass, or while the gate has not answered for it, aborts the run, and a hidden message carrying the gate's reason starts the next turn, so the operator sees at most those first words. Subagents' text is not watched, and a session without a window (print or RPC) keeps the stop refusal alone, so a run nobody watches is not continued without end.

The OMP adapter forwards an event's AbortSignal, cancels outstanding work on a new input, session activation or shutdown, and streams hook stderr to the terminal and the Tama hook status indicator. Judge requests announce czekam with the route and endpoint before waiting; completion or failure follows on the same stream. An unavailable stop reviewer refuses completion with TAMA_HOOK_INFRASTRUCTURE and its actual cause, not a claim that the assistant violated the review rule. These are event and process-state decisions, not a Tama handler time limit.

OMP races every extension handler against a fixed budget. It pauses a tool_call handler's budget while the handler shows a component, and pauses no other handler's. In a session with a window the adapter shows a one-line overlay while a tool call's gates run, naming the tool; the overlay takes no keys, so typing stays with the editor, and the call runs or is refused on its gates' verdict however long they take, instead of OMP refusing it with Extension … timed out. While the stop hooks assess an answer the same overlay says that Tama is assessing it and which dispatches run, and the turn ends on the candidate's own verdict when it comes within OMP's budget. When the budget runs out first, OMP closes the overlay and ends the turn without the adapter's answer; the assessment goes on, and a refusal it then gives is sent into the session as a message that starts the next turn on it, so a slow assessment still holds the end. An assessment that ends before its hooks answer carries what ended it. A message reaching the session (the operator's prompt) gives the turn to that message: the stop handler gives no refusal of its own, OMP runs the message, and the answer to it is assessed when it ends. An assessment restarted for the same answer, because the session roles its verdict depends on changed, is awaited in its turn. Any other end refuses naming its cause: a newer candidate answer replaced it, a tool returned its result, the session was compacted, changed or shut down, or the assessment failed. A session without a window (print or RPC) does not await the dispatch: it immediately refuses an unfinished assessment, naming the pending stop hooks and their measured elapsed time, so nothing passes on a clock. One owned assessment serves repeat requests for the same candidate. Its identity includes the session, work generation, assistant message, transcript file identity, and current enforcement selection. A changed candidate cancels the older assessment; only a completed pass for the exact unchanged candidate authorizes completion. A cancelled or failed assessment is not a pass. New input, activation, compaction, shutdown and tool completion relinquish ownership. An assessment abandoned by new input, an interrupt or shutdown sends nothing when it ends, so its result never restarts an interrupted session. This removes the previous idle-session continuation path.

Harness refusal continuations appended to the same transcript do not by themselves change the candidate. A changed completion message, replaced transcript or new work generation does require a new assessment. Operator interruption and notification-only events remain outside the adapter's cancellation-return contract.

Post-compaction context belongs to the session and work generation that requested it. If either changes while the native hook runs, the adapter discards the late context and records why; this guard does not prevent compaction or prove restoration.

Judge requests on a shared adapter

Before sending another request through a local Stado adapter, Tama reads the resolver's published resolver-waiting.json beside STADO_RESOLVER_STATE_FILE, or the standard resolver state in ~/.stado, and compares the outstanding connections on that adapter with the judge requests running Tama hooks own. A connection no running hook owns refuses the new request with its service, consumer, phase, destination, host and start time. This names an observed unanswered connection; it does not prove that the service is down. An endpoint with no published resolver state is not declared healthy by this check.

A judge request belongs to the hook process that sent it. While it waits, Tama keeps a record of it under ~/.tama/judge-channels/: the hook process, its command line, the HTTP client process, the route, the endpoint and when it was sent. Each process is named by its pid and by when the kernel says it started (hook_start, client_start), since the kernel gives an ended process's pid to the next one: a process now running under a recorded pid but started at another time is not the recorded one, so a record whose hook was killed is an ended hook's even after its pid is reused, and a process that only took the client's pid is never ended in its name. A record written before starts were kept is read by pid alone. A hook or client the kernel holds no start for refuses the request by name. The record goes when the client ends. A request a running hook owns is watched against Brama's readiness (below), so it is a request Brama is answering, not a dead channel: when every outstanding connection on the adapter is owned by a running hook, the next hook's request goes out beside them after Brama's own readiness, and judges of concurrent sessions are not serialized behind one answer. Which connection is whose is read from the connection itself, not from how many there are: a resolver that records the loopback address each held client connected from (client) lets Tama ask lsof -nP -FpR -iTCP:<port> which processes hold that port and which process started each, and the connection is a running hook's when one of them is the client process that hook recorded or a process that hook started — the readiness question its watch holds open (below). A running hook's own request the resolver does not list therefore lets no other connection through. Connections an older resolver lists without a client address are known only by count: each needs a running request no named connection matched. When lsof cannot answer, the refusal says lsof could not say which process holds loopback port <port> …. When some outstanding connection is no running hook's request, the refusal names the requests, their owners and the connections no running hook watches:

judge request to http://<resolver adapter>/v1/chat/completions not started: resolver at … reports 2 unanswered connection(s) on this adapter: service brama consumer tama phase answer to … since …; …; running Tama hooks own 1 request(s) (route <route> to <endpoint> sent by hook process <pid> (`tama hook check-speculation`) through client process <pid>, waiting for <n> s); 1 of the connections is no running hook's request: service brama consumer tama phase answer …; another request is not sent while a connection no running hook watches is waiting

A record whose hook process has already ended is nobody's channel: the next judge request ends its client (only while that process still runs the judge's HTTP client, /usr/bin/curl), removes the record and says tama: ended the judge client its hook left behind: … on stderr. When no running Tama hook owns any waiting connection, the refusal says no running Tama hook owns any of them: they are another consumer's or Stado's, read with stado resolver status. Real journey: tests/hooks/judge_channel.rs.

A judge request waits only while Brama serves

Before a judge request is sent to Brama, Tama asks Brama's own readiness report (GET /readyz on the same adapter). A Brama that declares it serves no traffic refuses the request with its declared reason (judge request to … not sent: Brama declares it serves no traffic at …/readyz: no provider capability or subscription is configured), and a readiness answer with an error status refuses with Brama's report, so a request Brama cannot serve ends with a named refusal and is never left waiting. There is no time limit. While a request Brama accepted waits, Tama asks the same readiness again naming the digest of the answer it last read (GET /readyz?after=<digest>), and Brama answers when that answer changes, so the watch makes one request per change of Brama's readiness, not one per answer. A Brama that stops declaring it serves, or an adapter that stops answering the readiness question, ends the hook's own waiting client, and the request is refused with the judge request to … was ended while it waited: Brama no longer declares it serves (…) carrying Brama's declaration, so a request forwarded to a Brama that never answers does not hold the adapter for every later judge. A Brama whose answer names no digest cannot be waited on that way: the request is then not watched further, and stderr says tama: the judge request to … is no longer watched: Brama at … names no digest of its readiness answer, so a change of it cannot be waited on. When the request ends, the watch ends the readiness question it holds open. A request Brama keeps declaring it serves waits until Brama answers or its hook process ends; a resolver that forwards the readiness question too and holds it unanswered leaves the watch waiting with the client, which Stado's own refusal of a forwarded connection its remote never answers frees. A provider declared with tama model-provider set declares no readiness and is neither asked nor watched. The journey cargo test --manifest-path rust/Cargo.toml -p tama-cli --test hooks_judge_channel runs the built tama model-provider check against a loopback listener that never answers: a waiting request of a running process is refused naming the process and its command, and one whose hook ended has its waiting client ended and its record removed. The journey cargo test --manifest-path rust/Cargo.toml -p tama-cli --test hooks_readiness_watch runs the built tama hook block-objective-substitution preflight against a Brama stand-in that declares itself ready, naming a digest, to the preflight and to the watch's first question, never answers the chat request and declares it serves no traffic to the question naming that digest: the hook ends its client, refuses with the stand-in's declaration, and the stand-in sees the chat connection closed; a third question naming no digest fails the case.

Malformed wait records refuse with their state path and JSON cause before chat or catalogue HTTP starts; they are not an empty list. Required fields follow Stado's published record contract; omitted legacy bind and phase retain the producer's compatibility defaults, and newer unrelated fields are tolerated. The registered model_provider_route malformed-state journey requires TAMA_TEST_SOURCE_REVISION, an isolated configured TAMA_TEST_BRAMA_HOME and a real TAMA_TEST_RESOLVER_ENDPOINT, and retains candidate and binary identity plus the actual CLI refusal for a malformed persisted record.

Hook supervision

Each hook has a native owned-hook supervisor in its own process group. The core holds a kernel lifetime channel; cancellation closes it, and a killed core closes it automatically. On EOF the supervisor ends its own group, including the hook and HTTP children. It does not query another process's birth through ps, so sandboxed runtimes need no external process-inspection permission.

The supervisor records its own start and group before starting the hook. It writes and syncs a complete temporary document, then atomically renames it to an in-flight record containing a distinct birth stamp. It holds that record's inode locked while alive. tama hooks running --json and native runtime status read this lock and report hookBirthIdentity and ownershipState without guessing from a reused PID. An unlocked ended record is removed; coreAlive: null means the core was not separately inspected, not that it is dead. Legacy records still use process inspection; refusals name the ps -A -o pid=,ppid=,pgid=,lstart=,command= interface and its actual OS or command failure. New owned records do not require that interface. An unreadable record remains an error, never an empty healthy list.

Concurrent inspectors serialize their birth-lock probes through ownership-inspection.lock, with visible waiting. An inspector's cleanup lock must not be mistaken for a live supervisor. A record disappearing after enumeration is completed cleanup; permission, decoding and lock failures remain errors.

In Tama Desktop, open Session and choose Inspect hooks and end orphans. The live adapter answers the same inspection, publishes it into the session record, and the screen lists each hook's owner, descendants and termination outcome. Hook progress appears there as stderr arrives. Inspection errors remain errors; a termination signal being sent is not presented as proof that the process has already exited.

The real held-endpoint journeys in tests/hooks-running/cancellation.rs run the compiled stop hook against a local TCP listener that deliberately sends no answer. They check dispatch cancellation, core-input closure, core death, connection EOF and recorded progress. The sandbox journey denies external process information, requires live status to identify the waiting hook without an identity error, and requires the real HTTP connection and supervisor lifetime lock to close when the core dies. Before launch, qualification checks the declared TAMA_TEST_SOURCE_REVISION against a clean tracked source tree and retains the binary digest and actual command, including the sandbox profile. Reports retain birth records, protocol frames, numeric exit status, signal termination and stderr in Cargo's test artifact directory before outcome assertions. owned-hook refuses direct invocation without its inherited core channel or when it does not lead its own process group.