@wrongstack/acp 1.0.29 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -239,6 +239,10 @@ async function handleSessionNewOp(ctx, id, params) {
239
239
  }
240
240
  const skipped = [];
241
241
  const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
242
+ if (ctx.sessions.size >= ctx.maxSessions) {
243
+ await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
244
+ return false;
245
+ }
242
246
  const sessionId = `sess_${ctx.allocId()}`;
243
247
  const now = (/* @__PURE__ */ new Date()).toISOString();
244
248
  const state = {
@@ -289,6 +293,9 @@ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
289
293
  if (!existing && sessionId && ctx.store) {
290
294
  const persisted = await ctx.store.load(sessionId);
291
295
  if (persisted) {
296
+ if (ctx.sessions.has(sessionId)) {
297
+ return handleSessionLoadOp(ctx, id, params, replayHistory);
298
+ }
292
299
  if (ctx.sessions.size >= ctx.maxSessions) {
293
300
  await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
294
301
  return false;
@@ -299,6 +306,13 @@ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
299
306
  }
300
307
  const candidateCwd = loadCwd ?? persisted.cwd ?? ctx.defaultCwd;
301
308
  const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
309
+ if (ctx.sessions.has(sessionId)) {
310
+ return handleSessionLoadOp(ctx, id, params, replayHistory);
311
+ }
312
+ if (ctx.sessions.size >= ctx.maxSessions) {
313
+ await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
314
+ return false;
315
+ }
302
316
  const restored = {
303
317
  id: sessionId,
304
318
  cwd: restoredCwd,
@@ -402,9 +416,17 @@ async function handleSessionForkOp(ctx, id, params) {
402
416
  }
403
417
  forkCwd = resolved;
404
418
  }
419
+ if (ctx.sessions.get(sourceId) !== source) {
420
+ await ctx.sendError(id, -32e3, `session not found: ${sourceId}`);
421
+ return false;
422
+ }
405
423
  const forkSkipped = [];
406
424
  const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
407
425
  const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
426
+ if (ctx.sessions.size >= ctx.maxSessions) {
427
+ await ctx.sendError(id, -32e3, `active session limit reached (${ctx.maxSessions})`);
428
+ return false;
429
+ }
408
430
  const now = (/* @__PURE__ */ new Date()).toISOString();
409
431
  const sessionId = `sess_${ctx.allocId()}`;
410
432
  const forked = {
@@ -1517,7 +1539,7 @@ function toACPInputSchema(src) {
1517
1539
  if (typeof s.maximum === "number") out.maximum = s.maximum;
1518
1540
  if (s.items) out.items = toACPInputSchema(s.items);
1519
1541
  if (s.properties && typeof s.properties === "object") {
1520
- const props = {};
1542
+ const props = /* @__PURE__ */ Object.create(null);
1521
1543
  for (const [k, v] of Object.entries(s.properties)) {
1522
1544
  props[k] = toACPInputSchema(v);
1523
1545
  }
@@ -1843,8 +1865,8 @@ function cancelLateAcpSession(transport, createPromise) {
1843
1865
  const timer = setTimeout(() => {
1844
1866
  reject(new Error("late session/cancel send timed out"));
1845
1867
  }, LATE_CANCEL_SEND_TIMEOUT_MS);
1846
- Promise.resolve(
1847
- transport.send({
1868
+ Promise.resolve().then(
1869
+ () => transport.send({
1848
1870
  jsonrpc: "2.0",
1849
1871
  method: "session/cancel",
1850
1872
  params: { sessionId: lateId }
@@ -1883,11 +1905,6 @@ function cancelLateAcpSession(transport, createPromise) {
1883
1905
  });
1884
1906
  }
1885
1907
 
1886
- // src/client/acp-message-routing.ts
1887
- function isBestEffortAckMethod(method) {
1888
- return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
1889
- }
1890
-
1891
1908
  // src/client/acp-session-errors.ts
1892
1909
  var ACPSessionError = class extends Error {
1893
1910
  kind;
@@ -1900,7 +1917,7 @@ var ACPSessionError = class extends Error {
1900
1917
  }
1901
1918
  };
1902
1919
  function isJsonRpcError(v) {
1903
- return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
1920
+ return typeof v === "object" && v !== null && Number.isSafeInteger(v.code) && typeof v.message === "string";
1904
1921
  }
1905
1922
  function isAuthRequiredError(err) {
1906
1923
  const message = err instanceof Error ? err.message : String(err);
@@ -1948,6 +1965,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
1948
1965
  if (ctx.sessionId && ctx.sessionId !== sessionId) {
1949
1966
  await ctx.closeSession();
1950
1967
  }
1968
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1951
1969
  ctx.resetScratch();
1952
1970
  const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1953
1971
  const id = ctx.allocId();
@@ -1959,6 +1977,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
1959
1977
  if (isJsonRpcError(result)) {
1960
1978
  throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
1961
1979
  }
1980
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1962
1981
  ctx.setSessionId(sessionId);
1963
1982
  }
1964
1983
  async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
@@ -1972,6 +1991,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
1972
1991
  if (ctx.sessionId && ctx.sessionId !== sessionId) {
1973
1992
  await ctx.closeSession();
1974
1993
  }
1994
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1975
1995
  const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1976
1996
  const id = ctx.allocId();
1977
1997
  const result = await ctx.sendRequest(id, "session/resume", {
@@ -1982,6 +2002,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
1982
2002
  if (isJsonRpcError(result)) {
1983
2003
  throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
1984
2004
  }
2005
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1985
2006
  ctx.setSessionId(sessionId);
1986
2007
  }
1987
2008
  async function executeListSessions(ctx, cursor, cwd) {
@@ -2130,10 +2151,11 @@ async function executeCreateSession(ctx) {
2130
2151
  }
2131
2152
 
2132
2153
  // src/client/acp-session-auth.ts
2154
+ function ensureOpen(host) {
2155
+ if (host.state === "closed") throw new ACPSessionError("closed", "session is closed");
2156
+ }
2133
2157
  async function authenticate(host, methodId) {
2134
- if (host.state === "closed") {
2135
- throw new ACPSessionError("closed", "session is closed");
2136
- }
2158
+ ensureOpen(host);
2137
2159
  if (host.state !== "ready" && host.state !== "authenticated") {
2138
2160
  throw new ACPSessionError(
2139
2161
  "protocol_error",
@@ -2158,12 +2180,11 @@ async function authenticate(host, methodId) {
2158
2180
  if (isJsonRpcError(result)) {
2159
2181
  throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
2160
2182
  }
2183
+ ensureOpen(host);
2161
2184
  host.state = "authenticated";
2162
2185
  }
2163
2186
  async function logout(host) {
2164
- if (host.state === "closed") {
2165
- throw new ACPSessionError("closed", "session is closed");
2166
- }
2187
+ ensureOpen(host);
2167
2188
  if (!host.agentCapabilities.auth?.logout) {
2168
2189
  throw new ACPSessionError(
2169
2190
  "unsupported_capability",
@@ -2175,6 +2196,7 @@ async function logout(host) {
2175
2196
  if (isJsonRpcError(result)) {
2176
2197
  throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
2177
2198
  }
2199
+ ensureOpen(host);
2178
2200
  host.state = "ready";
2179
2201
  }
2180
2202
  async function createSessionWithAuth(host) {
@@ -2216,6 +2238,44 @@ async function ensureAuthenticated(host) {
2216
2238
  );
2217
2239
  }
2218
2240
 
2241
+ // src/client/acp-session-content.ts
2242
+ function textContent(text) {
2243
+ return { type: "text", text };
2244
+ }
2245
+ function imageContent(mimeType, data) {
2246
+ return { type: "image", mimeType, data };
2247
+ }
2248
+ function audioContent(mimeType, data) {
2249
+ return { type: "audio", mimeType, data };
2250
+ }
2251
+ function extractText(block) {
2252
+ if (typeof block !== "object" || block === null) return "";
2253
+ const b = block;
2254
+ if (b.type === "text" && typeof b.text === "string") return b.text;
2255
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2256
+ return b.resource.text;
2257
+ }
2258
+ return "";
2259
+ }
2260
+ function isRecord(v) {
2261
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2262
+ }
2263
+ function emptyRunResult(stopReason) {
2264
+ return {
2265
+ text: "",
2266
+ stopReason,
2267
+ hasText: false,
2268
+ toolCalls: [],
2269
+ diffs: [],
2270
+ thoughts: ""
2271
+ };
2272
+ }
2273
+
2274
+ // src/client/acp-message-routing.ts
2275
+ function isBestEffortAckMethod(method) {
2276
+ return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
2277
+ }
2278
+
2219
2279
  // src/client/file-server.ts
2220
2280
  import { randomBytes } from "node:crypto";
2221
2281
  import { realpathSync } from "node:fs";
@@ -2272,6 +2332,9 @@ var FileServer = class {
2272
2332
  const stat3 = await this.operations.stat(safe).catch((err) => {
2273
2333
  throw mapFsError(err, safe);
2274
2334
  });
2335
+ if (controller.signal.aborted) {
2336
+ throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
2337
+ }
2275
2338
  if (stat3.size > this.maxReadBytes) {
2276
2339
  throw new FsError(
2277
2340
  "TOO_LARGE",
@@ -2283,6 +2346,17 @@ var FileServer = class {
2283
2346
  encoding: "utf8",
2284
2347
  signal: controller.signal
2285
2348
  });
2349
+ if (controller.signal.aborted) {
2350
+ throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
2351
+ }
2352
+ const actualSize = Buffer.byteLength(content, "utf8");
2353
+ if (actualSize > this.maxReadBytes) {
2354
+ throw new FsError(
2355
+ "TOO_LARGE",
2356
+ safe,
2357
+ `file is ${actualSize} bytes after read, max read is ${this.maxReadBytes} bytes`
2358
+ );
2359
+ }
2286
2360
  if (params.line == null && params.limit == null) return { content };
2287
2361
  const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2288
2362
  const start = (params.line ?? 1) - 1;
@@ -2321,9 +2395,21 @@ var FileServer = class {
2321
2395
  encoding: "utf8",
2322
2396
  signal: controller.signal
2323
2397
  });
2398
+ if (controller.signal.aborted) {
2399
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
2400
+ }
2324
2401
  await this.assertRealInside(tmp);
2402
+ if (controller.signal.aborted) {
2403
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
2404
+ }
2325
2405
  await this.assertRealInside(path2.dirname(safe));
2406
+ if (controller.signal.aborted) {
2407
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
2408
+ }
2326
2409
  await this.operations.rename(tmp, safe);
2410
+ if (controller.signal.aborted) {
2411
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
2412
+ }
2327
2413
  } catch (err) {
2328
2414
  if (err instanceof FsError) {
2329
2415
  await this.operations.unlink(tmp).catch(() => void 0);
@@ -2422,7 +2508,7 @@ function safeRealpathSync(p) {
2422
2508
 
2423
2509
  // src/client/acp-session-callbacks.ts
2424
2510
  var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
2425
- async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
2511
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions2 = {}) {
2426
2512
  const id = msg.id;
2427
2513
  if (id === void 0) return;
2428
2514
  const params = msg.params;
@@ -2439,7 +2525,7 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
2439
2525
  toolCall,
2440
2526
  options: permissionOptions
2441
2527
  },
2442
- callbackOptions
2528
+ callbackOptions2
2443
2529
  );
2444
2530
  await sender.sendResult(id, { outcome });
2445
2531
  } catch (err) {
@@ -2451,7 +2537,7 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
2451
2537
  await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2452
2538
  }
2453
2539
  }
2454
- async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
2540
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions2 = {}) {
2455
2541
  const id = msg.id;
2456
2542
  if (id === void 0) return;
2457
2543
  const params = msg.params;
@@ -2468,7 +2554,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
2468
2554
  kind: "edit",
2469
2555
  rawInput: { path: params.path, sessionId: params.sessionId }
2470
2556
  },
2471
- callbackOptions
2557
+ callbackOptions2
2472
2558
  );
2473
2559
  if (authorization !== "allowed") {
2474
2560
  const isCancelled = authorization === "cancelled";
@@ -2503,7 +2589,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
2503
2589
  await sender.sendErrorResponse(id, code, message);
2504
2590
  }
2505
2591
  }
2506
- async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
2592
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions2 = {}) {
2507
2593
  const id = msg.id;
2508
2594
  if (id === void 0) return;
2509
2595
  const params = msg.params ?? {};
@@ -2523,7 +2609,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2523
2609
  sessionId: params.sessionId
2524
2610
  }
2525
2611
  },
2526
- callbackOptions
2612
+ callbackOptions2
2527
2613
  );
2528
2614
  if (authorization !== "allowed") {
2529
2615
  const isCancelled = authorization === "cancelled";
@@ -2584,7 +2670,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2584
2670
  await sender.sendErrorResponse(id, -32603, message);
2585
2671
  }
2586
2672
  }
2587
- async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
2673
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions2) {
2588
2674
  try {
2589
2675
  const outcome = await runPermissionWithDeadline(
2590
2676
  permissionPolicy,
@@ -2602,25 +2688,25 @@ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions)
2602
2688
  { optionId: "reject", name: "Reject", kind: "reject_once" }
2603
2689
  ]
2604
2690
  },
2605
- callbackOptions
2691
+ callbackOptions2
2606
2692
  );
2607
2693
  return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2608
2694
  } catch (err) {
2609
2695
  return isAbortLikeError(err) ? "cancelled" : "denied";
2610
2696
  }
2611
2697
  }
2612
- async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2613
- const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2698
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions2) {
2699
+ const timeoutMs = resolvePermissionDeadline(callbackOptions2.permissionTimeoutMs);
2614
2700
  const controller = new AbortController();
2615
2701
  const abort = () => controller.abort();
2616
2702
  const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2617
2703
  let removeAbort;
2618
- if (callbackOptions.signal) {
2619
- if (callbackOptions.signal.aborted) {
2704
+ if (callbackOptions2.signal) {
2705
+ if (callbackOptions2.signal.aborted) {
2620
2706
  abort();
2621
2707
  } else {
2622
- callbackOptions.signal.addEventListener("abort", abort, { once: true });
2623
- removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2708
+ callbackOptions2.signal.addEventListener("abort", abort, { once: true });
2709
+ removeAbort = () => callbackOptions2.signal?.removeEventListener("abort", abort);
2624
2710
  }
2625
2711
  }
2626
2712
  try {
@@ -2656,54 +2742,21 @@ function resolvePermissionDeadline(value) {
2656
2742
  return DEFAULT_PERMISSION_TIMEOUT_MS;
2657
2743
  }
2658
2744
 
2659
- // src/client/acp-session-content.ts
2660
- function textContent(text) {
2661
- return { type: "text", text };
2662
- }
2663
- function imageContent(mimeType, data) {
2664
- return { type: "image", mimeType, data };
2665
- }
2666
- function audioContent(mimeType, data) {
2667
- return { type: "audio", mimeType, data };
2668
- }
2669
- function extractText(block) {
2670
- if (typeof block !== "object" || block === null) return "";
2671
- const b = block;
2672
- if (b.type === "text" && typeof b.text === "string") return b.text;
2673
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2674
- return b.resource.text;
2675
- }
2676
- return "";
2677
- }
2678
- function isRecord(v) {
2679
- return typeof v === "object" && v !== null && !Array.isArray(v);
2680
- }
2681
- function emptyRunResult(stopReason) {
2682
- return {
2683
- text: "",
2684
- stopReason,
2685
- hasText: false,
2686
- toolCalls: [],
2687
- diffs: [],
2688
- thoughts: ""
2689
- };
2690
- }
2691
-
2692
2745
  // src/client/acp-session-updates.ts
2693
2746
  function createSessionScratch() {
2694
2747
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2695
2748
  }
2696
- function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2749
+ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2697
2750
  const update = msg.params?.update;
2698
2751
  if (typeof update !== "object" || update === null) return;
2699
2752
  const u = update;
2700
- emitProgress({ type: "raw", update: u });
2753
+ emitProgress2({ type: "raw", update: u });
2701
2754
  switch (u.sessionUpdate) {
2702
2755
  case "agent_message_chunk": {
2703
2756
  const text = extractText(u.content);
2704
2757
  if (text) {
2705
2758
  scratch.text += text;
2706
- emitProgress({ type: "message", text });
2759
+ emitProgress2({ type: "message", text });
2707
2760
  }
2708
2761
  return;
2709
2762
  }
@@ -2712,29 +2765,29 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2712
2765
  const text = extractText(u.content);
2713
2766
  if (text) {
2714
2767
  scratch.thoughts += text;
2715
- emitProgress({ type: "thought", text });
2768
+ emitProgress2({ type: "thought", text });
2716
2769
  }
2717
2770
  return;
2718
2771
  }
2719
2772
  case "tool_call":
2720
2773
  case "tool_call_update":
2721
- captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
2774
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress2);
2722
2775
  return;
2723
2776
  case "plan":
2724
2777
  if (Array.isArray(u.entries)) {
2725
2778
  scratch.plan = u.entries;
2726
- emitProgress({ type: "plan", entries: u.entries });
2779
+ emitProgress2({ type: "plan", entries: u.entries });
2727
2780
  }
2728
2781
  return;
2729
2782
  case "usage_update":
2730
- if (typeof u.used === "number" && typeof u.size === "number") {
2783
+ if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
2731
2784
  const usage = {
2732
2785
  used: u.used,
2733
2786
  size: u.size,
2734
2787
  ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2735
2788
  };
2736
2789
  scratch.usage = usage;
2737
- emitProgress({ type: "usage", usage });
2790
+ emitProgress2({ type: "usage", usage });
2738
2791
  }
2739
2792
  return;
2740
2793
  case "available_commands_update":
@@ -2749,7 +2802,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2749
2802
  return;
2750
2803
  }
2751
2804
  }
2752
- function captureToolCall(u, isNew, scratch, emitProgress) {
2805
+ function captureToolCall(u, isNew, scratch, emitProgress2) {
2753
2806
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2754
2807
  if (!toolCallId) return;
2755
2808
  const prev = scratch.toolCalls.get(toolCallId);
@@ -2771,16 +2824,119 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
2771
2824
  newText: c.newText
2772
2825
  };
2773
2826
  scratch.diffs.push(diff);
2774
- emitProgress({ type: "diff", diff });
2827
+ emitProgress2({ type: "diff", diff });
2775
2828
  }
2776
2829
  }
2777
2830
  }
2778
- emitProgress({
2831
+ emitProgress2({
2779
2832
  type: isNew ? "tool_call" : "tool_call_update",
2780
2833
  toolCall: record
2781
2834
  });
2782
2835
  }
2783
2836
 
2837
+ // src/client/acp-session-messages.ts
2838
+ function responseSender() {
2839
+ return {
2840
+ sendResult: (id, result) => this.sendResult(id, result),
2841
+ sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
2842
+ };
2843
+ }
2844
+ function callbackOptions() {
2845
+ const promptSignal = this.promptCallbackAbort?.signal;
2846
+ const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
2847
+ return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
2848
+ }
2849
+ function handleMessage(msg) {
2850
+ if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
2851
+ const pending = this.pending.get(msg.id);
2852
+ if (!pending) return;
2853
+ clearTimeout(pending.timeoutHandle);
2854
+ this.pending.delete(msg.id);
2855
+ if (msg.error !== void 0) {
2856
+ const method = pending.method;
2857
+ const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
2858
+ pending.reject(
2859
+ new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
2860
+ );
2861
+ } else {
2862
+ pending.resolve(msg.result);
2863
+ }
2864
+ return;
2865
+ }
2866
+ if (msg.method === "session/update") {
2867
+ const updateSessionId = msg.params?.sessionId;
2868
+ if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
2869
+ handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
2870
+ return;
2871
+ }
2872
+ if (msg.method === "session/request_permission") {
2873
+ handleAcpPermissionRequest(
2874
+ msg,
2875
+ this.permissionPolicy,
2876
+ this.responseSender(),
2877
+ this.callbackOptions()
2878
+ ).catch(() => {
2879
+ });
2880
+ return;
2881
+ }
2882
+ if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
2883
+ handleAcpFsRequest(
2884
+ msg,
2885
+ this.fileServer,
2886
+ this.permissionPolicy,
2887
+ this.responseSender(),
2888
+ this.callbackOptions()
2889
+ ).catch(() => {
2890
+ });
2891
+ return;
2892
+ }
2893
+ if (msg.method?.startsWith("terminal/")) {
2894
+ handleAcpTerminalRequest(
2895
+ msg,
2896
+ this.terminalServer,
2897
+ this.permissionPolicy,
2898
+ this.responseSender(),
2899
+ this.callbackOptions()
2900
+ ).catch(() => {
2901
+ });
2902
+ return;
2903
+ }
2904
+ if (isBestEffortAckMethod(msg.method)) {
2905
+ if (msg.id !== void 0) {
2906
+ this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
2907
+ });
2908
+ }
2909
+ return;
2910
+ }
2911
+ if (msg.method === "$/cancel_request") {
2912
+ return;
2913
+ }
2914
+ if (msg.method) {
2915
+ if (msg.id !== void 0) {
2916
+ void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
2917
+ () => {
2918
+ }
2919
+ );
2920
+ return;
2921
+ }
2922
+ console.warn(
2923
+ JSON.stringify({
2924
+ level: "warn",
2925
+ event: "acp_session.unhandled_method",
2926
+ method: msg.method,
2927
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2928
+ })
2929
+ );
2930
+ }
2931
+ }
2932
+ function emitProgress(event) {
2933
+ if (!this.progressHandler) return;
2934
+ try {
2935
+ this.progressHandler(event);
2936
+ } catch {
2937
+ }
2938
+ }
2939
+
2784
2940
  // src/client/permission.ts
