Skip to content

Commit 82772b5

Browse files
connor4312Copilot
andcommitted
fix(node): reject sendAndWait on session shutdown
Treat session.shutdown as a terminal outcome while waiting for a response, including when the send RPC itself remains pending. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9d67eb7 commit 82772b5

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

nodejs/src/session.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,10 @@ export class CopilotSession {
750750
const outcomePromise = new Promise<SessionOutcome>((resolve) => {
751751
resolveOutcome = resolve;
752752
});
753+
let resolveShutdown: (error: Error) => void;
754+
const shutdownPromise = new Promise<Error>((resolve) => {
755+
resolveShutdown = resolve;
756+
});
753757

754758
let lastAssistantMessage: AssistantMessageEvent | undefined;
755759

@@ -764,12 +768,24 @@ export class CopilotSession {
764768
const error = new Error(event.data.message);
765769
error.stack = event.data.stack;
766770
resolveOutcome({ kind: "error", error });
771+
} else if (event.type === "session.shutdown") {
772+
const reason = event.data.errorReason ? `: ${event.data.errorReason}` : "";
773+
const error = new Error(
774+
`Session ${this.sessionId} shut down before becoming idle${reason}`
775+
);
776+
resolveOutcome({ kind: "error", error });
777+
resolveShutdown(error);
767778
}
768779
});
769780

770781
let timeoutId: ReturnType<typeof setTimeout> | undefined;
771782
try {
772-
await this.send(options);
783+
await Promise.race([
784+
this.send(options),
785+
shutdownPromise.then((error) => {
786+
throw error;
787+
}),
788+
]);
773789

774790
const timeoutPromise = new Promise<never>((_, reject) => {
775791
timeoutId = setTimeout(

nodejs/test/session-send-and-wait.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,23 @@ function errorEvent(message: string): SessionEvent {
2929
} as SessionEvent;
3030
}
3131

32+
function shutdownEvent(errorReason?: string): SessionEvent {
33+
return {
34+
type: "session.shutdown",
35+
id: "00000000-0000-4000-8000-000000000002",
36+
parentId: null,
37+
timestamp: new Date().toISOString(),
38+
data: {
39+
codeChanges: { filesModified: [], linesAdded: 0, linesRemoved: 0 },
40+
errorReason,
41+
modelMetrics: {},
42+
sessionStartTime: Date.now(),
43+
shutdownType: errorReason ? "error" : "routine",
44+
totalApiDurationMs: 0,
45+
},
46+
} as SessionEvent;
47+
}
48+
3249
function controlledSession(): {
3350
session: CopilotSession;
3451
sendStarted: Promise<void>;
@@ -134,4 +151,16 @@ describe("sendAndWait", () => {
134151
errorFirst.resolveSend();
135152
await expect(errorFirstPending).rejects.toThrow("first error");
136153
});
154+
155+
it("rejects when the session shuts down before becoming idle", async () => {
156+
const { session, sendStarted } = controlledSession();
157+
const pending = session.sendAndWait({ prompt: "hi" });
158+
await sendStarted;
159+
160+
session._dispatchEvent(shutdownEvent("runtime session was lost"));
161+
162+
await expect(pending).rejects.toThrow(
163+
"Session session-1 shut down before becoming idle: runtime session was lost"
164+
);
165+
});
137166
});

0 commit comments

Comments
 (0)