Follow-up from agent-orchestrator#53 (durable workflow engine). The orchestrator now dispatches workflow steps as synchronous tasks/send and holds the connection for the whole step — because of how forge executes tasks today, there is no safer option:
tasks/send executes inline in the HTTP request (forge-cli/runtime/runner.go handler → executeTask → blocking executor.Execute). The terminal task only exists in the response.
- Consent-gate parks block inside the request (
mcp_authgate.go Await → handle.WaitCtx(r.Context())). If the client disconnects — timeout, orchestrator pod restart, network blip — the request context cancels, the gate unwinds, and the task flips to canceled: the user's pending consent is silently destroyed.
TaskStore is an in-memory map (forge-core/a2a/taskstore.go) — after an agent pod restart, tasks/get for any prior task returns "task not found", so the orchestrator can't distinguish "never ran" from "ran and was lost" and must re-dispatch.
- Minor:
tasks/get for an unknown id returns JSON-RPC -32602 (invalid params) with a "task not found: <id>" message instead of the A2A -32001 TaskNotFound code — clients have to match on the message string.
Asks (in value order)
- Detached execution:
tasks/send (or an opt-in variant/param) accepts the task, runs it decoupled from the request lifetime, and returns submitted/working immediately; clients poll tasks/get (or subscribe) to terminal state. Parks then survive client disconnects, and the orchestrator can release its connection + goroutine while a consent gate waits — which is also the machinery the platform's HITL approval work (agent-orchestrator#54) wants underneath.
- Durable (or at least TTL'd + restart-tolerant) TaskStore so
tasks/get stays answerable across agent restarts for recent tasks. Client-chosen deterministic ids are already in use by the orchestrator (wf-<execution>-<node>-a<attempt>) — per-attempt records accumulate forever in the current map, so eviction is needed regardless.
-32001 TaskNotFound on tasks/get/tasks/cancel for unknown ids (keep the message; fix the code).
Until this lands, the orchestrator's workaround is: no fixed dispatch timeout (the per-step budget is the connection lifetime, default 35 min), a watchdog polling tasks/get to mirror the transient auth-required state, tasks/cancel before any deliberate abandon, and re-dispatch of a fresh attempt when a lost task can't be classified. It works, but every consent park pins a connection + goroutine on both sides for up to the step budget, and forge's 10-minute gate timeout remains the ceiling for how long a consent can wait.
Refs: initializ/agent-orchestrator#53, initializ/agent-orchestrator#54 (HITL approval op, will build on detached execution).
Follow-up from agent-orchestrator#53 (durable workflow engine). The orchestrator now dispatches workflow steps as synchronous
tasks/sendand holds the connection for the whole step — because of how forge executes tasks today, there is no safer option:tasks/sendexecutes inline in the HTTP request (forge-cli/runtime/runner.gohandler →executeTask→ blockingexecutor.Execute). The terminal task only exists in the response.mcp_authgate.goAwait→handle.WaitCtx(r.Context())). If the client disconnects — timeout, orchestrator pod restart, network blip — the request context cancels, the gate unwinds, and the task flips tocanceled: the user's pending consent is silently destroyed.TaskStoreis an in-memory map (forge-core/a2a/taskstore.go) — after an agent pod restart,tasks/getfor any prior task returns "task not found", so the orchestrator can't distinguish "never ran" from "ran and was lost" and must re-dispatch.tasks/getfor an unknown id returns JSON-RPC-32602(invalid params) with a"task not found: <id>"message instead of the A2A-32001TaskNotFound code — clients have to match on the message string.Asks (in value order)
tasks/send(or an opt-in variant/param) accepts the task, runs it decoupled from the request lifetime, and returnssubmitted/workingimmediately; clients polltasks/get(or subscribe) to terminal state. Parks then survive client disconnects, and the orchestrator can release its connection + goroutine while a consent gate waits — which is also the machinery the platform's HITL approval work (agent-orchestrator#54) wants underneath.tasks/getstays answerable across agent restarts for recent tasks. Client-chosen deterministic ids are already in use by the orchestrator (wf-<execution>-<node>-a<attempt>) — per-attempt records accumulate forever in the current map, so eviction is needed regardless.-32001TaskNotFound ontasks/get/tasks/cancelfor unknown ids (keep the message; fix the code).Until this lands, the orchestrator's workaround is: no fixed dispatch timeout (the per-step budget is the connection lifetime, default 35 min), a watchdog polling
tasks/getto mirror the transientauth-requiredstate,tasks/cancelbefore any deliberate abandon, and re-dispatch of a fresh attempt when a lost task can't be classified. It works, but every consent park pins a connection + goroutine on both sides for up to the step budget, and forge's 10-minute gate timeout remains the ceiling for how long a consent can wait.Refs: initializ/agent-orchestrator#53, initializ/agent-orchestrator#54 (HITL approval op, will build on detached execution).