Skip to content

Commit 5dd0bd1

Browse files
committed
fix agents numeric params and lsp exits
1 parent d7e2096 commit 5dd0bd1

4 files changed

Lines changed: 62 additions & 8 deletions

File tree

src/agents/agent-bundle-lsp-runtime.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ class MockChildProcess extends EventEmitter {
4747
readonly stderr = new PassThrough();
4848
readonly stdin: Writable;
4949

50-
constructor(private readonly initializeResponsePrefix = "") {
50+
constructor(
51+
private readonly initializeResponsePrefix = "",
52+
private readonly respondMethods?: ReadonlySet<string>,
53+
) {
5154
super();
5255
this.stdin = new Writable({
5356
write: (chunk, _encoding, callback) => {
@@ -70,6 +73,9 @@ class MockChildProcess extends EventEmitter {
7073
if (!body || typeof body.id !== "number" || typeof body.method !== "string") {
7174
return;
7275
}
76+
if (this.respondMethods && !this.respondMethods.has(body.method)) {
77+
return;
78+
}
7379
const result = body.method === "initialize" ? { capabilities: { hoverProvider: true } } : null;
7480
queueMicrotask(() => {
7581
this.stdout.write(
@@ -138,6 +144,38 @@ describe("bundle LSP runtime", () => {
138144
expect(killProcessTreeMock).toHaveBeenCalledWith(4321, { graceMs: 1000 });
139145
});
140146

147+
it("rejects pending LSP requests immediately when the child process exits", async () => {
148+
configureSingleLspServer();
149+
const child = new MockChildProcess("", new Set(["initialize"]));
150+
spawnMock.mockReturnValue(child);
151+
const { createBundleLspToolRuntime } = await import("./agent-bundle-lsp-runtime.js");
152+
153+
const runtime = await createBundleLspToolRuntime({ workspaceDir: "/tmp/workspace" });
154+
const hoverTool = runtime.tools.find((tool) => tool.name === "lsp_hover_typescript");
155+
if (!hoverTool) {
156+
throw new Error("expected hover tool");
157+
}
158+
159+
const request = hoverTool.execute("call-1", {
160+
uri: "file:///tmp/workspace/index.ts",
161+
line: 0,
162+
character: 0,
163+
});
164+
child.exitCode = 1;
165+
child.emit("exit", 1, null);
166+
167+
await expect(request).rejects.toThrow('LSP server "typescript" exited (1)');
168+
await expect(
169+
hoverTool.execute("call-2", {
170+
uri: "file:///tmp/workspace/index.ts",
171+
line: 0,
172+
character: 0,
173+
}),
174+
).rejects.toThrow('LSP server "typescript" exited (1)');
175+
176+
await runtime.dispose();
177+
});
178+
141179
it("keeps LSP framing aligned after multibyte messages in the same chunk", async () => {
142180
configureSingleLspServer();
143181
const prefix = encodeLspMessage({

src/agents/agent-bundle-lsp-runtime.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,27 @@ function registerActiveLspSession(session: LspSession): void {
110110
activeBundleLspSessions.add(session);
111111
}
112112

113+
function failLspSession(session: LspSession, error: Error): void {
114+
if (session.failure) {
115+
return;
116+
}
117+
session.failure = error;
118+
for (const pending of session.pendingRequests.values()) {
119+
clearTimeout(pending.timeout);
120+
pending.reject(error);
121+
}
122+
session.pendingRequests.clear();
123+
}
124+
113125
function attachLspProcessHandlers(session: LspSession): void {
114126
session.process.on("error", (error) => {
115-
session.failure = error;
116-
for (const pending of session.pendingRequests.values()) {
117-
clearTimeout(pending.timeout);
118-
pending.reject(error);
119-
}
120-
session.pendingRequests.clear();
127+
failLspSession(session, error);
128+
});
129+
session.process.on("exit", (code, signal) => {
130+
failLspSession(
131+
session,
132+
new Error(`LSP server "${session.serverName}" exited (${signal ?? code ?? "unknown"})`),
133+
);
121134
});
122135
session.process.stdout?.on("data", (chunk: Buffer | string) =>
123136
handleIncomingData(session, chunk),

src/agents/tools/common.params.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ describe("readNumberParam", () => {
166166
it("throws for invalid present bounded finite number params", () => {
167167
expect(readFiniteNumberParam({ quality: "0.75" }, "quality")).toBe(0.75);
168168
expect(readFiniteNumberParam({ quality: null }, "quality")).toBeUndefined();
169+
expect(readFiniteNumberParam({ quality: "" }, "quality")).toBeUndefined();
170+
expect(readFiniteNumberParam({ quality: " \t\n" }, "quality")).toBeUndefined();
169171
expect(() => readFiniteNumberParam({ quality: "0.8jpg" }, "quality")).toThrow(
170172
"quality must be a finite number",
171173
);

src/agents/tools/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ export function readFiniteNumberParam(
301301
strict: true,
302302
});
303303
if (value === undefined) {
304-
if (readParamRaw(params, key) != null) {
304+
const raw = readParamRaw(params, key);
305+
if (raw != null && !isBlankParamValue(raw)) {
305306
throw new ToolInputError(options.message ?? `${key} must be a finite number`);
306307
}
307308
return undefined;

0 commit comments

Comments
 (0)