-
Notifications
You must be signed in to change notification settings - Fork 892
Description
I am trying to use the equivalent of copilot --continue in the CLI to give a conversational agent continuity of context between sessions. copilot made an implementation but it didn't work because the SDK wasn't working as expected. I asked copilot to make a bug report with a reproducible example, which I include below:
Summary
CopilotClient.resumeSession() from @github/copilot-sdk resolves successfully but the returned session object does not process new messages. Calling session.send() on a resumed session completes without error, but no assistant.message_delta events are ever emitted. The session.idle event fires repeatedly (replayed from prior turn state), but no new response is generated.
Details
CopilotClient.resumeSession() resolves successfully but the returned session object does not process new messages. Calling session.send() on a resumed session completes without error, but no assistant.message_delta events are ever emitted. The session.idle event fires repeatedly (apparently replayed from the prior turn's state), but no new response is generated.
Environment
- SDK:
@github/copilot-sdk^0.1.25 - Node.js: 22.x+ (required for
node:sqlite) - Model:
claude-sonnet-4(also tested with default model) - Session config:
streaming: true,infiniteSessions: { enabled: true }
Steps to Reproduce
Minimal reproduction script
See resume-session-repro.ts below for a self-contained script. Run with:
npx tsx bug-reports/resume-session-repro.tsManual steps
- Create a
CopilotClientand callclient.start(). - Create a session with
client.createSession({ model: "claude-sonnet-4", streaming: true }). - Send a message with
session.send({ prompt: "Hello" }). Observe thatassistant.message_deltaevents fire and a response is received. ✅ - Save the
session.sessionId. - Call
client.resumeSession(sessionId). It resolves successfully. ✅ - Subscribe to events on the resumed session and call
session.send({ prompt: "Hello again" }). - Observe: no
assistant.message_deltaevents fire. ❌ - Observe:
session.idlefires immediately (or repeatedly), with no preceding delta events. ❌
Expected Behavior
After resumeSession(), the session should be fully functional. session.send() should produce assistant.message_delta events followed by assistant.message and session.idle, exactly as on a fresh session.
Actual Behavior
resumeSession()resolves without errorsession.send()returns a promise that does not rejectsession.idlefires prematurely — before any response eventsassistant.message_deltais never emittedassistant.messagemay fire with stale content from the prior turn (replayed)tool.execution_startmay also fire with stale data from the prior turn- The session is effectively dead; no new LLM inference occurs
Impact
This makes resumeSession() unusable for maintaining conversation continuity across reconnects. In our application (a web chat UI), users who disconnect and reconnect see no output from the agent until the system times out and falls back to a new session.
Observed Server Logs
[Web] Attempting to resume session 29f33291-2b36-4c70-b640-ec70f0fea4a4 for David
[Web] Resumed session 29f33291-2b36-4c70-b640-ec70f0fea4a4 for David
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Web] Resume attempt 1 failed for David: Resume attempt timeout
[Web] Retry 1/2 for resumed session (David)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Web] Resume attempt 2 failed for David: Resume attempt timeout
[Web] Retry 2/2 for resumed session (David)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Core] Ignoring premature session.idle (no response events received yet)
[Web] Resume attempt 3 failed for David: Resume attempt timeout
[Web] Resumed session unresponsive after 3 attempts for David, falling back to new session
Note: session.idle fires multiple times per attempt with no message_delta events in between. The resumed session is completely unresponsive to new prompts.
Impact
Session resume is currently disabled in Descartes conversational agent. Every reconnect creates a new session, losing conversation history.
Repro file: resume-session-repro.ts (upload failed so pasting here)
/**
* Reproduction script for @github/copilot-sdk resumeSession bug.
*
* Demonstrates that a session resumed via client.resumeSession() does not
* process new messages: session.send() completes but no assistant.message_delta
* events fire and session.idle fires prematurely (without a new response).
*
* Usage:
* npx tsx bug-reports/resume-session-repro.ts
*
* Requirements:
* - Node.js >= 22.x (SDK requires node:sqlite)
* - @github/copilot-sdk ^0.1.25
* - Authenticated GitHub Copilot token (via `copilot auth login` or env)
*/
import { CopilotClient, CopilotSession } from "@github/copilot-sdk";
const TIMEOUT_MS = 20000;
async function sendAndCollect(
session: CopilotSession,
prompt: string,
label: string
): Promise<string> {
return new Promise((resolve, reject) => {
let content = "";
let gotDelta = false;
const unsubs: (() => void)[] = [];
const cleanup = () => unsubs.forEach((fn) => fn());
unsubs.push(
session.on("assistant.message_delta", (event) => {
gotDelta = true;
if (event.data.deltaContent) {
content += event.data.deltaContent;
process.stdout.write(".");
}
})
);
unsubs.push(
session.on("assistant.message", (event) => {
if (event.data.content) {
content = event.data.content;
}
})
);
unsubs.push(
session.on("session.idle", () => {
console.log(
`\n[${label}] session.idle fired — gotDelta: ${gotDelta}, content length: ${content.length}`
);
if (!gotDelta) {
console.log(`[${label}] ⚠️ IDLE WITHOUT ANY DELTAS — session is unresponsive`);
// Don't resolve yet; let timeout handle it
return;
}
cleanup();
resolve(content);
})
);
unsubs.push(
session.on("session.error", (event) => {
cleanup();
reject(new Error(`[${label}] Session error: ${event.data.message}`));
})
);
const timer = setTimeout(() => {
cleanup();
if (gotDelta) {
resolve(content);
} else {
reject(new Error(`[${label}] Timed out after ${TIMEOUT_MS}ms with no response`));
}
}, TIMEOUT_MS);
unsubs.push(() => clearTimeout(timer));
console.log(`[${label}] Sending: "${prompt}"`);
session.send({ prompt }).catch(reject);
});
}
async function main() {
console.log("=== @github/copilot-sdk resumeSession Bug Reproduction ===\n");
// Step 1: Create a client
console.log("[1] Creating CopilotClient...");
const client = new CopilotClient();
await client.start();
console.log("[1] Client started\n");
// Step 2: Create a fresh session
console.log("[2] Creating session...");
const session = await client.createSession({
model: "claude-sonnet-4",
streaming: true,
});
const sessionId = session.sessionId;
console.log(`[2] Session created: ${sessionId}\n`);
// Step 3: Send a message on the fresh session — this should work
console.log("[3] Sending message on fresh session...");
try {
const response1 = await sendAndCollect(
session,
"Say exactly: Hello, session works!",
"FRESH"
);
console.log(`[3] ✅ Fresh session response (${response1.length} chars): "${response1.slice(0, 100)}"\n`);
} catch (err) {
console.log(`[3] ❌ Fresh session failed: ${(err as Error).message}\n`);
process.exit(1);
}
// Step 4: Resume the same session
console.log("[4] Resuming session...");
let resumed: CopilotSession;
try {
resumed = await client.resumeSession(sessionId);
console.log(`[4] Session resumed: ${resumed.sessionId}\n`);
} catch (err) {
console.log(`[4] ❌ Resume failed: ${(err as Error).message}`);
console.log(" (This may happen if the session expired — try running again quickly)\n");
process.exit(1);
}
// Step 5: Send a message on the resumed session — this is expected to fail
console.log("[5] Sending message on resumed session...");
try {
const response2 = await sendAndCollect(
resumed,
"Say exactly: Hello, resume works!",
"RESUMED"
);
console.log(`[5] ✅ Resumed session response (${response2.length} chars): "${response2.slice(0, 100)}"\n`);
console.log("=== RESULT: resumeSession is WORKING ===");
} catch (err) {
console.log(`[5] ❌ Resumed session failed: ${(err as Error).message}\n`);
console.log("=== RESULT: resumeSession is BROKEN ===");
console.log("The resumed session accepted send() but produced no message_delta events.");
console.log("session.idle fires prematurely (replayed from prior turn state).");
}
process.exit(0);
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});