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

    area:sessionsSession management, resume, history, session picker, and session state

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions