Skip to content

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

@xiaohuaxi

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
    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