@wrongstack/acp 0.297.0 → 0.298.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.
Files changed (55) hide show
  1. package/dist/agent/protocol-handler.d.ts +39 -46
  2. package/dist/agent/session-store.d.ts +55 -0
  3. package/dist/agent/stdio-transport.d.ts +74 -1
  4. package/dist/agent.js +472 -210
  5. package/dist/client/terminal-server.d.ts +5 -1
  6. package/dist/client.js +70 -44
  7. package/dist/index.js +433 -253
  8. package/dist/win32-cmd.d.ts +9 -6
  9. package/dist/wrongstack-acp-agent.js +312 -157
  10. package/package.json +4 -3
  11. package/dist/agent/index.d.ts.map +0 -1
  12. package/dist/agent/protocol-contract.d.ts.map +0 -1
  13. package/dist/agent/protocol-handler.d.ts.map +0 -1
  14. package/dist/agent/server-agent-turn.d.ts.map +0 -1
  15. package/dist/agent/session-store.d.ts.map +0 -1
  16. package/dist/agent/stdio-transport.d.ts.map +0 -1
  17. package/dist/agent/tools-registry.d.ts.map +0 -1
  18. package/dist/agent/wrongstack-acp-agent.d.ts.map +0 -1
  19. package/dist/agent/ws-bridge-transport.d.ts.map +0 -1
  20. package/dist/agent.js.map +0 -7
  21. package/dist/client/acp-message-routing.d.ts.map +0 -1
  22. package/dist/client/acp-session-callbacks.d.ts.map +0 -1
  23. package/dist/client/acp-session-content.d.ts.map +0 -1
  24. package/dist/client/acp-session-errors.d.ts.map +0 -1
  25. package/dist/client/acp-session-types.d.ts.map +0 -1
  26. package/dist/client/acp-session-updates.d.ts.map +0 -1
  27. package/dist/client/acp-session.d.ts.map +0 -1
  28. package/dist/client/file-server.d.ts.map +0 -1
  29. package/dist/client/index.d.ts.map +0 -1
  30. package/dist/client/permission.d.ts.map +0 -1
  31. package/dist/client/terminal-server.d.ts.map +0 -1
  32. package/dist/client/tool-translator.d.ts.map +0 -1
  33. package/dist/client/trust-boundary-permission.d.ts.map +0 -1
  34. package/dist/client/websocket-transport.d.ts.map +0 -1
  35. package/dist/client.js.map +0 -7
  36. package/dist/index.d.ts.map +0 -1
  37. package/dist/index.js.map +0 -7
  38. package/dist/integration/acp-bench.d.ts.map +0 -1
  39. package/dist/integration/acp-subagent-runner.d.ts.map +0 -1
  40. package/dist/integration/ensemble-runner.d.ts.map +0 -1
  41. package/dist/integration/run-one-acp-task.d.ts.map +0 -1
  42. package/dist/legacy.d.ts.map +0 -1
  43. package/dist/legacy.js.map +0 -7
  44. package/dist/registry/acp-registry-fetch.d.ts.map +0 -1
  45. package/dist/registry/agents.catalog.d.ts.map +0 -1
  46. package/dist/registry/ensemble-registry.d.ts.map +0 -1
  47. package/dist/sdk.d.ts.map +0 -1
  48. package/dist/sdk.js.map +0 -7
  49. package/dist/types/acp-messages.d.ts.map +0 -1
  50. package/dist/types/acp-v1.d.ts.map +0 -1
  51. package/dist/v1.d.ts.map +0 -1
  52. package/dist/v1.js.map +0 -7
  53. package/dist/version.d.ts.map +0 -1
  54. package/dist/win32-cmd.d.ts.map +0 -1
  55. package/dist/wrongstack-acp-agent.js.map +0 -7
@@ -55,7 +55,11 @@ export declare class TerminalServer {
55
55
  exitCode: number | null;
56
56
  signal: string | null;
57
57
  }>;
58
- /** Kill the process but keep the terminal record (agent can still read output). */
58
+ /**
59
+ * Kill the process but keep the terminal record (agent can still read output).
60
+ * On POSIX this signals only the direct child; descendants may survive because
61
+ * terminal processes are not spawned as process-group leaders.
62
+ */
59
63
  kill(terminalId: string): void;
60
64
  /** Kill the process if alive and remove the record. */
61
65
  release(terminalId: string): void;
package/dist/client.js CHANGED
@@ -3,32 +3,14 @@ import { expectDefined, writeErr } from "@wrongstack/core/utils";
3
3
  import { treeKill } from "@wrongstack/core/utils/tree-kill";
4
4
 
5
5
  // src/win32-cmd.ts
