Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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