-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Bug
Calling session.resume on a session that is already active on the same client connection causes all subsequent session.event JSON-RPC notifications to fire twice for that session. The doubling persists until the CLI process restarts.
Reproduction
Minimal SDK script (requires Node 22.5+):
const { CopilotClient, approveAll } = await import('@github/copilot-sdk');
const client = new CopilotClient({ cwd: process.cwd(), autoStart: true });
// Phase 1: create + send — events are SINGLE
const session = await client.createSession({
model: 'claude-sonnet-4-5',
onPermissionRequest: approveAll,
});
let count1 = 0;
const unsub1 = session.on(() => count1++);
await session.sendAndWait({ prompt: 'Say hello' });
unsub1();
console.log(`Phase 1 events: ${count1}`); // ~9
// Phase 2: resume same session + send — events are DOUBLED
const resumed = await client.resumeSession(session.sessionId, {
onPermissionRequest: approveAll,
});
let count2 = 0;
const unsub2 = resumed.on(() => count2++);
await resumed.sendAndWait({ prompt: 'Say hello again' });
unsub2();
console.log(`Phase 2 events: ${count2}`); // ~17 (1.9x)Expected
Event counts should be the same in both phases. session.resume on an already-active session should be idempotent with respect to event subscriptions.
Actual
After session.resume, 7 of 8 event types fire twice per occurrence:
user.message,assistant.message,assistant.turn_start,assistant.turn_end,assistant.usage,session.usage_info,pending_messages.modified— all doubledsession.idle— stays single (different code path?)
Impact
Any SDK client that uses resumeSession() as a health check (to verify a session is still alive before sending a message) will get doubled events for the rest of the session lifetime. This causes duplicate UI rendering, duplicate tool executions, and doubled state updates.
Environment
- CLI: v0.0.421 (latest as of Mar 2026)
- SDK: v0.1.22
- Node: v24.13.1
Workaround
Use session.abort() as a lightweight liveness check instead of resumeSession(). abort() is a no-op on idle sessions and throws if the session has been garbage-collected — same signal, no side effects.