@wrongstack/acp 0.319.1 → 1.0.0

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.
@@ -77,6 +77,8 @@ export declare class WrongStackACPServer {
77
77
  private readonly options;
78
78
  /** HTTP server when transport mode is HTTP. */
79
79
  private httpServer;
80
+ /** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
81
+ private readonly httpSockets;
80
82
  private running;
81
83
  constructor(opts?: WrongStackACPServerOptions);
82
84
  /**
@@ -87,8 +89,22 @@ export declare class WrongStackACPServer {
87
89
  start(): Promise<void>;
88
90
  private startStdio;
89
91
  private startHttp;
90
- /** Stop the server. */
91
- stop(): void;
92
+ /**
93
+ * `listen()` reports failure through an `'error'` event, not the callback.
94
+ * Without a listener that event is an unhandled exception *and* the start
95
+ * promise never settles — a hang rather than a diagnosable failure.
96
+ *
97
+ * An ephemeral bind (`port === 0`) additionally retries the transient
98
+ * resource-exhaustion codes: under a saturated parallel test run the OS can
99
+ * momentarily have no buffer or port to hand out even though nothing is wrong.
100
+ */
101
+ private listenWithRetry;
102
+ /**
103
+ * Stop the server. Resolves once the HTTP handle is fully closed, so a
104
+ * caller (a test `afterEach`, a restart) cannot race its next bind against
105
+ * sockets this server still owns.
106
+ */
107
+ stop(): Promise<void>;
92
108
  }
93
109
  declare function headerValue(value: string | string[] | undefined): string | undefined;
94
110
  declare function requestPath(value: string | undefined): string;
package/dist/agent.js CHANGED
@@ -1623,12 +1623,16 @@ import { createServer } from "node:http";
1623
1623
  import { isIP } from "node:net";
1624
1624
  import { fileURLToPath } from "node:url";
1625
1625
  import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
1626
+ var LISTEN_RETRY_LIMIT = 5;
1627
+ var LISTEN_RETRY_BASE_MS = 25;
1626
1628
  var WrongStackACPServer = class {
1627
1629
  transport;
1628
1630
  handler;
1629
1631
  options;
1630
1632
  /** HTTP server when transport mode is HTTP. */
1631
1633
  httpServer = null;
1634
+ /** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
1635
+ httpSockets = /* @__PURE__ */ new Set();
1632
1636
  running = false;
1633
1637
  constructor(opts = {}) {
1634
1638
  this.options = opts;
@@ -1683,7 +1687,7 @@ var WrongStackACPServer = class {
1683
1687
  throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
1684
1688
  }
1685
1689
  let httpChain = Promise.resolve();
1686
- this.httpServer = createServer(async (req, res) => {
1690
+ const httpServer = createServer(async (req, res) => {
1687
1691
  if (authToken) {
1688
1692
  const url = new URL(requestPath(req.url), `http://${host}:${port}`);
1689
1693
  const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
@@ -1780,24 +1784,73 @@ var WrongStackACPServer = class {
1780
1784
  res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
1781
1785
  }
1782
1786
  });
1783
- return new Promise((resolve2) => {
1784
- this.httpServer.listen(port, host, () => {
1785
- writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
1787
+ this.httpServer = httpServer;
1788
+ const sockets = this.httpSockets;
1789
+ httpServer.on("connection", (socket) => {
1790
+ sockets.add(socket);
1791
+ socket.on("close", () => sockets.delete(socket));
1792
+ });
1793
+ await this.listenWithRetry(port, host);
1794
+ const bound = httpServer.address();
1795
+ const shown = typeof bound === "object" && bound ? bound.port : port;
1796
+ writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${shown}
1786
1797
  `);
1787
- this.running = true;
1798
+ this.running = true;
1799
+ }
1800
+ /**
1801
+ * `listen()` reports failure through an `'error'` event, not the callback.
1802
+ * Without a listener that event is an unhandled exception *and* the start
1803
+ * promise never settles — a hang rather than a diagnosable failure.
1804
+ *
1805
+ * An ephemeral bind (`port === 0`) additionally retries the transient
1806
+ * resource-exhaustion codes: under a saturated parallel test run the OS can
1807
+ * momentarily have no buffer or port to hand out even though nothing is wrong.
1808
+ */
1809
+ listenWithRetry(port, host, attempt = 0) {
1810
+ const server = this.httpServer;
1811
+ if (!server) return Promise.resolve();
1812
+ return new Promise((resolve2, reject) => {
1813
+ const onListening = () => {
1814
+ server.removeListener("error", onError);
1788
1815
  resolve2();
1789
- });
1816
+ };
1817
+ const onError = (err) => {
1818
+ server.removeListener("listening", onListening);
1819
+ const transient = err.code === "ENOBUFS" || err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL";
1820
+ if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
1821
+ const timer = setTimeout(
1822
+ () => {
1823
+ this.listenWithRetry(port, host, attempt + 1).then(resolve2, reject);
1824
+ },
1825
+ LISTEN_RETRY_BASE_MS * (attempt + 1)
1826
+ );
1827
+ timer.unref?.();
1828
+ return;
1829
+ }
1830
+ reject(err);
1831
+ };
1832
+ server.once("error", onError);
1833
+ server.once("listening", onListening);
1834
+ server.listen(port, host);
1790
1835
  });
1791
1836
  }
1792
- /** Stop the server. */
1793
- stop() {
1837
+ /**
1838
+ * Stop the server. Resolves once the HTTP handle is fully closed, so a
1839
+ * caller (a test `afterEach`, a restart) cannot race its next bind against
1840
+ * sockets this server still owns.
1841
+ */
1842
+ async stop() {
1794
1843
  this.running = false;
1795
1844
  this.handler.close();
1796
1845
  this.transport.close();
1797
- if (this.httpServer) {
1798
- this.httpServer.close();
1799
- this.httpServer = null;
1800
- }
1846
+ const server = this.httpServer;
1847
+ this.httpServer = null;
1848
+ if (!server) return;
1849
+ for (const socket of this.httpSockets) socket.destroy();
1850
+ this.httpSockets.clear();
1851
+ await new Promise((resolve2) => {
1852
+ server.close(() => resolve2());
1853
+ });
1801
1854
  }
1802
1855
  };
1803
1856
  var defaultEchoRunTurn = async (_input, _emit) => {
package/dist/client.js CHANGED
@@ -2337,6 +2337,7 @@ var ToolTranslator = class {
2337
2337
  cancelAll() {
2338
2338
  for (const [, p] of this.pending) {
2339
2339
  clearTimeout(p.timeout);
2340
+ p.reject(new Error("Call cancelled by client"));
2340
2341
  }
2341
2342
  this.pending.clear();
2342
2343
  }
package/dist/index.js CHANGED
@@ -1298,12 +1298,16 @@ import { createServer } from "node:http";
1298
1298
  import { isIP } from "node:net";
1299
1299
  import { fileURLToPath } from "node:url";
1300
1300
  import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
1301
+ var LISTEN_RETRY_LIMIT = 5;
1302
+ var LISTEN_RETRY_BASE_MS = 25;
1301
1303
  var WrongStackACPServer = class {
1302
1304
  transport;
1303
1305
  handler;
1304
1306
  options;
1305
1307
  /** HTTP server when transport mode is HTTP. */
1306
1308
  httpServer = null;
1309
+ /** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
1310
+ httpSockets = /* @__PURE__ */ new Set();
1307
1311
  running = false;
1308
1312
  constructor(opts = {}) {
1309
1313
  this.options = opts;
@@ -1358,7 +1362,7 @@ var WrongStackACPServer = class {
1358
1362
  throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
1359
1363
  }
1360
1364
  let httpChain = Promise.resolve();
1361
- this.httpServer = createServer(async (req, res) => {
1365
+ const httpServer = createServer(async (req, res) => {
1362
1366
  if (authToken) {
1363
1367
  const url = new URL(requestPath(req.url), `http://${host}:${port}`);
1364
1368
  const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
@@ -1455,24 +1459,73 @@ var WrongStackACPServer = class {
1455
1459
  res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
1456
1460
  }
1457
1461
  });
1458
- return new Promise((resolve4) => {
1459
- this.httpServer.listen(port, host, () => {
1460
- writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
1462
+ this.httpServer = httpServer;
1463
+ const sockets = this.httpSockets;
1464
+ httpServer.on("connection", (socket) => {
1465
+ sockets.add(socket);
1466
+ socket.on("close", () => sockets.delete(socket));
1467
+ });
1468
+ await this.listenWithRetry(port, host);
1469
+ const bound = httpServer.address();
1470
+ const shown = typeof bound === "object" && bound ? bound.port : port;
1471
+ writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${shown}
1461
1472
  `);
1462
- this.running = true;
1473
+ this.running = true;
1474
+ }
1475
+ /**
1476
+ * `listen()` reports failure through an `'error'` event, not the callback.
1477
+ * Without a listener that event is an unhandled exception *and* the start
1478
+ * promise never settles — a hang rather than a diagnosable failure.
1479
+ *
1480
+ * An ephemeral bind (`port === 0`) additionally retries the transient
1481
+ * resource-exhaustion codes: under a saturated parallel test run the OS can
1482
+ * momentarily have no buffer or port to hand out even though nothing is wrong.
1483
+ */
1484
+ listenWithRetry(port, host, attempt = 0) {
1485
+ const server = this.httpServer;
1486
+ if (!server) return Promise.resolve();
1487
+ return new Promise((resolve4, reject) => {
1488
+ const onListening = () => {
1489
+ server.removeListener("error", onError);
1463
1490
  resolve4();
1464
- });
1491
+ };
1492
+ const onError = (err) => {
1493
+ server.removeListener("listening", onListening);
1494
+ const transient = err.code === "ENOBUFS" || err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL";
1495
+ if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
1496
+ const timer = setTimeout(
1497
+ () => {
1498
+ this.listenWithRetry(port, host, attempt + 1).then(resolve4, reject);
1499
+ },
1500
+ LISTEN_RETRY_BASE_MS * (attempt + 1)
1501
+ );
1502
+ timer.unref?.();
1503
+ return;
1504
+ }
1505
+ reject(err);
1506
+ };
1507
+ server.once("error", onError);
1508
+ server.once("listening", onListening);
1509
+ server.listen(port, host);
1465
1510
  });
1466
1511
  }
1467
- /** Stop the server. */
1468
- stop() {
1512
+ /**
1513
+ * Stop the server. Resolves once the HTTP handle is fully closed, so a
1514
+ * caller (a test `afterEach`, a restart) cannot race its next bind against
1515
+ * sockets this server still owns.
1516
+ */
1517
+ async stop() {
1469
1518
  this.running = false;
1470
1519
  this.handler.close();
1471
1520
  this.transport.close();
1472
- if (this.httpServer) {
1473
- this.httpServer.close();
1474
- this.httpServer = null;
1475
- }
1521
+ const server = this.httpServer;
1522
+ this.httpServer = null;
1523
+ if (!server) return;
1524
+ for (const socket of this.httpSockets) socket.destroy();
1525
+ this.httpSockets.clear();
1526
+ await new Promise((resolve4) => {
1527
+ server.close(() => resolve4());
1528
+ });
1476
1529
  }
1477
1530
  };
1478
1531
  var defaultEchoRunTurn = async (_input, _emit) => {
@@ -3474,6 +3527,7 @@ var ToolTranslator = class {
3474
3527
  cancelAll() {
3475
3528
  for (const [, p] of this.pending) {
3476
3529
  clearTimeout(p.timeout);
3530
+ p.reject(new Error("Call cancelled by client"));
3477
3531
  }
3478
3532
  this.pending.clear();
3479
3533
  }
@@ -942,12 +942,16 @@ var StdioTransport = class {
942
942
  };
943
943
 
944
944
  // src/agent/wrongstack-acp-agent.ts
945
+ var LISTEN_RETRY_LIMIT = 5;
946
+ var LISTEN_RETRY_BASE_MS = 25;
945
947
  var WrongStackACPServer = class {
946
948
  transport;
947
949
  handler;
948
950
  options;
949
951
  /** HTTP server when transport mode is HTTP. */
950
952
  httpServer = null;
953
+ /** Live sockets on `httpServer`; destroyed on `stop()` so `close()` settles. */
954
+ httpSockets = /* @__PURE__ */ new Set();
951
955
  running = false;
952
956
  constructor(opts = {}) {
953
957
  this.options = opts;
@@ -1002,7 +1006,7 @@ var WrongStackACPServer = class {
1002
1006
  throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
1003
1007
  }
1004
1008
  let httpChain = Promise.resolve();
1005
- this.httpServer = createServer(async (req, res) => {
1009
+ const httpServer = createServer(async (req, res) => {
1006
1010
  if (authToken) {
1007
1011
  const url = new URL(requestPath(req.url), `http://${host}:${port}`);
1008
1012
  const bearerToken = headerValue(req.headers.authorization)?.replace(/^Bearer\s+/i, "");
@@ -1099,24 +1103,73 @@ var WrongStackACPServer = class {
1099
1103
  res.end(JSON.stringify({ error: { code: -32603, message: "Internal error" } }));
1100
1104
  }
1101
1105
  });
1102
- return new Promise((resolve2) => {
1103
- this.httpServer.listen(port, host, () => {
1104
- writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
1106
+ this.httpServer = httpServer;
1107
+ const sockets = this.httpSockets;
1108
+ httpServer.on("connection", (socket) => {
1109
+ sockets.add(socket);
1110
+ socket.on("close", () => sockets.delete(socket));
1111
+ });
1112
+ await this.listenWithRetry(port, host);
1113
+ const bound = httpServer.address();
1114
+ const shown = typeof bound === "object" && bound ? bound.port : port;
1115
+ writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${shown}
1105
1116
  `);
1106
- this.running = true;
1117
+ this.running = true;
1118
+ }
1119
+ /**
1120
+ * `listen()` reports failure through an `'error'` event, not the callback.
1121
+ * Without a listener that event is an unhandled exception *and* the start
1122
+ * promise never settles — a hang rather than a diagnosable failure.
1123
+ *
1124
+ * An ephemeral bind (`port === 0`) additionally retries the transient
1125
+ * resource-exhaustion codes: under a saturated parallel test run the OS can
1126
+ * momentarily have no buffer or port to hand out even though nothing is wrong.
1127
+ */
1128
+ listenWithRetry(port, host, attempt = 0) {
1129
+ const server = this.httpServer;
1130
+ if (!server) return Promise.resolve();
1131
+ return new Promise((resolve2, reject) => {
1132
+ const onListening = () => {
1133
+ server.removeListener("error", onError);
1107
1134
  resolve2();
1108
- });
1135
+ };
1136
+ const onError = (err) => {
1137
+ server.removeListener("listening", onListening);
1138
+ const transient = err.code === "ENOBUFS" || err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL";
1139
+ if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
1140
+ const timer = setTimeout(
1141
+ () => {
1142
+ this.listenWithRetry(port, host, attempt + 1).then(resolve2, reject);
1143
+ },
1144
+ LISTEN_RETRY_BASE_MS * (attempt + 1)
1145
+ );
1146
+ timer.unref?.();
1147
+ return;
1148
+ }
1149
+ reject(err);
1150
+ };
1151
+ server.once("error", onError);
1152
+ server.once("listening", onListening);
1153
+ server.listen(port, host);
1109
1154
  });
1110
1155
  }
1111
- /** Stop the server. */
1112
- stop() {
1156
+ /**
1157
+ * Stop the server. Resolves once the HTTP handle is fully closed, so a
1158
+ * caller (a test `afterEach`, a restart) cannot race its next bind against
1159
+ * sockets this server still owns.
1160
+ */
1161
+ async stop() {
1113
1162
  this.running = false;
1114
1163
  this.handler.close();
1115
1164
  this.transport.close();
1116
- if (this.httpServer) {
1117
- this.httpServer.close();
1118
- this.httpServer = null;
1119
- }
1165
+ const server = this.httpServer;
1166
+ this.httpServer = null;
1167
+ if (!server) return;
1168
+ for (const socket of this.httpSockets) socket.destroy();
1169
+ this.httpSockets.clear();
1170
+ await new Promise((resolve2) => {
1171
+ server.close(() => resolve2());
1172
+ });
1120
1173
  }
1121
1174
  };
1122
1175
  var defaultEchoRunTurn = async (_input, _emit) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/acp",
3
- "version": "0.319.1",
3
+ "version": "1.0.0",
4
4
  "license": "MIT",
5
5
  "description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
6
6
  "keywords": [
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@agentclientprotocol/sdk": "^1.4.0",
55
- "@wrongstack/core": "0.319.1"
55
+ "@wrongstack/core": "1.0.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^26.2.0",