Bug Description
When a user clicks the "停止" (stop) button on an AI Card in DingTalk channel, only the card streaming is stopped (finalize is skipped). The underlying embedded agent run continues running in the background until it hits the full timeoutSeconds (120s by default).
Steps to Reproduce
- Send a message to the bot that triggers a long-running agent run
- While the AI Card is streaming, click the stop button
- The card stops, but the embedded run continues
- The run only terminates when it times out (after 120s by default)
Expected vs Actual Behavior
Expected: Stop button should abort both the card streaming AND the underlying embedded agent run, similar to the /stop CLI command.
Actual: Only the card is stopped. The embedded run continues until natural timeout.
Evidence
Gateway logs:
19:53:27.461 CardCallback: btn_stop action received
19:53:27.566 [Finalize] Skipping — card stop was requested
19:53:29.013 [DingTalk][CardStop] stop succeeded
19:55:29.032 embedded run timeout: runId=aab122ed... timeoutMs=120000
The embedded run continued for exactly 120 seconds (the configured timeoutSeconds) after the stop was clicked. No abort was logged. No abortEmbeddedPiRun was called.
Root Cause
The DingTalk plugin's card callback handler (channel.ts) only:
- Sets
stopped = true on the card draft controller
- Skips card finalization
It does not call chat.abort RPC or abortEmbeddedPiRun(sessionId).
The abort infrastructure exists in OpenClaw core:
chat.abort RPC method (src/gateway/server-methods/chat.ts)
abortEmbeddedPiRun(sessionId) (src/agents/pi-embedded-runner/runs.ts)
- The
/stop CLI command correctly calls abortEmbeddedPiRun()
Impact
- Wasted tokens: The run continues using tokens until timeout
- Confusing UX: New messages either get "✅ Done" (if run still busy) or are processed by the old run (if run ended around the same time)
- Inconsistent with
/stop command: The CLI /stop command properly aborts the run, but the UI stop button does not
Environment
- OpenClaw: v2026.4.2
- DingTalk plugin: @soimy/dingtalk v3.5.2
- Channel: DingTalk AI Card mode (
cardRealTimeStream: true)
- Timeout: 120000ms (default)
Suggested Fix
In the DingTalk plugin's card callback handler, when btn_stop is detected, also call chat.abort via the gateway RPC to properly abort the embedded run:
// In channel.ts card callback handler
if (analysis.actionId === 'btn_stop') {
// Existing: stop the card streaming
draft.stop()
// Missing: abort the embedded run
const chat = await this.#getChat(peer)
await chat.abort()
}
Or call abortEmbeddedPiRun(sessionId) if accessible from the plugin context.
Bug Description
When a user clicks the "停止" (stop) button on an AI Card in DingTalk channel, only the card streaming is stopped (finalize is skipped). The underlying embedded agent run continues running in the background until it hits the full
timeoutSeconds(120s by default).Steps to Reproduce
Expected vs Actual Behavior
Expected: Stop button should abort both the card streaming AND the underlying embedded agent run, similar to the
/stopCLI command.Actual: Only the card is stopped. The embedded run continues until natural timeout.
Evidence
Gateway logs:
The embedded run continued for exactly 120 seconds (the configured
timeoutSeconds) after the stop was clicked. No abort was logged. NoabortEmbeddedPiRunwas called.Root Cause
The DingTalk plugin's card callback handler (
channel.ts) only:stopped = trueon the card draft controllerIt does not call
chat.abortRPC orabortEmbeddedPiRun(sessionId).The abort infrastructure exists in OpenClaw core:
chat.abortRPC method (src/gateway/server-methods/chat.ts)abortEmbeddedPiRun(sessionId)(src/agents/pi-embedded-runner/runs.ts)/stopCLI command correctly callsabortEmbeddedPiRun()Impact
/stopcommand: The CLI/stopcommand properly aborts the run, but the UI stop button does notEnvironment
cardRealTimeStream: true)Suggested Fix
In the DingTalk plugin's card callback handler, when
btn_stopis detected, also callchat.abortvia the gateway RPC to properly abort the embedded run:Or call
abortEmbeddedPiRun(sessionId)if accessible from the plugin context.