Skip to content

fix(agent-events): deduplicate tool call events after watchdog force resends - #156

Open
alienfacepalm wants to merge 1 commit into
stablekernel:mainfrom
alienfacepalm:fix/dedupe-tool-calls-after-force-resend
Open

alienfacepalm wants to merge 1 commit into
stablekernel:mainfrom
alienfacepalm:fix/dedupe-tool-calls-after-force-resend

Conversation

@alienfacepalm

Copy link
Copy Markdown

Problem

In long agentic sessions on OpenCode v2.2.6, runs die with Error: Duplicate tool call: <id>, usually right after a bash/PowerShell tool call.

Root cause

When the stream watchdog detects a stall, it cancels and resends with local: { force: true }. The Cursor SDK can then replay tool-call-started events with the same callId. OpenCode v2 rejects repeated tool call IDs on purpose (packages/core/src/session/runner/publish-llm-event.ts, if (tool.called) ... Duplicate tool call), and the plugin forwarded the replays.

Fix

streamAgentTurn now tracks emitted tool call IDs in a per-turn Set that survives force resends, and skips repeated tool-call-started events (logged in debug mode). Tool results pass through unchanged.

Testing

  • New test/duplicate-tool-call.test.ts (3 cases: single duplicate, distinct IDs, repeated duplicates)
  • Full suite passes (659/659), typecheck and build clean
  • CHANGELOG updated under [Unreleased]

…resends

When the stream watchdog detects a stall and forces a resend with
`local: { force: true }`, the Cursor SDK may replay tool-call-started
events with the same callId. OpenCode v2 tracks which tool call IDs
have been received and rejects duplicates, throwing:
`Error: Duplicate tool call: {id}`

This was observed in long agentic sessions (15+ minutes) with OpenCode
v2.2.6, always right after bash/PowerShell tool calls when the watchdog
triggered a force resend.

Root cause: OpenCode v2's publish-llm-event handler
(packages/core/src/session/runner/publish-llm-event.ts:479) maintains
a per-turn registry of tool calls and explicitly rejects duplicates:

```typescript
if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`))
```

The plugin had no deduplication logic, so replayed events from force
resends reached OpenCode and were rejected.

Fix: Track emitted tool call IDs in a Set and skip duplicate
tool-call-started events. The Set is scoped per turn (local to
streamAgentTurn), persists across force resends, and only filters
tool-call events (tool-result events pass through unchanged).

Testing:
- Added test/duplicate-tool-call.test.ts with 3 test cases
- All new tests pass (3/3)
- All existing tests pass (659/659)
- Typecheck and build successful
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