Skip to content

[Feature]: Add agent.abort RPC to cancel embedded agent runs #66531

Description

@xiaohuaxi

Summary

Add a gateway RPC agent.abort so external clients can cancel runs started via the agent() RPC. Today they can't: chat.abort only tracks chat.send-initiated runs, and the underlying primitive abortEmbeddedPiRun(sessionId) has no RPC surface.

Problem to solve

  • agent() runs register into ACTIVE_EMBEDDED_RUNS (sessionId-keyed) at src/agents/pi-embedded-runner/run/attempt.ts:1572, but never into context.chatAbortControllers. chat.abort only consults chatAbortControllers, so calling chat.abort after an agent() run is a no-op.
  • The agent handlers map at src/gateway/server-methods/agent.ts:279 currently exposes only "agent", "agent.identity.get", and "agent.wait" — no cancel entry point.
  • The only external-facing workaround is reaching into globalThis[Symbol.for("openclaw.embeddedRunState")].activeRuns.get(sessionId)?.abort(). That works in-process but is an undocumented side door; true out-of-process clients (UI apps, ACP clients, iOS/macOS apps) cannot access process globals and are left without any cancellation path.

Proposed solution

Introduce a new RPC agent.abort with a params schema accepting either {runId} or {sessionId}. The UI layer typically has sessionId; some callers know only runId. Accepting both avoids pushing the resolution onto clients.

Internally the handler delegates to abortEmbeddedPiRun(sessionId) (already defined in src/agents/pi-embedded-runner/runs.ts:118-179), which handles the primary ACTIVE_EMBEDDED_RUNS lookup plus fallback to abortReplyRunBySessionId.

Response shape: {ok: true} on acceptance, {ok: false, reason} otherwise. Reasons should at minimum include not-found (sessionId not in ACTIVE_EMBEDDED_RUNS). The response represents "request accepted", not "run already terminated"; the end-of-run signal is left to the existing lifecycle:end event and the agent() RPC completion frame (which already carries result.meta.aborted).

Schema addition: AgentAbortParamsSchema in src/gateway/protocol/schema/agent.ts, sibling to the existing AgentParamsSchema (currently at L80-111).

Alternatives considered

  • Register agent() runs' AbortController into context.chatAbortControllers as well. Rejected because it conflates two different run kinds at the gateway layer; chat.send has concepts (S2C receipts, broadcast) the agent() RPC does not share.
  • Document the globalThis[Symbol.for(...)] side door as a public contract. Rejected because out-of-process clients cannot access process globals at all, so this does not solve the external-client gap.
  • Rely entirely on client-side cancellation (reject the RPC promise, let the server-side run continue). Rejected because the abandoned run still consumes tokens and tool budget, and may continue writing to the session transcript.

Impact

  • Affected: all external clients of the agent() RPC — CoClaw UI, ACP clients, third-party control surfaces, and potentially the iOS/macOS apps if they ever adopt agent() for run initiation.
  • Severity: Medium. Users can't reliably cancel long-running agent turns; background tokens and tool calls accumulate even after the user has given up.
  • Frequency: Every cancel attempt on an agent()-initiated run today. Workarounds exist only for in-process plugins.
  • Consequence: Wasted LLM spend, surprising side effects from tool calls issued after the user stopped caring, and confusing UX when partial outputs keep arriving.

Evidence/examples

  • chat.abort handler and registration for contrast: src/gateway/server-methods/chat.ts:1313 registers into chatAbortControllers; the handler itself delegates to src/gateway/chat-abort.ts:76-108 (abortChatRunById).
  • The side-door shape (for context, not a proposed contract): globalThis[Symbol.for("openclaw.embeddedRunState")] exposes {activeRuns, snapshots, sessionIdsByKey, waiters, modelSwitchRequests} per runs.ts:51-58; EmbeddedPiQueueHandle.abort is synchronous and idempotent (runs.ts:20-27).

Additional information

Verified against OpenClaw commit d7cc6f7643 (v2026.4.14-beta.1+69).

A companion feature request covers the complementary in-process gap — exposing abortEmbeddedPiRun and friends on api.runtime.agent so bundled plugins can drop their side-door usage. The two requests are independent but serve the same goal of supported cancellation APIs.


Reported by the CoClaw team.
This issue was discovered while developing @coclaw/openclaw-coclaw, a CoClaw channel plugin for OpenClaw.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions