Skip to content

feat(defer): survive long human approvals — detach deferred task context + async result delivery to the origin channel #314

Description

@initializ-mk

Follow-up from PR #311 (#310).

Problem

A channel-initiated conversation (Slack/Telegram → agent) runs under the inbound tasks/send request context, and the channel router holds that request synchronously with a 360s (6-minute) HTTP client timeout (forge-cli/channels/router.go NewRouter). The agent loop — and the parked DEFER hook — run under r.Context() (forge-cli/server/a2a_server.go:299).

Consequence: for a channel-initiated task, a human approval must land within ~6 minutes. If it doesn't:

  • the router's HTTP call times out → the request context is cancelled → the defer hook sees the cancellation and abandons the deferral (rolls status back, auto-resolves as timeout — forge-cli/runtime/defer.go:120–128); the tool call fails;
  • a later Approve click hits a task with no pending deferral → /tasks/{id}/decisions returns 404 → the approver sees ⚠️ could not record approve … 404.

This binds even when defer.timeout is longer (default 10 min; examples use 15 m) — the 6-minute channel wait is the real ceiling for channel-initiated tasks, and there is no async re-delivery of the resumed reply to the origin channel. So a legitimately-approved-at-8-minutes action is lost from the user's perspective.

PR #311 adds a startup warning when defer.timeout exceeds the channel router timeout (the cheap guard). This issue is the real fix.

Proposal

Let a deferred, channel-initiated task outlive the synchronous HTTP request and deliver its result asynchronously:

  1. Detach the deferred task's context from the inbound request when a tool defers (e.g. context.WithoutCancel + the defer timeout as the deadline), so a client/router HTTP timeout no longer tears down a pending approval. The agent loop keeps running (parked) until the approver acts or the defer timeout fires.
  2. Return the inbound tasks/send promptly with the task in deferred state (async A2A task semantics) instead of holding the HTTP connection open for the whole approval window.
  3. Deliver the resumed result asynchronously to the origin channel once the tool completes — reuse the channel-notifier path (like SetScheduleNotifier) keyed on the task's original channel + reply target, so the answer lands back in the conversation where it started, in the same session, however long the approval took.

Acceptance

  • A channel-initiated tool call deferred for longer than the router's 6-min sync window survives (not abandoned on the HTTP timeout).
  • Approval after the sync window resolves the deferral and the tool proceeds in the same session.
  • The resumed agent reply is delivered back to the origin channel/thread asynchronously.
  • Direct A2A clients that poll tasks/get see the deferred → resolved transition.
  • The PR feat(channels): native Slack interactive approvals for DEFER (R4c) (#310) #311 startup timeout-mismatch warning can then be relaxed/removed for channels that support async delivery.

Notes

  • Ties into the in-process-only limitation (a process restart still abandons pending deferrals; cross-restart persistence is a separate concern).
  • Keep the session intact across the detach — the resumed goroutine already holds it; this is about the context lifetime + result delivery, not the session itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-cliAffects the forge-cli command-line tool (init, run, build, mcp commands)securitySecurity vulnerability fixes

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions