The governed task-relay flow
Updated July 22, 2026
The governed task-relay flow
When an upstream answers a tools/call with a task_id instead of a result, the
real work finishes later – on a follow-up you make minutes or hours on – and
Hangar can no longer govern the call in one round trip. Its answer is to relay
and govern the task the upstream owns, and never to run it.
This page walks that governed relay one stage at a time. For why a proxy governs the gap rather than growing into an executor, see relay-with-governance; here the concern is the mechanism.
The flow, one stage at a time
Read the stages top to bottom:
- Mint. A long-running
tools/callcomes back not with a result but with a promise –CreateTaskResult(task_id). The upstream owns the work. - Relay + ledger + TaskCreated. Before the handle reaches the client,
relay_and_govern()does four things under one lock-held critical section, keyed on(target_server_id, task_id): it binds the owner (tenant + principal from the request identity, cross-checked against the authorized owner), pins the tool digest carried on the synchronous invoke path, stores the upstream-truthTasksnapshot, and emits theTaskCreatedprovenance head. If the event publish fails, the whole registration rolls back – no orphan binding, no headless provenance head, zero governed state survives. A relayedtask_idis therefore always locally known: the dead-handle failure cannot recur. - Follow-ups. The client polls and collects through four native
tasks/*handlers. Every one is fail-closed and owner-scoped – a baretask_idyou do not own resolves the same as one that never existed, so denial never confirms existence.tasks/resultre-verifies the pinned digest first; drift fails the task rather than handing back a result shaped by a contract nobody authorized. - Consent gate. When the upstream status turns
input_required, the task is paused mid-flight.tasks/getresolves it in-handler: it elicits the downstream client for consent, and the gate opens only on a confirmed accept. Consent is obtained before the gate opens – there is no open-then-decide race. - Recorded decision. Either branch is terminal and leaves a signed line on
the provenance chain. On accept (5a) the answer relays upstream and
TaskConsentDecided(granted=True)is recorded. On any other outcome (5b) – decline, no negotiated back-channel, or an elicitation error – the task is failed closed, a best-efforttasks/cancelis relayed, andTaskConsentDecided(granted=False)is recorded. A paused task is never left dangling on a maybe.
One activation detail keeps the diagram honest: even on the v2 preview the relay
seam is gated by the relay_tasks_enabled kill-switch and, per ADR-014’s
build-now-activate-per-upstream decision, engages per-upstream the first time
an upstream actually emits a task. Until that first real task – or with the
switch flipped off for a fast per-deployment rollback – the upstream still gets
the clean TaskRelayNotSupported rejection. That per-upstream activation is why
a v2 deployment can still observe TaskRelayNotSupported, and why an upstream
that never emits tasks sees no behavioral change at all.
Two things the diagram is quietly asserting
Hangar relays; it does not execute. No scheduler, no result store, no GC/TTL correctness, no cancellation-race ownership, no worker → main-loop bridge. The ledger holds only who owns it, which pinned contract, and the last known upstream status – never a result payload, never execution state. Governance binds at the same proxy seam that governs synchronous calls, so the “one bug and governance silently fails to bind” failure mode a background thread would introduce does not exist to break.
Stage 4 is the only genuinely interactive consent in Hangar. Do not confuse
it with the synchronous L7 requireApproval gate, which fails closed – a
gated tools/call is blocked, not parked in an interactive approval queue.
Nothing prompts a human and waits on the synchronous path. The async gate is
different only because the task is already paused and the polling client is right
there on a live session with a negotiated elicitation capability: a real
back-channel exists, so a real human decision can be routed back. Everywhere else,
governance is deterministic enforcement – no scores, no queue.
Grounded in ADR-014 (relay-with-governance, not executor). For the full argument – the ledger boundary, digest re-verification, and every fail-closed branch of the consent gate – read Governed async tasks: relay-with-governance, not an executor. Everything is MIT and self-hosted – no SaaS.