All concepts
VisualCore4 minSince 2.0.0
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 (re-verify pinned digest, outcome inlined) · tasks/update · tasks/canceleach 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 → client answers via governed tasks/updatethe 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/get re-verifies the pinned digest before handing any payload over; drift fails the task rather than returning a result shaped by a contract nobody authorized. SEP-2663 inlines that payload on the poll – there is no tasks/result to call.
  4. Consent gate. When the upstream status turns input_required, the task is paused mid-flight and its snapshot carries an inputRequests map naming what it needs. The client answers by driving an inbound tasks/update, and that update is the consent. The gate opens before the answer reaches the upstream – there is no relay-then-record race.
  5. Recorded decision. TaskConsentDecided records the decision with the principal that made it. A confirmed relay consumes the single-use consent; a transient upstream refusal discards the gate without consuming it and does not fail the task, so a retry re-drives the update and completes. An evicted consent slot fails the task closed – an expired consent is never read as an implicit yes.

One activation detail keeps the diagram honest: even on 2.0.0 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.

Neither gate prompts a human. The synchronous L7 requireApproval gate fails closed – a gated tools/call is blocked, not parked in an interactive approval queue. Stage 4 is fail-closed too, on a decision the client volunteers by driving tasks/update. Hangar used to be the interactive one, on the 2025-11-25 wire it no longer serves; what remains is governance over an answer, not a question.


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.