All concepts
VisualCore4 minv2 preview
tasksasyncgovernanceconsentadr-014

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

The governed task-relay flow1 · MintUpstream answers the call with a promise: CreateTaskResult(task_id).2 · Relay + ledger + TaskCreatedbind owner · pin digest · store upstream snapshot · emit TaskCreatedone lock-held section, before the handle reaches the client3 · Follow-upstasks/get · tasks/result (re-verify pinned digest) · tasks/cancel · tasks/listeach fail-closed, owner-scoped, upstream-truthful –a bare task_id you do not own is indistinguishable from one that does not exist4 · Consent gatestatus input_required → elicit the downstream client for consentthe gate opens only on a confirmed accept – decide first, then open5a · Recorded decisionacceptrelay the answer upstream ·TaskConsentDecided(granted=True)the task resumes, upstream-owned5b · Failed closedany non-acceptdecline · no back-channel · error →best-effort tasks/cancel ·TaskConsentDecided(granted=False)

Read the stages top to bottom:

  1. Mint. A long-running tools/call comes back not with a result but with a promise – CreateTaskResult(task_id). The upstream owns the work.
  2. 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-truth Task snapshot, and emits the TaskCreated provenance head. If the event publish fails, the whole registration rolls back – no orphan binding, no headless provenance head, zero governed state survives. A relayed task_id is therefore always locally known: the dead-handle failure cannot recur.
  3. Follow-ups. The client polls and collects through four native tasks/* handlers. Every one is fail-closed and owner-scoped – a bare task_id you do not own resolves the same as one that never existed, so denial never confirms existence. tasks/result re-verifies the pinned digest first; drift fails the task rather than handing back a result shaped by a contract nobody authorized.
  4. Consent gate. When the upstream status turns input_required, the task is paused mid-flight. tasks/get resolves 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.
  5. 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-effort tasks/cancel is relayed, and TaskConsentDecided(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.