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.
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.