anthropics / anthropics/anthropic-sdk-go
SessionToolRunner redispatches stale tool calls after a processed session-wide interrupt
- Dominant language
- Go
- Stars
- 1.2k
- Forks
- 213
- Avg merge
- 1d 12h
- Merged PRs (30d)
- 11
Description
## Summary
`SessionToolRunner` can repeatedly execute a stale tool call after a processed, session-wide `user.interrupt`.
A processed interrupt with no `session_thread_id` invalidates the session's outstanding tool calls. Those `agent.tool_use` / `agent.custom_tool_use` events remain in event history, however, and the runner currently tracks only whether calls were seen or answered. On reconnect, `reconcile` treats the invalidated call as unanswered and dispatches it again.
The server no longer accepts a result for that call, so the result POST receives a permanent 4xx. The failed call remains unanswered and is dispatched again on the next reconcile, creating an execute / reject / reconnect loop that can pin a self-hosted worker to one session.
## Version
- `anthropic-sdk-go` v1.57.0 (`b6d3cc8b6363f88d70e5c08ffb375471e7e8aab8`)
- Reproduced through a self-hosted Managed Agents worker built with `ant` v1.17.0
## Reproduction shape
The failure can be reproduced with a scripted session event service:
1. Emit an `agent.tool_use` with `processed_at = T0`.
2. Process a session-wide `user.interrupt` with `processed_at = T1`, where `T1 > T0` and `session_thread_id` is absent.
3. Reconnect the event stream. Keep the old tool-use event in list history without a corresponding result.
4. Let the runner execute the historical tool call and return a permanent 4xx from the result POST.
5. Reconnect again. The same tool call is executed and rejected again.
There is an additional ordering detail: the list endpoint is ordered by `created_at`, while queued user events receive `processed_at` later. An interrupt can therefore appear before a tool-use event in listed history even though the interrupt's processing timestamp invalidates that tool call. Clearing only the history entries encountered before the interrupt is insufficient; the cutoff must be based on `processed_at`.
## Expected behavior
- A processed session-wide interrupt settles every tool call whose `processed_at` is at or before the interrupt cutoff.
- Calls after the cutoff continue normally.
- Queued interrupts (`processed_at: null`) and thread-targeted interrupts do not cancel unrelated calls.
- A locally running tool receives context cancellation when its call is interrupted.
- A permanent result rejection cannot leave an unrecoverable call eligible for infinite redispatch. If history shows that an interrupt or another runner's result settled the call before the local SSE stream observed it, the runner should treat it as settled; otherwise it should terminate with an error so the worker can release the session.
## Impact
The worker process remains healthy while making no forward progress. With a single worker used for session/workspace affinity, one interrupted session can prevent queued sessions from being claimed until the worker is restarted or the session is cleaned up out of band.
## Related
- #377 and #387 cover a different trigger with the same liveness failure shape: an empty tool result is rejected with 400.
- This issue is specifically about processed `user.interrupt` events leaving historical tool calls eligible for redispatch.
Contributor guide
Assessment
This issue has not been assessed yet.