2785
2941
  function pickAllow(options) {
2786
2942
  const ranked = [...options].sort((a, b) => {
@@ -2819,6 +2975,7 @@ function makePermissionPolicy(decide) {
2819
2975
  return async (req) => {
2820
2976
  if (req.signal.aborted) return { outcome: "cancelled" };
2821
2977
  const allow = await decide(req);
2978
+ if (req.signal.aborted) return { outcome: "cancelled" };
2822
2979
  return allow ? pickAllow(req.options) : pickReject(req.options);
2823
2980
  };
2824
2981
  }
@@ -2871,8 +3028,8 @@ var TerminalServer = class {
2871
3028
  constructor(opts) {
2872
3029
  this.projectRoot = path3.resolve(opts.projectRoot);
2873
3030
  this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
2874
- this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
2875
- this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
3031
+ this.outputByteLimit = this.clampFiniteInt(opts.outputByteLimit, 1024 * 1024);
3032
+ this.maxOutputByteLimit = this.clampFiniteInt(opts.maxOutputByteLimit, 16 * 1024 * 1024);
2876
3033
  this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
2877
3034
  this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
2878
3035
  this.abortSignal = opts.signal;
@@ -2933,7 +3090,7 @@ var TerminalServer = class {
2933
3090
  clearTimeout(state.timeoutHandle);
2934
3091
  state.timeoutHandle = null;
2935
3092
  }
2936
- const exitStatus = {
3093
+ const exitStatus = state.exitStatus ?? {
2937
3094
  exitCode: typeof code === "number" ? code : null,
2938
3095
  signal: typeof signalName === "string" ? signalName : null
2939
3096
  };
@@ -3133,7 +3290,7 @@ var TerminalServer = class {
3133
3290
  * unbounded memory growth.
3134
3291
  */
3135
3292
  clampFiniteInt(value, defaultValue) {
3136
- if (value === void 0 || !Number.isFinite(value) || value < 1) {
3293
+ if (value === void 0 || !Number.isFinite(value) || value < 1 || !Number.isSafeInteger(Math.trunc(value))) {
3137
3294
  return defaultValue;
3138
3295
  }
3139
3296
  return Math.trunc(value);
@@ -3264,7 +3421,7 @@ var WebSocketClientTransport = class {
3264
3421
  )
3265
3422
  );
3266
3423
  }
3267
- const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
3424
+ const timeoutMs = finitePositiveLimit(this.opts.handshakeTimeoutMs, 3e4);
3268
3425
  return new Promise((resolve5, reject) => {
3269
3426
  const ws = new WS(this.opts.url, this.opts.protocols);
3270
3427
  ws.binaryType = "arraybuffer";
@@ -3410,7 +3567,9 @@ var WebSocketClientTransport = class {
3410
3567
  }
3411
3568
  };
3412
3569
  function finitePositiveLimit(value, fallback) {
3413
- return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
3570
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
3571
+ const normalized = Math.floor(value);
3572
+ return Number.isSafeInteger(normalized) && normalized >= 1 ? normalized : fallback;
3414
3573
  }
3415
3574
 
3416
3575
  // src/client/acp-session.ts
@@ -3622,7 +3781,9 @@ var ACPSession = class _ACPSession {
3622
3781
  opContext() {
3623
3782
  const self = this;
3624
3783
  return {
3625
- closed: this.closed,
3784
+ get closed() {
3785
+ return self.closed;
3786
+ },
3626
3787
  get sessionId() {
3627
3788
  return self.sessionId;
3628
3789
  },
@@ -3923,105 +4084,16 @@ var ACPSession = class _ACPSession {
3923
4084
  });
3924
4085
  }
3925
4086
  responseSender() {
3926
- return {
3927
- sendResult: (id, result) => this.sendResult(id, result),
3928
- sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
3929
- };
4087
+ return responseSender.call(this.acpSessionMessagesHost());
3930
4088
  }
3931
4089
  callbackOptions() {
3932
- const promptSignal = this.promptCallbackAbort?.signal;
3933
- const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
3934
- return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
4090
+ return callbackOptions.call(this.acpSessionMessagesHost());
3935
4091
  }
3936
4092
  handleMessage(msg) {
3937
- if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
3938
- const pending = this.pending.get(msg.id);
3939
- if (!pending) return;
3940
- clearTimeout(pending.timeoutHandle);
3941
- this.pending.delete(msg.id);
3942
- if (msg.error !== void 0) {
3943
- const method = pending.method;
3944
- const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
3945
- pending.reject(
3946
- new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
3947
- );
3948
- } else {
3949
- pending.resolve(msg.result);
3950
- }
3951
- return;
3952
- }
3953
- if (msg.method === "session/update") {
3954
- const updateSessionId = msg.params?.sessionId;
3955
- if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
3956
- handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
3957
- return;
3958
- }
3959
- if (msg.method === "session/request_permission") {
3960
- handleAcpPermissionRequest(
3961
- msg,
3962
- this.permissionPolicy,
3963
- this.responseSender(),
3964
- this.callbackOptions()
3965
- ).catch(() => {
3966
- });
3967
- return;
3968
- }
3969
- if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
3970
- handleAcpFsRequest(
3971
- msg,
3972
- this.fileServer,
3973
- this.permissionPolicy,
3974
- this.responseSender(),
3975
- this.callbackOptions()
3976
- ).catch(() => {
3977
- });
3978
- return;
3979
- }
3980
- if (msg.method?.startsWith("terminal/")) {
3981
- handleAcpTerminalRequest(
3982
- msg,
3983
- this.terminalServer,
3984
- this.permissionPolicy,
3985
- this.responseSender(),
3986
- this.callbackOptions()
3987
- ).catch(() => {
3988
- });
3989
- return;
3990
- }
3991
- if (isBestEffortAckMethod(msg.method)) {
3992
- if (msg.id !== void 0) {
3993
- this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
3994
- });
3995
- }
3996
- return;
3997
- }
3998
- if (msg.method === "$/cancel_request") {
3999
- return;
4000
- }
4001
- if (msg.method) {
4002
- if (msg.id !== void 0) {
4003
- void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
4004
- () => {
4005
- }
4006
- );
4007
- return;
4008
- }
4009
- console.warn(
4010
- JSON.stringify({
4011
- level: "warn",
4012
- event: "acp_session.unhandled_method",
4013
- method: msg.method,
4014
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4015
- })
4016
- );
4017
- }
4093
+ handleMessage.call(this.acpSessionMessagesHost(), msg);
4018
4094
  }
4019
4095
  emitProgress(event) {
4020
- if (!this.progressHandler) return;
4021
- try {
4022
- this.progressHandler(event);
4023
- } catch {
4024
- }
4096
+ emitProgress.call(this.acpSessionMessagesHost(), event);
4025
4097
  }
4026
4098
  progressHandler = null;
4027
4099
  scratch = createSessionScratch();
@@ -4051,6 +4123,23 @@ var ACPSession = class _ACPSession {
4051
4123
  opts: this.opts
4052
4124
  };
4053
4125
  }
4126
+ acpSessionMessagesHost() {
4127
+ void this.sendResult;
4128
+ void this.sendErrorResponse;
4129
+ void this.promptCallbackAbort;
4130
+ void this.callbackAbort;
4131
+ void this.pending;
4132
+ void this.sessionId;
4133
+ void this.scratch;
4134
+ void this.emitProgress;
4135
+ void this.permissionPolicy;
4136
+ void this.responseSender;
4137
+ void this.callbackOptions;
4138
+ void this.fileServer;
4139
+ void this.terminalServer;
4140
+ void this.progressHandler;
4141
+ return this;
4142
+ }
4054
4143
  };
4055
4144
 
4056
4145
  // src/client/tool-translator.ts
@@ -4096,18 +4185,30 @@ var ToolTranslator = class {
4096
4185
  * response arrives.
4097
4186
  */
4098
4187
  async callTool(transport, name, args, callId = crypto.randomUUID()) {
4099
- await transport.send({
4100
- jsonrpc: "2.0",
4101
- method: "tools/call",
4102
- id: callId,
4103
- params: { name, arguments: args }
4104
- });
4188
+ if (this.pending.has(callId)) throw new Error(`Tool call id ${callId} is already pending`);
4105
4189
  return new Promise((resolve5, reject) => {
4106
4190
  const timeout = setTimeout(() => {
4107
4191
  this.pending.delete(callId);
4108
4192
  reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
4109
4193
  }, this.opts.totalTimeoutMs);
4110
- this.pending.set(callId, { resolve: resolve5, reject, timeout });
4194
+ const pending = { resolve: resolve5, reject, timeout };
4195
+ this.pending.set(callId, pending);
4196
+ const failSend = (err) => {
4197
+ if (this.pending.get(callId) !== pending) return;
4198
+ clearTimeout(timeout);
4199
+ this.pending.delete(callId);
4200
+ reject(err instanceof Error ? err : new Error(String(err)));
4201
+ };
4202
+ try {
4203
+ transport.send({
4204
+ jsonrpc: "2.0",
4205
+ method: "tools/call",
4206
+ id: callId,
4207
+ params: { name, arguments: args }
4208
+ }).catch(failSend);
4209
+ } catch (err) {
4210
+ failSend(err);
4211
+ }
4111
4212
  });
4112
4213
  }
4113
4214
  cancelAll() {
@@ -4241,7 +4342,8 @@ async function runAcpBench(opts) {
4241
4342
  const timeoutMs = opts.timeoutMs ?? 6e4;
4242
4343
  const checkFs = opts.checkFs ?? false;
4243
4344
  const marker = opts.marker ?? randomMarker();
4244
- const concurrency = Math.max(1, opts.concurrency ?? 2);
4345
+ const requestedConcurrency = opts.concurrency ?? 2;
4346
+ const concurrency = Number.isNaN(requestedConcurrency) ? 2 : Math.max(1, Math.floor(requestedConcurrency));
4245
4347
  const seen = /* @__PURE__ */ new Set();
4246
4348
  const ids = [];
4247
4349
  for (const raw of opts.agentIds) {
@@ -5081,7 +5183,8 @@ var DEFAULT_MAX_CONCURRENCY = 4;
5081
5183
  async function mapBound(items, worker, limit) {
5082
5184
  const results = new Array(items.length);
5083
5185
  if (items.length === 0) return results;
5084
- const safeLimit = Math.max(1, Math.min(limit, items.length));
5186
+ const normalizedLimit = Number.isNaN(limit) ? DEFAULT_MAX_CONCURRENCY : Math.floor(limit);
5187
+ const safeLimit = Math.max(1, Math.min(normalizedLimit, items.length));
5085
5188
  let nextIndex = 0;
5086
5189
  const workerCount = Math.min(safeLimit, items.length);
5087
5190
  const runners = [];