6
- var WIN32_CMD_META = /[&|<>"\r\n\0]/;
7
- function buildWin32CmdShimInvocation(command, args = []) {
8
- assertSafeWin32CmdArgs([command, ...args]);
9
- const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
10
- return {
11
- command: process.env["COMSPEC"] ?? "cmd.exe",
12
- args: ["/d", "/c", line],
13
- windowsVerbatimArguments: true
14
- };
15
- }
16
- function assertSafeWin32CmdArgs(args) {
17
- for (const arg of args) {
18
- if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
19
- throw new Error(
20
- 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
21
- );
22
- }
23
- }
24
- }
25
- function quoteWin32CmdArg(arg) {
26
- return `"${arg}"`;
27
- }
6
+ import {
7
+ buildWin32CmdShimInvocation
8
+ } from "@wrongstack/core/utils";
28
9
 
29
10
  // src/agent/stdio-transport.ts
30
11
  var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
31
12
  var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
13
+ var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
32
14
  function positiveLimit(value, fallback) {
33
15
  return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
34
16
  }
@@ -36,12 +18,15 @@ var ClientTransport = class {
36
18
  child = null;
37
19
  buffer = "";
38
20
  handlers = /* @__PURE__ */ new Set();
21
+ claimHandlers = /* @__PURE__ */ new Set();
39
22
  closed = false;
40
23
  resolveRead = null;
41
24
  messageQueue = [];
25
+ queuedChars = 0;
42
26
  opts;
43
27
  maxFrameChars;
44
28
  maxQueuedMessages;
29
+ maxQueuedChars;
45
30
  constructor(options) {
46
31
  this.opts = {
47
32
  handshakeTimeoutMs: 3e4,
@@ -49,6 +34,7 @@ var ClientTransport = class {
49
34
  };
50
35
  this.maxFrameChars = positiveLimit(options.maxFrameChars, DEFAULT_MAX_FRAME_CHARS);
51
36
  this.maxQueuedMessages = positiveLimit(options.maxQueuedMessages, DEFAULT_MAX_QUEUED_MESSAGES);
37
+ this.maxQueuedChars = positiveLimit(options.maxQueuedChars, DEFAULT_MAX_QUEUED_CHARS);
52
38
  }
53
39
  async start() {
54
40
  if (this.child) return;
@@ -149,8 +135,11 @@ var ClientTransport = class {
149
135
  });
150
136
  }
151
137
  read() {
152
- if (this.messageQueue.length > 0)
153
- return Promise.resolve(expectDefined(this.messageQueue.shift()));
138
+ if (this.messageQueue.length > 0) {
139
+ const queued = expectDefined(this.messageQueue.shift());
140
+ this.queuedChars = Math.max(0, this.queuedChars - queued.chars);
141
+ return Promise.resolve(queued.message);
142
+ }
154
143
  if (this.closed) return Promise.resolve(null);
155
144
  return new Promise((resolve3) => {
156
145
  this.resolveRead = resolve3;
@@ -160,13 +149,29 @@ var ClientTransport = class {
160
149
  this.handlers.add(handler);
161
150
  return () => this.handlers.delete(handler);
162
151
  }
152
+ /**
153
+ * Register a handler that MAY claim a message by returning `true`.
154
+ * Claimed messages are NOT enqueued for the read() loop — they are
155
+ * considered fully consumed by the handler. This is how the ACP
156
+ * server transport prevents the correlation handler (which always
157
+ * fires on every message but only actually correlates responses)
158
+ * from starving the read() loop of pipelined requests, notifications,
159
+ * and any message the handler chose to ignore. See dispatch() for
160
+ * the gating logic.
161
+ */
162
+ onMessageClaim(handler) {
163
+ this.claimHandlers.add(handler);
164
+ return () => this.claimHandlers.delete(handler);
165
+ }
163
166
  stop() {
164
167
  this.closed = true;
165
168
  this.resolveRead?.(null);
166
169
  this.resolveRead = null;
167
170
  this.buffer = "";
168
171
  this.messageQueue.length = 0;
172
+ this.queuedChars = 0;
169
173
  this.handlers.clear();
174
+ this.claimHandlers.clear();
170
175
  const child = this.child;
171
176
  if (!child) return;
172
177
  treeKill(child);
@@ -191,7 +196,7 @@ var ClientTransport = class {
191
196
  return;
192
197
  }
193
198
  try {
194
- this.dispatch(JSON.parse(raw));
199
+ this.dispatch(JSON.parse(raw), raw.length);
195
200
  } catch {
196
201
  }
197
202
  }
@@ -201,29 +206,34 @@ var ClientTransport = class {
201
206
  }
202
207
  onChildClose(code) {
203
208
  this.closed = true;
209
+ this.child = null;
204
210
  this.resolveRead?.(null);
205
211
  this.resolveRead = null;
206
212
  this.buffer = "";
207
213
  this.messageQueue.length = 0;
214
+ this.queuedChars = 0;
208
215
  this.handlers.clear();
209
216
  if (code !== 0 && code !== null) {
210
217
  writeErr(`[acp-child exited with code ${code}]
211
218
  `);
212
219
  }
213
220
  }
214
- dispatch(msg) {
221
+ dispatch(msg, chars = JSON.stringify(msg).length) {
215
222
  if (this.resolveRead) {
216
223
  const resolve3 = this.resolveRead;
217
224
  this.resolveRead = null;
218
225
  resolve3(msg);
219
226
  } else if (this.handlers.size === 0) {
220
- if (this.messageQueue.length >= this.maxQueuedMessages) {
221
- writeErr(`[acp-child message queue exceeds ${this.maxQueuedMessages} entries]
222
- `);
227
+ if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
228
+ writeErr(
229
+ `[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
230
+ `
231
+ );
223
232
  this.stop();
224
233
  return;
225
234
  }
226
- this.messageQueue.push(msg);
235
+ this.messageQueue.push({ message: msg, chars });
236
+ this.queuedChars += chars;
227
237
  }
228
238
  for (const handler of this.handlers) {
229
239
  try {
@@ -508,6 +518,7 @@ import { spawn } from "node:child_process";
508
518
  import { realpathSync as realpathSync2 } from "node:fs";
509
519
  import * as path2 from "node:path";
510
520
  import { buildChildEnv } from "@wrongstack/core/utils";
521
+ import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
511
522
  var EMPTY_BUFFER = Buffer.alloc(0);
512
523
  var TerminalServer = class {
513
524
  terminals = /* @__PURE__ */ new Map();
@@ -627,13 +638,11 @@ var TerminalServer = class {
627
638
  state.outputHead = 0;
628
639
  }
629
640
  };
641
+ state.onData = onData;
630
642
  proc.stdout?.on("data", onData);
631
643
  proc.stderr?.on("data", onData);
632
644
  state.timeoutHandle = setTimeout(() => {
633
- try {
634
- proc.kill("SIGTERM");
635
- } catch {
636
- }
645
+ treeKill2(proc);
637
646
  }, this.commandTimeoutMs);
638
647
  this.terminals.set(id, state);
639
648
  return { terminalId: id };
@@ -657,14 +666,15 @@ var TerminalServer = class {
657
666
  if (!state) throw new Error(`unknown terminal: ${terminalId}`);
658
667
  return state.exitPromise;
659
668
  }
660
- /** Kill the process but keep the terminal record (agent can still read output). */
669
+ /**
670
+ * Kill the process but keep the terminal record (agent can still read output).
671
+ * On POSIX this signals only the direct child; descendants may survive because
672
+ * terminal processes are not spawned as process-group leaders.
673
+ */
661
674
  kill(terminalId) {
662
675
  const state = this.terminals.get(terminalId);
663
676
  if (!state) throw new Error(`unknown terminal: ${terminalId}`);
664
- try {
665
- state.proc.kill("SIGTERM");
666
- } catch {
667
- }
677
+ treeKill2(state.proc);
668
678
  }
669
679
  /** Kill the process if alive and remove the record. */
670
680
  release(terminalId) {
@@ -674,10 +684,17 @@ var TerminalServer = class {
674
684
  clearTimeout(state.timeoutHandle);
675
685
  state.timeoutHandle = null;
676
686
  }
677
- try {
678
- state.proc.kill("SIGKILL");
679
- } catch {
680
- }
687
+ if (state.onData) {
688
+ state.proc.stdout?.off("data", state.onData);
689
+ state.proc.stderr?.off("data", state.onData);
690
+ state.onData = void 0;
691
+ }
692
+ state.proc.stdout?.destroy?.();
693
+ state.proc.stderr?.destroy?.();
694
+ state.outputChunks.length = 0;
695
+ state.outputHead = 0;
696
+ state.retainedBytes = 0;
697
+ treeKill2(state.proc, { force: true });
681
698
  this.terminals.delete(terminalId);
682
699
  }
683
700
  /** Kill all active terminals. Used on session close. */
@@ -862,6 +879,9 @@ var WebSocketClientTransport = class {
862
879
  const pending = this.pendingStart;
863
880
  if (pending === null) return;
864
881
  this.pendingStart = null;
882
+ this.closed = true;
883
+ if (this.ws === ws) this.ws = null;
884
+ this.handlers.clear();
865
885
  try {
866
886
  ws.close();
867
887
  } catch {
@@ -879,16 +899,21 @@ var WebSocketClientTransport = class {
879
899
  ws.addEventListener("error", (ev) => {
880
900
  const pending = this.pendingStart;
881
901
  if (pending === null) {
882
- this.closed = true;
902
+ this.stop();
883
903
  return;
884
904
  }
885
905
  this.pendingStart = null;
906
+ this.closed = true;
907
+ if (this.ws === ws) this.ws = null;
908
+ this.handlers.clear();
886
909
  clearTimeout(pending.timer);
887
910
  const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
888
911
  pending.reject(new Error(message));
889
912
  });
890
913
  ws.addEventListener("close", () => {
891
914
  this.closed = true;
915
+ if (this.ws === ws) this.ws = null;
916
+ this.handlers.clear();
892
917
  const pending = this.pendingStart;
893
918
  if (pending !== null) {
894
919
  this.pendingStart = null;
@@ -924,6 +949,7 @@ var WebSocketClientTransport = class {
924
949
  }
925
950
  stop() {
926
951
  this.closed = true;
952
+ this.handlers.clear();
927
953
  if (this.pendingStart !== null) {
928
954
  const pending = this.pendingStart;
929
955
  this.pendingStart = null;