Skip to content

resumeSession causes N+1 duplicate assistant.message_delta events per resumed turn #418

@0GiS0

Description

@0GiS0

Description

When using CopilotClient.resumeSession() for multi-turn conversations, each assistant.message_delta event is emitted N+1 times, where N is the number of times the session has been resumed. The duplicates share the same event.id.

  • createSession + first message: 1x per delta (correct)
  • After 1st resumeSession: 2x per delta
  • After 2nd resumeSession: 3x per delta
  • After Nth resumeSession: (N+1)x per delta

Reproduction

import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient({ githubToken: TOKEN });

// First turn — works correctly
let session = await client.createSession({
  sessionId: "test-session",
  model: "gpt-4.1",
  streaming: true,
});

session.on("assistant.message_delta", (event) => {
  console.log("DELTA:", event.id, event.data?.deltaContent);
});

await session.sendAndWait({ prompt: "Say hello" });
// Each delta fires 1x ✅

// Second turn — deltas duplicated
session = await client.resumeSession("test-session", {
  model: "gpt-4.1",
  streaming: true,
});

session.on("assistant.message_delta", (event) => {
  console.log("DELTA:", event.id, event.data?.deltaContent);
});

await session.sendAndWait({ prompt: "Say goodbye" });
// Each delta fires 2x ❌ (same event.id on both copies)

Observed behavior (server logs)

After the 1st resume (2nd message), each delta arrives exactly twice:

DELTA #1 id=… size=1056 "En una pequeña"
DELTA #2 id=… size=1056 "En una pequeña"    ← duplicate
DELTA #3 id=… size=1098 " aldea rodeada"
DELTA #4 id=… size=1098 " aldea rodeada"    ← duplicate

After the 2nd resume (3rd message), each delta arrives 3 times:

DELTA #1 id=… "Había"
DELTA #2 id=… "Había"    ← duplicate
DELTA #3 id=… "Había"    ← duplicate

SDK source analysis

I inspected the SDK wrapper code (@github/copilot-sdk@0.1.23) and the dispatch path appears clean:

  1. attachConnectionHandlers() — registers onNotification("session.event") once per connection (not per session).
  2. handleSessionEventNotification() — does a single this.sessions.get(sessionId) lookup and calls session._dispatchEvent(event) once.
  3. resumeSession() — creates a new CopilotSession object and overwrites the entry in this.sessions Map, so only one session object exists per ID.
  4. CopilotSession._dispatchEvent() — iterates typed handlers + wildcard handlers once each, no accumulation.

Since the SDK client-side dispatch path invokes _dispatchEvent exactly once per incoming JSON-RPC notification, the duplication appears to originate in the CLI server subprocess (@github/copilot package) which sends N+1 session.event JSON-RPC notifications per logical delta after N resumes.

Workaround

Deduplicate using event.id, which is shared across all copies of the same logical delta:

const seenEventIds = new Set();

const unsubscribe = session.on("assistant.message_delta", (event) => {
  if (event.id && seenEventIds.has(event.id)) return;
  if (event.id) seenEventIds.add(event.id);

  // Process delta normally
  console.log(event.data?.deltaContent);
});

Environment

  • @github/copilot-sdk: 0.1.23
  • Node.js: v22.x
  • OS: macOS

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions