Skip to content

session.resume on active session doubles session.event notifications #1933

@darthmolen

Description

@darthmolen

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 doubled
  • session.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions