Skip to content

Emit heartbeats for long-running tool calls#733

Open
ilteris wants to merge 2 commits into
agentclientprotocol:mainfrom
ilteris:codex/claude-acp-tool-heartbeats
Open

Emit heartbeats for long-running tool calls#733
ilteris wants to merge 2 commits into
agentclientprotocol:mainfrom
ilteris:codex/claude-acp-tool-heartbeats

Conversation

@ilteris
Copy link
Copy Markdown

@ilteris ilteris commented Jun 1, 2026

Why

ACP clients commonly use tool lifecycle updates as the only observable liveness signal for an in-flight tool call. Between the initial tool_call and the terminal tool_result update, the bridge emits nothing — so a client cannot distinguish a healthy quiet tool from a wedged bridge or a dropped/delayed terminal update. In practice this trips client-side stall watchdogs and cancels a turn even though Claude is still working.

This is fundamentally a transport-liveness gap, not a claim that any specific tool is slow. The heartbeat keeps the existing tool row alive without adding user-visible output or fake progress text, so a quiet-but-healthy tool is no longer indistinguishable from a hang.

Summary

  • Start a lightweight per-tool heartbeat after emitting the initial ACP tool_call.
  • Send periodic tool_call_update notifications (every 60s) while a tool remains pending.
  • Stop the heartbeat when the matching tool_result arrives.
  • Clear leaked heartbeats when a tool ends without a result. A tool_use can terminate with no tool_result — turn cancelled, stream aborted, or session torn down. clearToolCallHeartbeatsForSession sweeps a session's timers, and is called from the prompt turn-end, cancel(), and (via cancel) teardown paths so an unresolved tool can't leak its interval for the lifetime of the process.

Tests

  • Heartbeat fires every 60s while pending and stops once the tool_result lands.
  • Leak regression: after clearToolCallHeartbeatsForSession, advancing timers past several intervals produces no further beats.
  • Heartbeat tests live in their own describe block with isolated fake-timer cleanup.

Verification

  • npm test -- --run (301 passed)
  • npm run build
  • npm run check

ilteris added 2 commits May 31, 2026 21:04
A tool_use only stops its heartbeat when the matching tool_result
arrives. If a tool_use ends without one — turn cancelled, stream
aborted, session torn down — the setInterval and its Map entry leak for
the lifetime of the process. Add clearToolCallHeartbeatsForSession and
call it from the prompt turn-end, cancel(), and (via cancel) teardown
paths. Add a leak regression test and move the heartbeat tests into a
dedicated describe block with their own fake-timer cleanup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant