@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/client.js CHANGED
@@ -318,8 +318,8 @@ function cancelLateAcpSession(transport, createPromise) {
318
318
  const timer = setTimeout(() => {
319
319
  reject(new Error("late session/cancel send timed out"));
320
320
  }, LATE_CANCEL_SEND_TIMEOUT_MS);
321
- Promise.resolve(
322
- transport.send({
321
+ Promise.resolve().then(
322
+ () => transport.send({
323
323
  jsonrpc: "2.0",
324
324
  method: "session/cancel",
325
325
  params: { sessionId: lateId }
@@ -358,11 +358,6 @@ function cancelLateAcpSession(transport, createPromise) {
358
358
  });
359
359
  }
360
360
 
361
- // src/client/acp-message-routing.ts
362
- function isBestEffortAckMethod(method) {
363
- return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
364
- }
365
-
366
361
  // src/client/acp-session-errors.ts
367
362
  var ACPSessionError = class extends Error {
368
363
  kind;
@@ -375,7 +370,7 @@ var ACPSessionError = class extends Error {
375
370
  }
376
371
  };
377
372
  function isJsonRpcError(v) {
378
- return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
373
+ return typeof v === "object" && v !== null && Number.isSafeInteger(v.code) && typeof v.message === "string";
379
374
  }
380
375
  function isAuthRequiredError(err) {
381
376
  const message = err instanceof Error ? err.message : String(err);
@@ -423,6 +418,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
423
418
  if (ctx.sessionId && ctx.sessionId !== sessionId) {
424
419
  await ctx.closeSession();
425
420
  }
421
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
426
422
  ctx.resetScratch();
427
423
  const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
428
424
  const id = ctx.allocId();
@@ -434,6 +430,7 @@ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
434
430
  if (isJsonRpcError(result)) {
435
431
  throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
436
432
  }
433
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
437
434
  ctx.setSessionId(sessionId);
438
435
  }
439
436
  async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
@@ -447,6 +444,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
447
444
  if (ctx.sessionId && ctx.sessionId !== sessionId) {
448
445
  await ctx.closeSession();
449
446
  }
447
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
450
448
  const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
451
449
  const id = ctx.allocId();
452
450
  const result = await ctx.sendRequest(id, "session/resume", {
@@ -457,6 +455,7 @@ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
457
455
  if (isJsonRpcError(result)) {
458
456
  throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
459
457
  }
458
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
460
459
  ctx.setSessionId(sessionId);
461
460
  }
462
461
  async function executeListSessions(ctx, cursor, cwd) {
@@ -605,10 +604,11 @@ async function executeCreateSession(ctx) {
605
604
  }
606
605
 
607
606
  // src/client/acp-session-auth.ts
607
+ function ensureOpen(host) {
608
+ if (host.state === "closed") throw new ACPSessionError("closed", "session is closed");
609
+ }
608
610
  async function authenticate(host, methodId) {
609
- if (host.state === "closed") {
610
- throw new ACPSessionError("closed", "session is closed");
611
- }
611
+ ensureOpen(host);
612
612
  if (host.state !== "ready" && host.state !== "authenticated") {
613
613
  throw new ACPSessionError(
614
614
  "protocol_error",
@@ -633,12 +633,11 @@ async function authenticate(host, methodId) {
633
633
  if (isJsonRpcError(result)) {
634
634
  throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
635
635
  }
636
+ ensureOpen(host);
636
637
  host.state = "authenticated";
637
638
  }
638
639
  async function logout(host) {
639
- if (host.state === "closed") {
640
- throw new ACPSessionError("closed", "session is closed");
641
- }
640
+ ensureOpen(host);
642
641
  if (!host.agentCapabilities.auth?.logout) {
643
642
  throw new ACPSessionError(
644
643
  "unsupported_capability",
@@ -650,6 +649,7 @@ async function logout(host) {
650
649
  if (isJsonRpcError(result)) {
651
650
  throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
652
651
  }
652
+ ensureOpen(host);
653
653
  host.state = "ready";
654
654
  }
655
655
  async function createSessionWithAuth(host) {
@@ -691,6 +691,44 @@ async function ensureAuthenticated(host) {
691
691
  );
692
692
  }
693
693
 
694
+ // src/client/acp-session-content.ts
695
+ function textContent(text) {
696
+ return { type: "text", text };
697
+ }
698
+ function imageContent(mimeType, data) {
699
+ return { type: "image", mimeType, data };
700
+ }
701
+ function audioContent(mimeType, data) {
702
+ return { type: "audio", mimeType, data };
703
+ }
704
+ function extractText(block) {
705
+ if (typeof block !== "object" || block === null) return "";
706
+ const b = block;
707
+ if (b.type === "text" && typeof b.text === "string") return b.text;
708
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
709
+ return b.resource.text;
710
+ }
711
+ return "";
712
+ }
713
+ function isRecord(v) {
714
+ return typeof v === "object" && v !== null && !Array.isArray(v);
715
+ }
716
+ function emptyRunResult(stopReason) {
717
+ return {
718
+ text: "",
719
+ stopReason,
720
+ hasText: false,
721
+ toolCalls: [],
722
+ diffs: [],
723
+ thoughts: ""
724
+ };
725
+ }
726
+
727
+ // src/client/acp-message-routing.ts
728
+ function isBestEffortAckMethod(method) {
729
+ return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
730
+ }
731
+
694
732
  // src/client/file-server.ts
695
733
  import { randomBytes } from "node:crypto";
696
734
  import { realpathSync } from "node:fs";
@@ -747,6 +785,9 @@ var FileServer = class {
747
785
  const stat2 = await this.operations.stat(safe).catch((err) => {
748
786
  throw mapFsError(err, safe);
749
787
  });
788
+ if (controller.signal.aborted) {
789
+ throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
790
+ }
750
791
  if (stat2.size > this.maxReadBytes) {
751
792
  throw new FsError(
752
793
  "TOO_LARGE",
@@ -758,6 +799,17 @@ var FileServer = class {
758
799
  encoding: "utf8",
759
800
  signal: controller.signal
760
801
  });
802
+ if (controller.signal.aborted) {
803
+ throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
804
+ }
805
+ const actualSize = Buffer.byteLength(content, "utf8");
806
+ if (actualSize > this.maxReadBytes) {
807
+ throw new FsError(
808
+ "TOO_LARGE",
809
+ safe,
810
+ `file is ${actualSize} bytes after read, max read is ${this.maxReadBytes} bytes`
811
+ );
812
+ }
761
813
  if (params.line == null && params.limit == null) return { content };
762
814
  const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
763
815
  const start = (params.line ?? 1) - 1;
@@ -796,9 +848,21 @@ var FileServer = class {
796
848
  encoding: "utf8",
797
849
  signal: controller.signal
798
850
  });
851
+ if (controller.signal.aborted) {
852
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
853
+ }
799
854
  await this.assertRealInside(tmp);
855
+ if (controller.signal.aborted) {
856
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
857
+ }
800
858
  await this.assertRealInside(path.dirname(safe));
859
+ if (controller.signal.aborted) {
860
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
861
+ }
801
862
  await this.operations.rename(tmp, safe);
863
+ if (controller.signal.aborted) {
864
+ throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
865
+ }
802
866
  } catch (err) {
803
867
  if (err instanceof FsError) {
804
868
  await this.operations.unlink(tmp).catch(() => void 0);
@@ -897,7 +961,7 @@ function safeRealpathSync(p) {
897
961
 
898
962
  // src/client/acp-session-callbacks.ts
899
963
  var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
900
- async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
964
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions2 = {}) {
901
965
  const id = msg.id;
902
966
  if (id === void 0) return;
903
967
  const params = msg.params;
@@ -914,7 +978,7 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
914
978
  toolCall,
915
979
  options: permissionOptions
916
980
  },
917
- callbackOptions
981
+ callbackOptions2
918
982
  );
919
983
  await sender.sendResult(id, { outcome });
920
984
  } catch (err) {
@@ -926,7 +990,7 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
926
990
  await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
927
991
  }
928
992
  }
929
- async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
993
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions2 = {}) {
930
994
  const id = msg.id;
931
995
  if (id === void 0) return;
932
996
  const params = msg.params;
@@ -943,7 +1007,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
943
1007
  kind: "edit",
944
1008
  rawInput: { path: params.path, sessionId: params.sessionId }
945
1009
  },
946
- callbackOptions
1010
+ callbackOptions2
947
1011
  );
948
1012
  if (authorization !== "allowed") {
949
1013
  const isCancelled = authorization === "cancelled";
@@ -978,7 +1042,7 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
978
1042
  await sender.sendErrorResponse(id, code, message);
979
1043
  }
980
1044
  }
981
- async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
1045
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions2 = {}) {
982
1046
  const id = msg.id;
983
1047
  if (id === void 0) return;
984
1048
  const params = msg.params ?? {};
@@ -998,7 +1062,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
998
1062
  sessionId: params.sessionId
999
1063
  }
1000
1064
  },
1001
- callbackOptions
1065
+ callbackOptions2
1002
1066
  );
1003
1067
  if (authorization !== "allowed") {
1004
1068
  const isCancelled = authorization === "cancelled";
@@ -1059,7 +1123,7 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
1059
1123
  await sender.sendErrorResponse(id, -32603, message);
1060
1124
  }
1061
1125
  }
1062
- async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
1126
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions2) {
1063
1127
  try {
1064
1128
  const outcome = await runPermissionWithDeadline(
1065
1129
  permissionPolicy,
@@ -1077,25 +1141,25 @@ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions)
1077
1141
  { optionId: "reject", name: "Reject", kind: "reject_once" }
1078
1142
  ]
1079
1143
  },
1080
- callbackOptions
1144
+ callbackOptions2
1081
1145
  );
1082
1146
  return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
1083
1147
  } catch (err) {
1084
1148
  return isAbortLikeError(err) ? "cancelled" : "denied";
1085
1149
  }
1086
1150
  }
1087
- async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
1088
- const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
1151
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions2) {
1152
+ const timeoutMs = resolvePermissionDeadline(callbackOptions2.permissionTimeoutMs);
1089
1153
  const controller = new AbortController();
1090
1154
  const abort = () => controller.abort();
1091
1155
  const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
1092
1156
  let removeAbort;
1093
- if (callbackOptions.signal) {
1094
- if (callbackOptions.signal.aborted) {
1157
+ if (callbackOptions2.signal) {
1158
+ if (callbackOptions2.signal.aborted) {
1095
1159
  abort();
1096
1160
  } else {
1097
- callbackOptions.signal.addEventListener("abort", abort, { once: true });
1098
- removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
1161
+ callbackOptions2.signal.addEventListener("abort", abort, { once: true });
1162
+ removeAbort = () => callbackOptions2.signal?.removeEventListener("abort", abort);
1099
1163
  }
1100
1164
  }
1101
1165
  try {
@@ -1131,54 +1195,21 @@ function resolvePermissionDeadline(value) {
1131
1195
  return DEFAULT_PERMISSION_TIMEOUT_MS;
1132
1196
  }
1133
1197
 
1134
- // src/client/acp-session-content.ts
1135
- function textContent(text) {
1136
- return { type: "text", text };
1137
- }
1138
- function imageContent(mimeType, data) {
1139
- return { type: "image", mimeType, data };
1140
- }
1141
- function audioContent(mimeType, data) {
1142
- return { type: "audio", mimeType, data };
1143
- }
1144
- function extractText(block) {
1145
- if (typeof block !== "object" || block === null) return "";
1146
- const b = block;
1147
- if (b.type === "text" && typeof b.text === "string") return b.text;
1148
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1149
- return b.resource.text;
1150
- }
1151
- return "";
1152
- }
1153
- function isRecord(v) {
1154
- return typeof v === "object" && v !== null && !Array.isArray(v);
1155
- }
1156
- function emptyRunResult(stopReason) {
1157
- return {
1158
- text: "",
1159
- stopReason,
1160
- hasText: false,
1161
- toolCalls: [],
1162
- diffs: [],
1163
- thoughts: ""
1164
- };
1165
- }
1166
-
1167
1198
  // src/client/acp-session-updates.ts
1168
1199
  function createSessionScratch() {
1169
1200
  return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1170
1201
  }
1171
- function handleAcpSessionUpdate(msg, scratch, emitProgress) {
1202
+ function handleAcpSessionUpdate(msg, scratch, emitProgress2) {
1172
1203
  const update = msg.params?.update;
1173
1204
  if (typeof update !== "object" || update === null) return;
1174
1205
  const u = update;
1175
- emitProgress({ type: "raw", update: u });
1206
+ emitProgress2({ type: "raw", update: u });
1176
1207
  switch (u.sessionUpdate) {
1177
1208
  case "agent_message_chunk": {
1178
1209
  const text = extractText(u.content);
1179
1210
  if (text) {
1180
1211
  scratch.text += text;
1181
- emitProgress({ type: "message", text });
1212
+ emitProgress2({ type: "message", text });
1182
1213
  }
1183
1214
  return;
1184
1215
  }
@@ -1187,29 +1218,29 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
1187
1218
  const text = extractText(u.content);
1188
1219
  if (text) {
1189
1220
  scratch.thoughts += text;
1190
- emitProgress({ type: "thought", text });
1221
+ emitProgress2({ type: "thought", text });
1191
1222
  }
1192
1223
  return;
1193
1224
  }
1194
1225
  case "tool_call":
1195
1226
  case "tool_call_update":
1196
- captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
1227
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress2);
1197
1228
  return;
1198
1229
  case "plan":
1199
1230
  if (Array.isArray(u.entries)) {
1200
1231
  scratch.plan = u.entries;
1201
- emitProgress({ type: "plan", entries: u.entries });
1232
+ emitProgress2({ type: "plan", entries: u.entries });
1202
1233
  }
1203
1234
  return;
1204
1235
  case "usage_update":
1205
- if (typeof u.used === "number" && typeof u.size === "number") {
1236
+ if (typeof u.used === "number" && Number.isFinite(u.used) && u.used >= 0 && typeof u.size === "number" && Number.isFinite(u.size) && u.size >= 0) {
1206
1237
  const usage = {
1207
1238
  used: u.used,
1208
1239
  size: u.size,
1209
1240
  ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
1210
1241
  };
1211
1242
  scratch.usage = usage;
1212
- emitProgress({ type: "usage", usage });
1243
+ emitProgress2({ type: "usage", usage });
1213
1244
  }
1214
1245
  return;
1215
1246
  case "available_commands_update":
@@ -1224,7 +1255,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
1224
1255
  return;
1225
1256
  }
1226
1257
  }
1227
- function captureToolCall(u, isNew, scratch, emitProgress) {
1258
+ function captureToolCall(u, isNew, scratch, emitProgress2) {
1228
1259
  const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
1229
1260
  if (!toolCallId) return;
1230
1261
  const prev = scratch.toolCalls.get(toolCallId);
@@ -1246,16 +1277,119 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
1246
1277
  newText: c.newText
1247
1278
  };
1248
1279
  scratch.diffs.push(diff);
1249
- emitProgress({ type: "diff", diff });
1280
+ emitProgress2({ type: "diff", diff });
1250
1281
  }
1251
1282
  }
1252
1283
  }
1253
- emitProgress({
1284
+ emitProgress2({
1254
1285
  type: isNew ? "tool_call" : "tool_call_update",
1255
1286
  toolCall: record
1256
1287
  });
1257
1288
  }
1258
1289
 
1290
+ // src/client/acp-session-messages.ts
1291
+ function responseSender() {
1292
+ return {
1293
+ sendResult: (id, result) => this.sendResult(id, result),
1294
+ sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
1295
+ };
1296
+ }
1297
+ function callbackOptions() {
1298
+ const promptSignal = this.promptCallbackAbort?.signal;
1299
+ const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
1300
+ return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
1301
+ }
1302
+ function handleMessage(msg) {
1303
+ if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
1304
+ const pending = this.pending.get(msg.id);
1305
+ if (!pending) return;
1306
+ clearTimeout(pending.timeoutHandle);
1307
+ this.pending.delete(msg.id);
1308
+ if (msg.error !== void 0) {
1309
+ const method = pending.method;
1310
+ const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
1311
+ pending.reject(
1312
+ new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
1313
+ );
1314
+ } else {
1315
+ pending.resolve(msg.result);
1316
+ }
1317
+ return;
1318
+ }
1319
+ if (msg.method === "session/update") {
1320
+ const updateSessionId = msg.params?.sessionId;
1321
+ if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
1322
+ handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
1323
+ return;
1324
+ }
1325
+ if (msg.method === "session/request_permission") {
1326
+ handleAcpPermissionRequest(
1327
+ msg,
1328
+ this.permissionPolicy,
1329
+ this.responseSender(),
1330
+ this.callbackOptions()
1331
+ ).catch(() => {
1332
+ });
1333
+ return;
1334
+ }
1335
+ if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
1336
+ handleAcpFsRequest(
1337
+ msg,
1338
+ this.fileServer,
1339
+ this.permissionPolicy,
1340
+ this.responseSender(),
1341
+ this.callbackOptions()
1342
+ ).catch(() => {
1343
+ });
1344
+ return;
1345
+ }
1346
+ if (msg.method?.startsWith("terminal/")) {
1347
+ handleAcpTerminalRequest(
1348
+ msg,
1349
+ this.terminalServer,
1350
+ this.permissionPolicy,
1351
+ this.responseSender(),
1352
+ this.callbackOptions()
1353
+ ).catch(() => {
1354
+ });
1355
+ return;
1356
+ }
1357
+ if (isBestEffortAckMethod(msg.method)) {
1358
+ if (msg.id !== void 0) {
1359
+ this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
1360
+ });
1361
+ }
1362
+ return;
1363
+ }
1364
+ if (msg.method === "$/cancel_request") {
1365
+ return;
1366
+ }
1367
+ if (msg.method) {
1368
+ if (msg.id !== void 0) {
1369
+ void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
1370
+ () => {
1371
+ }
1372
+ );
1373
+ return;
1374
+ }
1375
+ console.warn(
1376
+ JSON.stringify({
1377
+ level: "warn",
1378
+ event: "acp_session.unhandled_method",
1379
+ method: msg.method,
1380
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1381
+ })
1382
+ );
1383
+ }
1384
+ }
1385
+ function emitProgress(event) {
1386
+ if (!this.progressHandler) return;
1387
+ try {
1388
+ this.progressHandler(event);
1389
+ } catch {
1390
+ }
1391
+ }
1392
+
1259
1393
  // src/client/permission.ts
1260
1394
  function pickAllow(options) {
1261
1395
  const ranked = [...options].sort((a, b) => {
@@ -1294,6 +1428,7 @@ function makePermissionPolicy(decide) {
1294
1428
  return async (req) => {
1295
1429
  if (req.signal.aborted) return { outcome: "cancelled" };
1296
1430
  const allow = await decide(req);
1431
+ if (req.signal.aborted) return { outcome: "cancelled" };
1297
1432
  return allow ? pickAllow(req.options) : pickReject(req.options);
1298
1433
  };
1299
1434
  }
@@ -1346,8 +1481,8 @@ var TerminalServer = class {
1346
1481
  constructor(opts) {
1347
1482
  this.projectRoot = path2.resolve(opts.projectRoot);
1348
1483
  this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
1349
- this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
1350
- this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
1484
+ this.outputByteLimit = this.clampFiniteInt(opts.outputByteLimit, 1024 * 1024);
1485
+ this.maxOutputByteLimit = this.clampFiniteInt(opts.maxOutputByteLimit, 16 * 1024 * 1024);
1351
1486
  this.instanceId = `term_srv_${randomBytes2(4).toString("hex")}`;
1352
1487
  this.maxTerminals = this.clampFiniteInt(opts.maxTerminals, 32);
1353
1488
  this.abortSignal = opts.signal;
@@ -1408,7 +1543,7 @@ var TerminalServer = class {
1408
1543
  clearTimeout(state.timeoutHandle);
1409
1544
  state.timeoutHandle = null;
1410
1545
  }
1411
- const exitStatus = {
1546
+ const exitStatus = state.exitStatus ?? {
1412
1547
  exitCode: typeof code === "number" ? code : null,
1413
1548
  signal: typeof signalName === "string" ? signalName : null
1414
1549
  };
@@ -1608,7 +1743,7 @@ var TerminalServer = class {
1608
1743
  * unbounded memory growth.
1609
1744
  */
1610
1745
  clampFiniteInt(value, defaultValue) {
1611
- if (value === void 0 || !Number.isFinite(value) || value < 1) {
1746
+ if (value === void 0 || !Number.isFinite(value) || value < 1 || !Number.isSafeInteger(Math.trunc(value))) {
1612
1747
  return defaultValue;
1613
1748
  }
1614
1749
  return Math.trunc(value);
@@ -1739,7 +1874,7 @@ var WebSocketClientTransport = class {
1739
1874
  )
1740
1875
  );
1741
1876
  }
1742
- const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
1877
+ const timeoutMs = finitePositiveLimit(this.opts.handshakeTimeoutMs, 3e4);
1743
1878
  return new Promise((resolve3, reject) => {
1744
1879
  const ws = new WS(this.opts.url, this.opts.protocols);
1745
1880
  ws.binaryType = "arraybuffer";
@@ -1885,7 +2020,9 @@ var WebSocketClientTransport = class {
1885
2020
  }
1886
2021
  };
1887
2022
  function finitePositiveLimit(value, fallback) {
1888
- return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
2023
+ if (value === void 0 || !Number.isFinite(value)) return fallback;
2024
+ const normalized = Math.floor(value);
2025
+ return Number.isSafeInteger(normalized) && normalized >= 1 ? normalized : fallback;
1889
2026
  }
1890
2027
 
1891
2028
  // src/client/acp-session.ts
@@ -2097,7 +2234,9 @@ var ACPSession = class _ACPSession {
2097
2234
  opContext() {
2098
2235
  const self = this;
2099
2236
  return {
2100
- closed: this.closed,
2237
+ get closed() {
2238
+ return self.closed;
2239
+ },
2101
2240
  get sessionId() {
2102
2241
  return self.sessionId;
2103
2242
  },
@@ -2398,105 +2537,16 @@ var ACPSession = class _ACPSession {
2398
2537
  });
2399
2538
  }
2400
2539
  responseSender() {
2401
- return {
2402
- sendResult: (id, result) => this.sendResult(id, result),
2403
- sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
2404
- };
2540
+ return responseSender.call(this.acpSessionMessagesHost());
2405
2541
  }
2406
2542
  callbackOptions() {
2407
- const promptSignal = this.promptCallbackAbort?.signal;
2408
- const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
2409
- return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
2543
+ return callbackOptions.call(this.acpSessionMessagesHost());
2410
2544
  }
2411
2545
  handleMessage(msg) {
2412
- if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
2413
- const pending = this.pending.get(msg.id);
2414
- if (!pending) return;
2415
- clearTimeout(pending.timeoutHandle);
2416
- this.pending.delete(msg.id);
2417
- if (msg.error !== void 0) {
2418
- const method = pending.method;
2419
- const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
2420
- pending.reject(
2421
- new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
2422
- );
2423
- } else {
2424
- pending.resolve(msg.result);
2425
- }
2426
- return;
2427
- }
2428
- if (msg.method === "session/update") {
2429
- const updateSessionId = msg.params?.sessionId;
2430
- if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
2431
- handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
2432
- return;
2433
- }
2434
- if (msg.method === "session/request_permission") {
2435
- handleAcpPermissionRequest(
2436
- msg,
2437
- this.permissionPolicy,
2438
- this.responseSender(),
2439
- this.callbackOptions()
2440
- ).catch(() => {
2441
- });
2442
- return;
2443
- }
2444
- if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
2445
- handleAcpFsRequest(
2446
- msg,
2447
- this.fileServer,
2448
- this.permissionPolicy,
2449
- this.responseSender(),
2450
- this.callbackOptions()
2451
- ).catch(() => {
2452
- });
2453
- return;
2454
- }
2455
- if (msg.method?.startsWith("terminal/")) {
2456
- handleAcpTerminalRequest(
2457
- msg,
2458
- this.terminalServer,
2459
- this.permissionPolicy,
2460
- this.responseSender(),
2461
- this.callbackOptions()
2462
- ).catch(() => {
2463
- });
2464
- return;
2465
- }
2466
- if (isBestEffortAckMethod(msg.method)) {
2467
- if (msg.id !== void 0) {
2468
- this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
2469
- });
2470
- }
2471
- return;
2472
- }
2473
- if (msg.method === "$/cancel_request") {
2474
- return;
2475
- }
2476
- if (msg.method) {
2477
- if (msg.id !== void 0) {
2478
- void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
2479
- () => {
2480
- }
2481
- );
2482
- return;
2483
- }
2484
- console.warn(
2485
- JSON.stringify({
2486
- level: "warn",
2487
- event: "acp_session.unhandled_method",
2488
- method: msg.method,
2489
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2490
- })
2491
- );
2492
- }
2546
+ handleMessage.call(this.acpSessionMessagesHost(), msg);
2493
2547
  }
2494
2548
  emitProgress(event) {
2495
- if (!this.progressHandler) return;
2496
- try {
2497
- this.progressHandler(event);
2498
- } catch {
2499
- }
2549
+ emitProgress.call(this.acpSessionMessagesHost(), event);
2500
2550
  }
2501
2551
  progressHandler = null;
2502
2552
  scratch = createSessionScratch();
@@ -2526,6 +2576,23 @@ var ACPSession = class _ACPSession {
2526
2576
  opts: this.opts
2527
2577
  };
2528
2578
  }
2579
+ acpSessionMessagesHost() {
2580
+ void this.sendResult;
2581
+ void this.sendErrorResponse;
2582
+ void this.promptCallbackAbort;
2583
+ void this.callbackAbort;
2584
+ void this.pending;
2585
+ void this.sessionId;
2586
+ void this.scratch;
2587
+ void this.emitProgress;
2588
+ void this.permissionPolicy;
2589
+ void this.responseSender;
2590
+ void this.callbackOptions;
2591
+ void this.fileServer;
2592
+ void this.terminalServer;
2593
+ void this.progressHandler;
2594
+ return this;
2595
+ }
2529
2596
  };
2530
2597
 
2531
2598
  // src/integration/acp-subagent-runner.ts
@@ -2743,18 +2810,30 @@ var ToolTranslator = class {
2743
2810
  * response arrives.
2744
2811
  */
2745
2812
  async callTool(transport, name, args, callId = crypto.randomUUID()) {
2746
- await transport.send({
2747
- jsonrpc: "2.0",
2748
- method: "tools/call",
2749
- id: callId,
2750
- params: { name, arguments: args }
2751
- });
2813
+ if (this.pending.has(callId)) throw new Error(`Tool call id ${callId} is already pending`);
2752
2814
  return new Promise((resolve3, reject) => {
2753
2815
  const timeout = setTimeout(() => {
2754
2816
  this.pending.delete(callId);
2755
2817
  reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
2756
2818
  }, this.opts.totalTimeoutMs);
2757
- this.pending.set(callId, { resolve: resolve3, reject, timeout });
2819
+ const pending = { resolve: resolve3, reject, timeout };
2820
+ this.pending.set(callId, pending);
2821
+ const failSend = (err) => {
2822
+ if (this.pending.get(callId) !== pending) return;
2823
+ clearTimeout(timeout);
2824
+ this.pending.delete(callId);
2825
+ reject(err instanceof Error ? err : new Error(String(err)));
2826
+ };
2827
+ try {
2828
+ transport.send({
2829
+ jsonrpc: "2.0",
2830
+ method: "tools/call",
2831
+ id: callId,
2832
+ params: { name, arguments: args }
2833
+ }).catch(failSend);
2834
+ } catch (err) {
2835
+ failSend(err);
2836
+ }
2758
2837
  });
2759
2838
  }
2760
2839
  cancelAll() {