@wrongstack/acp 1.0.28 → 1.0.32

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;
@@ -297,8 +304,15 @@ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
297
304
  await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
298
305
  return false;
299
306
  }
300
- const candidateCwd = persisted.cwd ?? loadCwd ?? ctx.defaultCwd;
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 = {
@@ -478,6 +500,7 @@ async function handleSessionPromptOp(ctx, id, params) {
478
500
  let result;
479
501
  const pendingNotifications = [];
480
502
  const emit = (update) => {
503
+ if (ctx.sessions.get(sessionId) !== session) return;
481
504
  const notifPromise = ctx.sendNotification({ sessionId, update });
482
505
  pendingNotifications.push(notifPromise.catch(() => {
483
506
  }));
@@ -1516,7 +1539,7 @@ function toACPInputSchema(src) {
1516
1539
  if (typeof s.maximum === "number") out.maximum = s.maximum;
1517
1540
  if (s.items) out.items = toACPInputSchema(s.items);
1518
1541
  if (s.properties && typeof s.properties === "object") {
1519
- const props = {};
1542
+ const props = /* @__PURE__ */ Object.create(null);
1520
1543
  for (const [k, v] of Object.entries(s.properties)) {
1521
1544
  props[k] = toACPInputSchema(v);
1522
1545
  }
@@ -1842,8 +1865,8 @@ function cancelLateAcpSession(transport, createPromise) {
1842
1865
  const timer = setTimeout(() => {
1843
1866
  reject(new Error("late session/cancel send timed out"));
1844
1867
  }, LATE_CANCEL_SEND_TIMEOUT_MS);
1845
- Promise.resolve(
1846
- transport.send({
1868
+ Promise.resolve().then(
1869
+ () => transport.send({
1847
1870
  jsonrpc: "2.0",
1848
1871
  method: "session/cancel",
1849
1872
  params: { sessionId: lateId }
@@ -1882,11 +1905,6 @@ function cancelLateAcpSession(transport, createPromise) {
1882
1905
  });
1883
1906
  }
1884
1907
 
1885
- // src/client/acp-message-routing.ts
1886
- function isBestEffortAckMethod(method) {
1887
- return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
1888
- }
1889
-
1890
1908
  // src/client/acp-session-errors.ts
1891
1909
  var ACPSessionError = class extends Error {
1892
1910
  kind;
@@ -1899,7 +1917,7 @@ var ACPSessionError = class extends Error {
1899
1917
  }
1900
1918
  };
1901
1919
  function isJsonRpcError(v) {
1902
- 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";
1903
1921
  }
1904
1922
  function isAuthRequiredError(err) {
1905
1923
  const message = err instanceof Error ? err.message : String(err);
@@ -1947,6 +1965,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
1947
1965
  if (ctx.sessionId && ctx.sessionId !== sessionId) {
1948
1966
  await ctx.closeSession();
1949
1967
  }
1968
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1950
1969
  ctx.resetScratch();
1951
1970
  const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1952
1971
  const id = ctx.allocId();
@@ -1958,6 +1977,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
1958
1977
  if (isJsonRpcError(result)) {
1959
1978
  throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
1960
1979
  }
1980
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1961
1981
  ctx.setSessionId(sessionId);
1962
1982
  }
1963
1983
  async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
@@ -1971,6 +1991,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
1971
1991
  if (ctx.sessionId && ctx.sessionId !== sessionId) {
1972
1992
  await ctx.closeSession();
1973
1993
  }
1994
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1974
1995
  const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1975
1996
  const id = ctx.allocId();
1976
1997
  const result = await ctx.sendRequest(id, "session/resume", {
@@ -1981,6 +2002,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
1981
2002
  if (isJsonRpcError(result)) {
1982
2003
  throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
1983
2004
  }
2005
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1984
2006
  ctx.setSessionId(sessionId);
1985
2007
  }
1986
2008
  async function executeListSessions(ctx, cursor, cwd) {
@@ -2129,10 +2151,11 @@ async function executeCreateSession(ctx) {
2129
2151
  }
2130
2152
 
2131
2153
  // src/client/acp-session-auth.ts
2154
+ function ensureOpen(host) {
2155
+ if (host.state === "closed") throw new ACPSessionError("closed", "session is closed");
2156
+ }
2132
2157
  async function authenticate(host, methodId) {
2133
- if (host.state === "closed") {
2134
- throw new ACPSessionError("closed", "session is closed");
2135
- }
2158
+ ensureOpen(host);
2136
2159
  if (host.state !== "ready" && host.state !== "authenticated") {
2137
2160
  throw new ACPSessionError(
2138
2161
  "protocol_error",
@@ -2157,12 +2180,11 @@ async function authenticate(host, methodId) {
2157
2180
  if (isJsonRpcError(result)) {
2158
2181
  throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
2159
2182
  }
2183
+ ensureOpen(host);
2160
2184
  host.state = "authenticated";
2161
2185
  }
2162
2186
  async function logout(host) {
2163
- if (host.state === "closed") {
2164
- throw new ACPSessionError("closed", "session is closed");
2165
- }
2187
+ ensureOpen(host);
2166
2188
  if (!host.agentCapabilities.auth?.logout) {
2167
2189
  throw new ACPSessionError(
2168
2190
  "unsupported_capability",
@@ -2174,6 +2196,7 @@ async function logout(host) {
2174
2196
  if (isJsonRpcError(result)) {
2175
2197
  throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
2176
2198
  }
2199
+ ensureOpen(host);
2177
2200
  host.state = "ready";
2178
2201
  }
2179
2202
  async function createSessionWithAuth(host) {
@@ -2215,6 +2238,44 @@ async function ensureAuthenticated(host) {
2215
2238
  );
2216
2239
  }
2217
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
+
2218
2279
  // src/client/file-server.ts
2219
2280
  import { randomBytes } from "node:crypto";
2220
2281
  import { realpathSync } from "node:fs";
@@ -2271,6 +2332,9 @@ var FileServer = class {
2271
2332
  const stat3 = await this.operations.stat(safe).catch((err) => {
2272
2333
  throw mapFsError(err, safe);
2273
2334
  });
2335
+ if (controller.signal.aborted) {
2336
+ throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
2337
+ }
2274
2338
  if (stat3.size > this.maxReadBytes) {
2275
2339
  throw new FsError(
2276
2340
  "TOO_LARGE",
@@ -2282,6 +2346,17 @@ var FileServer = class {
2282
2346
  encoding: "utf8",
2283
2347
  signal: controller.signal
2284
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
+ }
2285
2360
  if (params.line == null && params.limit == null) return { content };
2286
2361
  const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2287
2362
  const start = (params.line ?? 1) - 1;
@@ -2320,9 +2395,21 @@ var FileServer = class {
2320
2395
  encoding: "utf8",
2321
2396
  signal: controller.signal
2322
2397
  });
2398
+ if (controller.signal.aborted) {
2399
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
2400
+ }
2323
2401
  await this.assertRealInside(tmp);
2402
+ if (controller.signal.aborted) {
2403
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
2404
+ }
2324
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
+ }
2325
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
+ }
2326
2413
  } catch (err) {
2327
2414
  if (err instanceof FsError) {
2328
2415
  await this.operations.unlink(tmp).catch(() => void 0);
@@ -2421,7 +2508,7 @@ function safeRealpathSync(p) {
2421
2508
 
2422
2509
  // src/client/acp-session-callbacks.ts
2423
2510
  var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
2424
- async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
2511
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions2 = {}) {
2425
2512
  const id = msg.id;
2426
2513
  if (id === void 0) return;
2427
2514
  const params = msg.params;
@@ -2438,7 +2525,7 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
2438
2525
  toolCall,
2439
2526
  options: permissionOptions
2440
2527
  },
2441
- callbackOptions
2528
+ callbackOptions2
2442
2529
  );
2443
2530
  await sender.sendResult(id, { outcome });
2444
2531
  } catch (err) {
@@ -2450,7 +2537,7 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
2450
2537
  await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2451
2538
  }
2452
2539
  }
2453
- async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
2540
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions2 = {}) {
2454
2541
  const id = msg.id;
2455
2542
  if (id === void 0) return;
2456
2543
  const params = msg.params;
@@ -2467,7 +2554,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
2467
2554
  kind: "edit",
2468
2555
  rawInput: { path: params.path, sessionId: params.sessionId }
2469
2556
  },
2470
- callbackOptions
2557
+ callbackOptions2
2471
2558
  );
2472
2559
  if (authorization !== "allowed") {
2473
2560
  const isCancelled = authorization === "cancelled";
@@ -2502,7 +2589,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
2502
2589
  await sender.sendErrorResponse(id, code, message);
2503
2590
  }
2504
2591
  }
2505
- async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
2592
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions2 = {}) {
2506
2593
  const id = msg.id;
2507
2594
  if (id === void 0) return;
2508
2595
  const params = msg.params ?? {};
@@ -2522,7 +2609,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2522
2609
  sessionId: params.sessionId
2523
2610
  }
2524
2611
  },
2525
- callbackOptions
2612
+ callbackOptions2
2526
2613
  );
2527
2614
  if (authorization !== "allowed") {
2528
2615
  const isCancelled = authorization === "cancelled";
@@ -2583,7 +2670,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2583
2670
  await sender.sendErrorResponse(id, -32603, message);
2584
2671
  }
2585
2672
  }
2586
- async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
2673
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions2) {
2587
2674
  try {
2588
2675
  const outcome = await runPermissionWithDeadline(
2589
2676
  permissionPolicy,
@@ -2601,25 +2688,25 @@ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions)
2601
2688
  { optionId: "reject", name: "Reject", kind: "reject_once" }
2602
2689
  ]
2603
2690
  },
2604
- callbackOptions
2691
+ callbackOptions2
2605
2692
  );
2606
2693
  return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2607
2694
  } catch (err) {
2608
2695
  return isAbortLikeError(err) ? "cancelled" : "denied";
2609
2696
  }
2610
2697
  }
2611
- async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2612
- const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2698
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions2) {
2699
+ const timeoutMs = resolvePermissionDeadline(callbackOptions2.permissionTimeoutMs);
2613
2700
  const controller = new AbortController();
2614
2701
  const abort = () => controller.abort();
2615
2702
  const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2616
2703
  let removeAbort;
2617
- if (callbackOptions.signal) {
2618
- if (callbackOptions.signal.aborted) {
2704
+ if (callbackOptions2.signal) {
2705
+ if (callbackOptions2.signal.aborted) {
2619
2706
  abort();
2620
2707
  } else {
2621
- callbackOptions.signal.addEventListener("abort", abort, { once: true });
2622
- removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2708
+ callbackOptions2.signal.addEventListener("abort", abort, { once: true });
2709
+ removeAbort = () => callbackOptions2.signal?.removeEventListener("abort", abort);
2623
2710
  }
2624
2711
  }
2625
2712
  try {
@@ -2655,54 +2742,21 @@ function resolvePermissionDeadline(value) {
2655
2742
  return DEFAULT_PERMISSION_TIMEOUT_MS;
2656
2743
  }
2657
2744
 
2658
- // src/client/acp-session-content.ts
2659
- function textContent(text) {
2660
- return { type: "text", text };
2661
- }
2662
- function imageContent(mimeType, data) {
2663
- return { type: "image", mimeType, data };
2664
- }
2665
- function audioContent(mimeType, data) {
2666
- return { type: "audio", mimeType, data };
2667
- }
2668
- function extractText(block) {
2669
- if (typeof block !== "object" || block === null) return "";
2670
- const b = block;
2671
- if (b.type === "text" && typeof b.text === "string") return b.text;
2672
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2673
- return b.resource.text;
2674
- }
2675
- return "";
2676
- }
2677
- function isRecord(v) {
2678
- return typeof v === "object" && v !== null && !Array.isArray(v);
2679
- }
2680
- function emptyRunResult(stopReason) {
2681
- return {
2682
- text: "",
2683
- stopReason,
2684
- hasText: false,
2685
- toolCalls: [],
2686
- diffs: [],
2687
- thoughts: ""
2688
- };
2689
- }
2690
-
2691
2745
  // src/client/acp-session-updates.ts
2692
2746
  function createSessionScratch() {
2693
2747
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2694
2748
  }
2695
- function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2749
+ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
2696
2750
  const update = msg.params?.update;
2697
2751
  if (typeof update !== "object" || update === null) return;
2698
2752
  const u = update;
2699
- emitProgress({ type: "raw", update: u });
2753
+ emitProgress2({ type: "raw", update: u });
2700
2754
  switch (u.sessionUpdate) {
2701
2755
  case "agent_message_chunk": {
2702
2756
  const text = extractText(u.content);
2703
2757
  if (text) {
2704
2758
  scratch.text += text;
2705
- emitProgress({ type: "message", text });
2759
+ emitProgress2({ type: "message", text });
2706
2760
  }
2707
2761
  return;
2708
2762
  }
@@ -2711,29 +2765,29 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2711
2765
  const text = extractText(u.content);
2712
2766
  if (text) {
2713
2767
  scratch.thoughts += text;
2714
- emitProgress({ type: "thought", text });
2768
+ emitProgress2({ type: "thought", text });
2715
2769
  }
2716
2770
  return;
2717
2771
  }
2718
2772
  case "tool_call":
2719
2773
  case "tool_call_update":
2720
- captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
2774
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress2);
2721
2775
  return;
2722
2776
  case "plan":
2723
2777
  if (Array.isArray(u.entries)) {
2724
2778
  scratch.plan = u.entries;
2725
- emitProgress({ type: "plan", entries: u.entries });
2779
+ emitProgress2({ type: "plan", entries: u.entries });
2726
2780
  }
2727
2781
  return;
2728
2782
  case "usage_update":
2729
- 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) {
2730
2784
  const usage = {
2731
2785
  used: u.used,
2732
2786
  size: u.size,
2733
2787
  ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2734
2788
  };
2735
2789
  scratch.usage = usage;
2736
- emitProgress({ type: "usage", usage });
2790
+ emitProgress2({ type: "usage", usage });
2737
2791
  }
2738
2792
  return;
2739
2793
  case "available_commands_update":
@@ -2748,7 +2802,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2748
2802
  return;
2749
2803
  }
2750
2804
  }
2751
- function captureToolCall(u, isNew, scratch, emitProgress) {
2805
+ function captureToolCall(u, isNew, scratch, emitProgress2) {
2752
2806
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2753
2807
  if (!toolCallId) return;
2754
2808
  const prev = scratch.toolCalls.get(toolCallId);
@@ -2770,16 +2824,119 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
2770
2824
  newText: c.newText
2771
2825
  };
2772
2826
  scratch.diffs.push(diff);
2773
- emitProgress({ type: "diff", diff });
2827
+ emitProgress2({ type: "diff", diff });
2774
2828
  }
2775
2829
  }
2776
2830
  }
2777
- emitProgress({
2831
+ emitProgress2({
2778
2832
  type: isNew ? "tool_call" : "tool_call_update",
2779
2833
  toolCall: record
2780
2834
  });
2781
2835
  }
2782
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
+
2783
2940
  // src/client/permission.ts
2784
2941
  function pickAllow(options) {
2785
2942
  const ranked = [...options].sort((a, b) => {
@@ -2818,6 +2975,7 @@ function makePermissionPolicy(decide) {
2818
2975
  return async (req) => {
2819
2976
  if (req.signal.aborted) return { outcome: "cancelled" };
2820
2977
  const allow = await decide(req);
2978
+ if (req.signal.aborted) return { outcome: "cancelled" };
2821
2979
  return allow ? pickAllow(req.options) : pickReject(req.options);
2822
2980
  };
2823
2981
  }
@@ -2870,8 +3028,8 @@ var TerminalServer = class {
2870
3028
  constructor(opts) {
2871
3029
  this.projectRoot = path3.resolve(opts.projectRoot);
2872
3030
  this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
2873
- this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
2874
- 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);
2875
3033
  this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
2876
3034
  this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
2877
3035
  this.abortSignal = opts.signal;
@@ -2932,7 +3090,7 @@ var TerminalServer = class {
2932
3090
  clearTimeout(state.timeoutHandle);
2933
3091
  state.timeoutHandle = null;
2934
3092
  }
2935
- const exitStatus = {
3093
+ const exitStatus = state.exitStatus ?? {
2936
3094
  exitCode: typeof code === "number" ? code : null,
2937
3095
  signal: typeof signalName === "string" ? signalName : null
2938
3096
  };
@@ -3132,7 +3290,7 @@ var TerminalServer = class {
3132
3290
  * unbounded memory growth.
3133
3291
  */
3134
3292
  clampFiniteInt(value, defaultValue) {
3135
- if (value === void 0 || !Number.isFinite(value) || value < 1) {
3293
+ if (value === void 0 || !Number.isFinite(value) || value < 1 || !Number.isSafeInteger(Math.trunc(value))) {
3136
3294
  return defaultValue;
3137
3295
  }
3138
3296
  return Math.trunc(value);
@@ -3263,7 +3421,7 @@ var WebSocketClientTransport = class {
3263
3421
  )
3264
3422
  );
3265
3423
  }
3266
- const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
3424
+ const timeoutMs = finitePositiveLimit(this.opts.handshakeTimeoutMs, 3e4);
3267
3425
  return new Promise((resolve5, reject) => {
3268
3426
  const ws = new WS(this.opts.url, this.opts.protocols);
3269
3427
  ws.binaryType = "arraybuffer";
@@ -3409,7 +3567,9 @@ var WebSocketClientTransport = class {
3409
3567
  }
3410
3568
  };
3411
3569
  function finitePositiveLimit(value, fallback) {
3412
- 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;
3413
3573
  }
3414
3574
 
3415
3575
  // src/client/acp-session.ts
@@ -3621,7 +3781,9 @@ var ACPSession = class _ACPSession {
3621
3781
  opContext() {
3622
3782
  const self = this;
3623
3783
  return {
3624
- closed: this.closed,
3784
+ get closed() {
3785
+ return self.closed;
3786
+ },
3625
3787
  get sessionId() {
3626
3788
  return self.sessionId;
3627
3789
  },
@@ -3723,10 +3885,6 @@ var ACPSession = class _ACPSession {
3723
3885
  createPromise,
3724
3886
  new Promise((_, reject) => {
3725
3887
  rejectCreate = reject;
3726
- if (signal.aborted) {
3727
- reject(new ACPSessionError("aborted", "prompt was aborted by the parent"));
3728
- return;
3729
- }
3730
3888
  signal.addEventListener("abort", onCreateAbort, { once: true });
3731
3889
  })
3732
3890
  ]);
@@ -3744,12 +3902,6 @@ var ACPSession = class _ACPSession {
3744
3902
  }
3745
3903
  signal.removeEventListener("abort", onCreateAbort);
3746
3904
  rejectCreate = void 0;
3747
- if (typeof sessionId !== "string" || sessionId.length === 0) {
3748
- signal.removeEventListener("abort", onAbort);
3749
- this.promptCallbackAbort?.abort();
3750
- this.promptCallbackAbort = null;
3751
- throw new ACPSessionError("protocol_error", "session/new returned no session id");
3752
- }
3753
3905
  this.sessionId = sessionId;
3754
3906
  }
3755
3907
  if (signal.aborted) {
@@ -3932,103 +4084,16 @@ var ACPSession = class _ACPSession {
3932
4084
  });
3933
4085
  }
3934
4086
  responseSender() {
3935
- return {
3936
- sendResult: (id, result) => this.sendResult(id, result),
3937
- sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
3938
- };
4087
+ return responseSender.call(this.acpSessionMessagesHost());
3939
4088
  }
3940
4089
  callbackOptions() {
3941
- const promptSignal = this.promptCallbackAbort?.signal;
3942
- const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
3943
- return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
4090
+ return callbackOptions.call(this.acpSessionMessagesHost());
3944
4091
  }
3945
4092
  handleMessage(msg) {
3946
- if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
3947
- const pending = this.pending.get(msg.id);
3948
- if (!pending) return;
3949
- clearTimeout(pending.timeoutHandle);
3950
- this.pending.delete(msg.id);
3951
- if (msg.error !== void 0) {
3952
- const method = pending.method;
3953
- const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
3954
- pending.reject(
3955
- new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
3956
- );
3957
- } else {
3958
- pending.resolve(msg.result);
3959
- }
3960
- return;
3961
- }
3962
- if (msg.method === "session/update") {
3963
- handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
3964
- return;
3965
- }
3966
- if (msg.method === "session/request_permission") {
3967
- handleAcpPermissionRequest(
3968
- msg,
3969
- this.permissionPolicy,
3970
- this.responseSender(),
3971
- this.callbackOptions()
3972
- ).catch(() => {
3973
- });
3974
- return;
3975
- }
3976
- if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
3977
- handleAcpFsRequest(
3978
- msg,
3979
- this.fileServer,
3980
- this.permissionPolicy,
3981
- this.responseSender(),
3982
- this.callbackOptions()
3983
- ).catch(() => {
3984
- });
3985
- return;
3986
- }
3987
- if (msg.method?.startsWith("terminal/")) {
3988
- handleAcpTerminalRequest(
3989
- msg,
3990
- this.terminalServer,
3991
- this.permissionPolicy,
3992
- this.responseSender(),
3993
- this.callbackOptions()
3994
- ).catch(() => {
3995
- });
3996
- return;
3997
- }
3998
- if (isBestEffortAckMethod(msg.method)) {
3999
- if (msg.id !== void 0) {
4000
- this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
4001
- });
4002
- }
4003
- return;
4004
- }
4005
- if (msg.method === "$/cancel_request") {
4006
- return;
4007
- }
4008
- if (msg.method) {
4009
- if (msg.id !== void 0) {
4010
- void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
4011
- () => {
4012
- }
4013
- );
4014
- return;
4015
- }
4016
- console.warn(
4017
- JSON.stringify({
4018
- level: "warn",
4019
- event: "acp_session.unhandled_method",
4020
- method: msg.method,
4021
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4022
- })
4023
- );
4024
- }
4093
+ handleMessage.call(this.acpSessionMessagesHost(), msg);
4025
4094
  }
4026
4095
  emitProgress(event) {
4027
- if (!this.progressHandler) return;
4028
- try {
4029
- this.progressHandler(event);
4030
- } catch {
4031
- }
4096
+ emitProgress.call(this.acpSessionMessagesHost(), event);
4032
4097
  }
4033
4098
  progressHandler = null;
4034
4099
  scratch = createSessionScratch();
@@ -4058,6 +4123,23 @@ var ACPSession = class _ACPSession {
4058
4123
  opts: this.opts
4059
4124
  };
4060
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
+ }
4061
4143
  };
4062
4144
 
4063
4145
  // src/client/tool-translator.ts
@@ -4103,18 +4185,30 @@ var ToolTranslator = class {
4103
4185
  * response arrives.
4104
4186
  */
4105
4187
  async callTool(transport, name, args, callId = crypto.randomUUID()) {
4106
- await transport.send({
4107
- jsonrpc: "2.0",
4108
- method: "tools/call",
4109
- id: callId,
4110
- params: { name, arguments: args }
4111
- });
4188
+ if (this.pending.has(callId)) throw new Error(`Tool call id ${callId} is already pending`);
4112
4189
  return new Promise((resolve5, reject) => {
4113
4190
  const timeout = setTimeout(() => {
4114
4191
  this.pending.delete(callId);
4115
4192
  reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
4116
4193
  }, this.opts.totalTimeoutMs);
4117
- 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
+ }
4118
4212
  });
4119
4213
  }
4120
4214
  cancelAll() {
@@ -4248,7 +4342,8 @@ async function runAcpBench(opts) {
4248
4342
  const timeoutMs = opts.timeoutMs ?? 6e4;
4249
4343
  const checkFs = opts.checkFs ?? false;
4250
4344
  const marker = opts.marker ?? randomMarker();
4251
- 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));
4252
4347
  const seen = /* @__PURE__ */ new Set();
4253
4348
  const ids = [];
4254
4349
  for (const raw of opts.agentIds) {
@@ -5088,7 +5183,8 @@ var DEFAULT_MAX_CONCURRENCY = 4;
5088
5183
  async function mapBound(items, worker, limit) {
5089
5184
  const results = new Array(items.length);
5090
5185
  if (items.length === 0) return results;
5091
- 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));
5092
5188
  let nextIndex = 0;
5093
5189
  const workerCount = Math.min(safeLimit, items.length);
5094
5190
  const runners = [];