@wrongstack/acp 1.0.26 → 1.0.28
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/agent/server-agent-turn.d.ts +4 -2
- package/dist/agent/stdio-transport.d.ts +12 -1
- package/dist/agent.js +49 -26
- package/dist/client/acp-request-state.d.ts +7 -0
- package/dist/client/websocket-transport.d.ts +5 -1
- package/dist/client.js +118 -24
- package/dist/index.js +127 -32
- package/dist/wrongstack-acp-agent.js +19 -8
- package/package.json +3 -3
|
@@ -162,8 +162,10 @@ declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
|
162
162
|
* When the prompt is all text we return a plain string (the common,
|
|
163
163
|
* cheapest path). When it carries images we build a multimodal
|
|
164
164
|
* `ContentBlock[]` the provider can pass to a vision-capable model.
|
|
165
|
-
*
|
|
166
|
-
*
|
|
165
|
+
* Images go through core's shared ingest (allowlist, base64, size and count
|
|
166
|
+
* caps, the bytes decide the media type): the client's label used to reach
|
|
167
|
+
* the provider unchecked. Every other block renders as in the text path, so
|
|
168
|
+
* an embedded resource keeps its `[embedded resource: <uri>]` header.
|
|
167
169
|
*/
|
|
168
170
|
declare function promptToAgentInput(blocks: readonly ContentBlock[]): AgentInput;
|
|
169
171
|
/**
|
|
@@ -55,6 +55,13 @@ export interface ACPClientTransport {
|
|
|
55
55
|
* message stream via onMessage, so claim semantics are not needed.
|
|
56
56
|
*/
|
|
57
57
|
onMessageClaim?(handler: (msg: ACPMessage) => boolean): () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Called once when the connection ends for any reason (the agent exited,
|
|
60
|
+
* the socket closed, a frame limit tripped, stop()). Optional for custom
|
|
61
|
+
* transports; without it a client learns of a dead agent only when each
|
|
62
|
+
* in-flight request times out.
|
|
63
|
+
*/
|
|
64
|
+
onClose?(handler: (reason: string) => void): () => void;
|
|
58
65
|
stop(): void;
|
|
59
66
|
}
|
|
60
67
|
export declare class StdioTransport implements AgentServerTransport {
|
|
@@ -134,6 +141,7 @@ export declare class ClientTransport implements ACPClientTransport {
|
|
|
134
141
|
private buffer;
|
|
135
142
|
private readonly handlers;
|
|
136
143
|
private readonly claimHandlers;
|
|
144
|
+
private readonly closeHandlers;
|
|
137
145
|
private closed;
|
|
138
146
|
private resolveRead;
|
|
139
147
|
private messageQueue;
|
|
@@ -158,7 +166,10 @@ export declare class ClientTransport implements ACPClientTransport {
|
|
|
158
166
|
* the gating logic.
|
|
159
167
|
*/
|
|
160
168
|
onMessageClaim(handler: (msg: ACPMessage) => boolean): () => void;
|
|
161
|
-
|
|
169
|
+
onClose(handler: (reason: string) => void): () => void;
|
|
170
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
171
|
+
private notifyClosed;
|
|
172
|
+
stop(reason?: string): void;
|
|
162
173
|
private onChildData;
|
|
163
174
|
private onChildError;
|
|
164
175
|
private onChildClose;
|
package/dist/agent.js
CHANGED
|
@@ -534,6 +534,7 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
534
534
|
modeId
|
|
535
535
|
}
|
|
536
536
|
});
|
|
537
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
537
538
|
await ctx.sendResult(id, {});
|
|
538
539
|
return false;
|
|
539
540
|
}
|
|
@@ -559,7 +560,7 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
559
560
|
configOptions: options
|
|
560
561
|
}
|
|
561
562
|
});
|
|
562
|
-
await ctx.persist(session);
|
|
563
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
563
564
|
await ctx.sendResult(id, { configOptions: options });
|
|
564
565
|
return false;
|
|
565
566
|
}
|
|
@@ -976,6 +977,7 @@ var ACPProtocolHandler = class {
|
|
|
976
977
|
};
|
|
977
978
|
|
|
978
979
|
// src/agent/server-agent-turn.ts
|
|
980
|
+
import { parseIncomingImages } from "@wrongstack/core/utils";
|
|
979
981
|
function makeACPServerAgentTurn(opts) {
|
|
980
982
|
const agents = /* @__PURE__ */ new Map();
|
|
981
983
|
const timeouts = /* @__PURE__ */ new Map();
|
|
@@ -987,6 +989,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
987
989
|
const maxHistoryBytes = finitePositiveLimit(opts.maxHistoryBytes, 8 * 1024 * 1024);
|
|
988
990
|
const deliveryNoticeDebounceMs = finiteNonNegativeLimit(opts.deliveryNoticeDebounceMs, 1500);
|
|
989
991
|
const notifiers = /* @__PURE__ */ new Map();
|
|
992
|
+
const liveTurns = /* @__PURE__ */ new Map();
|
|
990
993
|
const stillPending = (sessionId) => {
|
|
991
994
|
if (!opts.pendingDeliveries) return true;
|
|
992
995
|
try {
|
|
@@ -1056,6 +1059,9 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1056
1059
|
return state;
|
|
1057
1060
|
};
|
|
1058
1061
|
const turn = async (input, emit, api) => {
|
|
1062
|
+
const token = Symbol(input.sessionId);
|
|
1063
|
+
liveTurns.set(input.sessionId, token);
|
|
1064
|
+
const alive = () => liveTurns.get(input.sessionId) === token;
|
|
1059
1065
|
let agent = agents.get(input.sessionId);
|
|
1060
1066
|
if (!agent) {
|
|
1061
1067
|
agent = await opts.agentFor(
|
|
@@ -1064,6 +1070,12 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1064
1070
|
api,
|
|
1065
1071
|
input.mcpServers
|
|
1066
1072
|
);
|
|
1073
|
+
if (!alive()) {
|
|
1074
|
+
const stale = agent;
|
|
1075
|
+
void Promise.resolve().then(() => stale.teardown?.()).catch(() => {
|
|
1076
|
+
});
|
|
1077
|
+
return { stopReason: "cancelled" };
|
|
1078
|
+
}
|
|
1067
1079
|
agents.set(input.sessionId, agent);
|
|
1068
1080
|
if (pendingSeed.has(input.sessionId)) {
|
|
1069
1081
|
pendingSeed.delete(input.sessionId);
|
|
@@ -1129,10 +1141,11 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1129
1141
|
content: { type: "text", text }
|
|
1130
1142
|
});
|
|
1131
1143
|
}
|
|
1144
|
+
const record = alive();
|
|
1132
1145
|
const userText = promptToText(input.prompt);
|
|
1133
1146
|
const hist = history.get(input.sessionId) ?? [];
|
|
1134
1147
|
let retainedHistoryBytes = historyBytes.get(input.sessionId) ?? 0;
|
|
1135
|
-
if (userText) {
|
|
1148
|
+
if (record && userText) {
|
|
1136
1149
|
const update = {
|
|
1137
1150
|
sessionUpdate: "user_message_chunk",
|
|
1138
1151
|
content: { type: "text", text: userText }
|
|
@@ -1140,7 +1153,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1140
1153
|
hist.push(update);
|
|
1141
1154
|
retainedHistoryBytes += replayEntryBytes(update);
|
|
1142
1155
|
}
|
|
1143
|
-
if (text) {
|
|
1156
|
+
if (record && text) {
|
|
1144
1157
|
const update = {
|
|
1145
1158
|
sessionUpdate: "agent_message_chunk",
|
|
1146
1159
|
content: { type: "text", text }
|
|
@@ -1188,6 +1201,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1188
1201
|
if (usage) result_out.usage = usage;
|
|
1189
1202
|
return result_out;
|
|
1190
1203
|
} finally {
|
|
1204
|
+
if (alive()) liveTurns.delete(input.sessionId);
|
|
1191
1205
|
clearTimeout(timer);
|
|
1192
1206
|
timeouts.delete(input.sessionId);
|
|
1193
1207
|
input.signal.removeEventListener("abort", onParentAbort);
|
|
@@ -1214,6 +1228,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1214
1228
|
pendingSeed.add(sessionId);
|
|
1215
1229
|
};
|
|
1216
1230
|
const dispose = (sessionId) => {
|
|
1231
|
+
liveTurns.delete(sessionId);
|
|
1217
1232
|
const timer = timeouts.get(sessionId);
|
|
1218
1233
|
if (timer) clearTimeout(timer);
|
|
1219
1234
|
timeouts.delete(sessionId);
|
|
@@ -1322,26 +1337,24 @@ function isRecord(v) {
|
|
|
1322
1337
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1323
1338
|
}
|
|
1324
1339
|
function promptToAgentInput(blocks) {
|
|
1325
|
-
const
|
|
1326
|
-
if (
|
|
1340
|
+
const images = blocks.filter((b) => b.type === "image");
|
|
1341
|
+
if (images.length === 0) {
|
|
1327
1342
|
return promptToText(blocks);
|
|
1328
1343
|
}
|
|
1344
|
+
let parsed;
|
|
1345
|
+
try {
|
|
1346
|
+
parsed = parseIncomingImages(images.map((b) => ({ data: b.data, mediaType: b.mimeType })));
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
throw Object.assign(new Error(err.message), { code: -32602 });
|
|
1349
|
+
}
|
|
1329
1350
|
const out = [];
|
|
1351
|
+
let next = 0;
|
|
1330
1352
|
for (const b of blocks) {
|
|
1331
|
-
if (b.type === "
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
source: { type: "base64", media_type: b.mimeType, data: b.data }
|
|
1337
|
-
});
|
|
1338
|
-
} else if (b.type === "audio") {
|
|
1339
|
-
out.push({ type: "text", text: `[audio: ${b.mimeType}]` });
|
|
1340
|
-
} else if (b.type === "resource") {
|
|
1341
|
-
const text = "text" in b.resource && typeof b.resource.text === "string" ? b.resource.text : `[embedded resource: ${b.resource.uri}]`;
|
|
1342
|
-
out.push({ type: "text", text });
|
|
1343
|
-
} else if (b.type === "resource_link") {
|
|
1344
|
-
out.push({ type: "text", text: `[resource link: ${b.uri}]` });
|
|
1353
|
+
if (b.type === "image") out.push(parsed[next++]);
|
|
1354
|
+
else if (b.type === "text") out.push({ type: "text", text: b.text });
|
|
1355
|
+
else {
|
|
1356
|
+
const text = promptToText([b]);
|
|
1357
|
+
if (text) out.push({ type: "text", text });
|
|
1345
1358
|
}
|
|
1346
1359
|
}
|
|
1347
1360
|
return out;
|
|
@@ -1691,6 +1704,16 @@ import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
|
1691
1704
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
1692
1705
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
1693
1706
|
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
1707
|
+
function takeLines(pending, chunk) {
|
|
1708
|
+
const lines = [];
|
|
1709
|
+
let start = 0;
|
|
1710
|
+
for (let nl = chunk.indexOf("\n"); nl !== -1; nl = chunk.indexOf("\n", start)) {
|
|
1711
|
+
lines.push(pending + chunk.slice(start, nl));
|
|
1712
|
+
pending = "";
|
|
1713
|
+
start = nl + 1;
|
|
1714
|
+
}
|
|
1715
|
+
return { lines, pending: pending + chunk.slice(start) };
|
|
1716
|
+
}
|
|
1694
1717
|
function positiveLimit(value, fallback) {
|
|
1695
1718
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
1696
1719
|
}
|
|
@@ -1778,9 +1801,8 @@ var StdioTransport = class {
|
|
|
1778
1801
|
this.claimHandlers.clear();
|
|
1779
1802
|
}
|
|
1780
1803
|
onData(chunk) {
|
|
1781
|
-
this.buffer
|
|
1782
|
-
|
|
1783
|
-
this.buffer = lines.pop() ?? "";
|
|
1804
|
+
const { lines, pending } = takeLines(this.buffer, chunk);
|
|
1805
|
+
this.buffer = pending;
|
|
1784
1806
|
if (this.buffer.length > this.maxFrameChars) {
|
|
1785
1807
|
this.stderr.write(
|
|
1786
1808
|
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
@@ -2087,16 +2109,17 @@ var WrongStackACPServer = class {
|
|
|
2087
2109
|
return;
|
|
2088
2110
|
}
|
|
2089
2111
|
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
2090
|
-
|
|
2112
|
+
const chunks = [];
|
|
2091
2113
|
let bodyBytes = 0;
|
|
2092
2114
|
let tooLarge = false;
|
|
2093
2115
|
for await (const chunk of req) {
|
|
2094
2116
|
bodyBytes += chunk.length;
|
|
2095
2117
|
if (bodyBytes > MAX_HTTP_BODY) {
|
|
2096
2118
|
tooLarge = true;
|
|
2097
|
-
|
|
2119
|
+
chunks.length = 0;
|
|
2120
|
+
if (bodyBytes > 2 * MAX_HTTP_BODY) break;
|
|
2098
2121
|
}
|
|
2099
|
-
|
|
2122
|
+
if (!tooLarge) chunks.push(chunk);
|
|
2100
2123
|
}
|
|
2101
2124
|
if (tooLarge) {
|
|
2102
2125
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
@@ -2105,7 +2128,7 @@ var WrongStackACPServer = class {
|
|
|
2105
2128
|
}
|
|
2106
2129
|
let msg;
|
|
2107
2130
|
try {
|
|
2108
|
-
msg = JSON.parse(
|
|
2131
|
+
msg = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2109
2132
|
} catch {
|
|
2110
2133
|
res.writeHead(400);
|
|
2111
2134
|
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
@@ -6,4 +6,11 @@ export interface PendingRequest {
|
|
|
6
6
|
timeoutHandle: ReturnType<typeof setTimeout>;
|
|
7
7
|
}
|
|
8
8
|
export type State = 'init' | 'ready' | 'authenticated' | 'sessioning' | 'prompting' | 'done' | 'closed';
|
|
9
|
+
/**
|
|
10
|
+
* Fail and forget every in-flight request. Without a `reason` the session was
|
|
11
|
+
* closed locally; with one, the agent connection ended (the process exited,
|
|
12
|
+
* the socket closed), so no reply can arrive and waiting out each request's
|
|
13
|
+
* timeout would only stall the caller.
|
|
14
|
+
*/
|
|
15
|
+
export declare function rejectPendingRequests(pending: Map<string | number, PendingRequest>, reason?: string): void;
|
|
9
16
|
//# sourceMappingURL=acp-request-state.d.ts.map
|
|
@@ -31,6 +31,7 @@ export interface WebSocketClientTransportOptions {
|
|
|
31
31
|
export declare class WebSocketClientTransport implements ACPClientTransport {
|
|
32
32
|
private ws;
|
|
33
33
|
private readonly handlers;
|
|
34
|
+
private readonly closeHandlers;
|
|
34
35
|
private closed;
|
|
35
36
|
private readonly opts;
|
|
36
37
|
private readonly maxBufferedBytes;
|
|
@@ -41,7 +42,10 @@ export declare class WebSocketClientTransport implements ACPClientTransport {
|
|
|
41
42
|
start(): Promise<void>;
|
|
42
43
|
send(msg: ACPMessage): Promise<void>;
|
|
43
44
|
onMessage(handler: (msg: ACPMessage) => void): () => void;
|
|
44
|
-
|
|
45
|
+
onClose(handler: (reason: string) => void): () => void;
|
|
46
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
47
|
+
private notifyClosed;
|
|
48
|
+
stop(reason?: string): void;
|
|
45
49
|
private onData;
|
|
46
50
|
private dispatch;
|
|
47
51
|
}
|
package/dist/client.js
CHANGED
|
@@ -11,6 +11,16 @@ import {
|
|
|
11
11
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
12
12
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
13
13
|
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
14
|
+
function takeLines(pending, chunk) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
let start = 0;
|
|
17
|
+
for (let nl = chunk.indexOf("\n"); nl !== -1; nl = chunk.indexOf("\n", start)) {
|
|
18
|
+
lines.push(pending + chunk.slice(start, nl));
|
|
19
|
+
pending = "";
|
|
20
|
+
start = nl + 1;
|
|
21
|
+
}
|
|
22
|
+
return { lines, pending: pending + chunk.slice(start) };
|
|
23
|
+
}
|
|
14
24
|
function positiveLimit(value, fallback) {
|
|
15
25
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
16
26
|
}
|
|
@@ -19,6 +29,7 @@ var ClientTransport = class {
|
|
|
19
29
|
buffer = "";
|
|
20
30
|
handlers = /* @__PURE__ */ new Set();
|
|
21
31
|
claimHandlers = /* @__PURE__ */ new Set();
|
|
32
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
22
33
|
closed = false;
|
|
23
34
|
resolveRead = null;
|
|
24
35
|
messageQueue = [];
|
|
@@ -126,6 +137,7 @@ var ClientTransport = class {
|
|
|
126
137
|
}
|
|
127
138
|
send(msg) {
|
|
128
139
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
140
|
+
if (this.closed) return Promise.reject(new Error("ClientTransport is closed"));
|
|
129
141
|
return new Promise((resolve3, reject) => {
|
|
130
142
|
const line = JSON.stringify(msg) + "\n";
|
|
131
143
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
@@ -163,7 +175,22 @@ var ClientTransport = class {
|
|
|
163
175
|
this.claimHandlers.add(handler);
|
|
164
176
|
return () => this.claimHandlers.delete(handler);
|
|
165
177
|
}
|
|
166
|
-
|
|
178
|
+
onClose(handler) {
|
|
179
|
+
this.closeHandlers.add(handler);
|
|
180
|
+
return () => this.closeHandlers.delete(handler);
|
|
181
|
+
}
|
|
182
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
183
|
+
notifyClosed(reason) {
|
|
184
|
+
const handlers = [...this.closeHandlers];
|
|
185
|
+
this.closeHandlers.clear();
|
|
186
|
+
for (const handler of handlers) {
|
|
187
|
+
try {
|
|
188
|
+
handler(reason);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
stop(reason = "transport stopped") {
|
|
167
194
|
this.closed = true;
|
|
168
195
|
this.resolveRead?.(null);
|
|
169
196
|
this.resolveRead = null;
|
|
@@ -172,19 +199,19 @@ var ClientTransport = class {
|
|
|
172
199
|
this.queuedChars = 0;
|
|
173
200
|
this.handlers.clear();
|
|
174
201
|
this.claimHandlers.clear();
|
|
202
|
+
this.notifyClosed(reason);
|
|
175
203
|
const child = this.child;
|
|
176
204
|
if (!child) return;
|
|
177
205
|
treeKill(child);
|
|
178
206
|
this.child = null;
|
|
179
207
|
}
|
|
180
208
|
onChildData(chunk) {
|
|
181
|
-
this.buffer
|
|
182
|
-
|
|
183
|
-
this.buffer = lines.pop() ?? "";
|
|
209
|
+
const { lines, pending } = takeLines(this.buffer, chunk);
|
|
210
|
+
this.buffer = pending;
|
|
184
211
|
if (this.buffer.length > this.maxFrameChars) {
|
|
185
212
|
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
186
213
|
`);
|
|
187
|
-
this.stop();
|
|
214
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
188
215
|
return;
|
|
189
216
|
}
|
|
190
217
|
for (const raw of lines) {
|
|
@@ -192,7 +219,7 @@ var ClientTransport = class {
|
|
|
192
219
|
if (raw.length > this.maxFrameChars) {
|
|
193
220
|
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
194
221
|
`);
|
|
195
|
-
this.stop();
|
|
222
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
196
223
|
return;
|
|
197
224
|
}
|
|
198
225
|
try {
|
|
@@ -218,6 +245,7 @@ var ClientTransport = class {
|
|
|
218
245
|
writeErr(`[acp-child exited with code ${code}]
|
|
219
246
|
`);
|
|
220
247
|
}
|
|
248
|
+
this.notifyClosed(`agent process exited with code ${code}`);
|
|
221
249
|
}
|
|
222
250
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
223
251
|
if (this.resolveRead) {
|
|
@@ -364,6 +392,16 @@ function isAuthRequiredError(err) {
|
|
|
364
392
|
return false;
|
|
365
393
|
}
|
|
366
394
|
|
|
395
|
+
// src/client/acp-request-state.ts
|
|
396
|
+
function rejectPendingRequests(pending, reason) {
|
|
397
|
+
for (const p of pending.values()) {
|
|
398
|
+
clearTimeout(p.timeoutHandle);
|
|
399
|
+
const message = reason === void 0 ? "session was closed" : `agent connection closed (${reason}) during ${p.method}`;
|
|
400
|
+
p.reject(new ACPSessionError("closed", message));
|
|
401
|
+
}
|
|
402
|
+
pending.clear();
|
|
403
|
+
}
|
|
404
|
+
|
|
367
405
|
// src/client/acp-session-ops.ts
|
|
368
406
|
function filterMcpServers(agentCapabilities, servers) {
|
|
369
407
|
if (!servers || servers.length === 0) return [];
|
|
@@ -1263,11 +1301,30 @@ function makePermissionPolicy(decide) {
|
|
|
1263
1301
|
// src/client/terminal-server.ts
|
|
1264
1302
|
import { spawn } from "node:child_process";
|
|
1265
1303
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1266
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1304
|
+
import { accessSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1267
1305
|
import * as path2 from "node:path";
|
|
1268
1306
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1269
1307
|
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
1270
1308
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
1309
|
+
function win32BatchTarget(command) {
|
|
1310
|
+
if (/\.(?:cmd|bat)$/i.test(command)) return command;
|
|
1311
|
+
if (/[\\/]/.test(command) || path2.extname(command)) return void 0;
|
|
1312
|
+
const exts = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").toLowerCase().split(";");
|
|
1313
|
+
for (const dir of (process.env["PATH"] ?? "").split(path2.delimiter)) {
|
|
1314
|
+
if (!dir) continue;
|
|
1315
|
+
for (const ext of exts) {
|
|
1316
|
+
if (!ext) continue;
|
|
1317
|
+
const full = path2.join(dir, `${command}${ext}`);
|
|
1318
|
+
try {
|
|
1319
|
+
accessSync(full);
|
|
1320
|
+
} catch {
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
return ext === ".cmd" || ext === ".bat" ? full : void 0;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return void 0;
|
|
1327
|
+
}
|
|
1271
1328
|
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
1272
1329
|
var TerminalServer = class {
|
|
1273
1330
|
terminals = /* @__PURE__ */ new Map();
|
|
@@ -1318,11 +1375,14 @@ var TerminalServer = class {
|
|
|
1318
1375
|
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1319
1376
|
this.maxOutputByteLimit
|
|
1320
1377
|
);
|
|
1321
|
-
const
|
|
1378
|
+
const batch = process.platform === "win32" ? win32BatchTarget(params.command) : void 0;
|
|
1379
|
+
const shim = batch ? buildWin32CmdShimInvocation(batch, params.args ?? []) : void 0;
|
|
1380
|
+
const proc = spawn(shim?.command ?? params.command, shim?.args ?? params.args ?? [], {
|
|
1322
1381
|
cwd,
|
|
1323
1382
|
env: this.buildEnv(params.env),
|
|
1324
1383
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1325
|
-
windowsHide: true
|
|
1384
|
+
windowsHide: true,
|
|
1385
|
+
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
1326
1386
|
// shell: false on purpose. The terminal server is invoked with
|
|
1327
1387
|
// the agent's explicit argv; turning on shell-mode would make
|
|
1328
1388
|
// the command a single shell-parsed string, which breaks
|
|
@@ -1619,7 +1679,7 @@ function isAllowed(decision) {
|
|
|
1619
1679
|
}
|
|
1620
1680
|
function toTrustBoundaryRequest(request, options) {
|
|
1621
1681
|
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
1622
|
-
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId :
|
|
1682
|
+
const sessionId = options.actor?.sessionId ?? (typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : void 0);
|
|
1623
1683
|
return {
|
|
1624
1684
|
version: 1,
|
|
1625
1685
|
requestId: String(request.toolCall.toolCallId),
|
|
@@ -1655,6 +1715,7 @@ function makeTrustBoundaryPermissionPolicy(options) {
|
|
|
1655
1715
|
var WebSocketClientTransport = class {
|
|
1656
1716
|
ws = null;
|
|
1657
1717
|
handlers = /* @__PURE__ */ new Set();
|
|
1718
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
1658
1719
|
closed = false;
|
|
1659
1720
|
opts;
|
|
1660
1721
|
maxBufferedBytes;
|
|
@@ -1681,6 +1742,7 @@ var WebSocketClientTransport = class {
|
|
|
1681
1742
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
1682
1743
|
return new Promise((resolve3, reject) => {
|
|
1683
1744
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
1745
|
+
ws.binaryType = "arraybuffer";
|
|
1684
1746
|
this.ws = ws;
|
|
1685
1747
|
const timer = setTimeout(() => {
|
|
1686
1748
|
const pending = this.pendingStart;
|
|
@@ -1706,7 +1768,7 @@ var WebSocketClientTransport = class {
|
|
|
1706
1768
|
ws.addEventListener("error", (ev) => {
|
|
1707
1769
|
const pending = this.pendingStart;
|
|
1708
1770
|
if (pending === null) {
|
|
1709
|
-
this.stop();
|
|
1771
|
+
this.stop("WebSocket error");
|
|
1710
1772
|
return;
|
|
1711
1773
|
}
|
|
1712
1774
|
this.pendingStart = null;
|
|
@@ -1727,6 +1789,7 @@ var WebSocketClientTransport = class {
|
|
|
1727
1789
|
clearTimeout(pending.timer);
|
|
1728
1790
|
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
1729
1791
|
}
|
|
1792
|
+
this.notifyClosed("WebSocket closed");
|
|
1730
1793
|
});
|
|
1731
1794
|
ws.addEventListener("message", (ev) => {
|
|
1732
1795
|
this.onData(ev.data);
|
|
@@ -1754,9 +1817,25 @@ var WebSocketClientTransport = class {
|
|
|
1754
1817
|
this.handlers.add(handler);
|
|
1755
1818
|
return () => this.handlers.delete(handler);
|
|
1756
1819
|
}
|
|
1757
|
-
|
|
1820
|
+
onClose(handler) {
|
|
1821
|
+
this.closeHandlers.add(handler);
|
|
1822
|
+
return () => this.closeHandlers.delete(handler);
|
|
1823
|
+
}
|
|
1824
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
1825
|
+
notifyClosed(reason) {
|
|
1826
|
+
const handlers = [...this.closeHandlers];
|
|
1827
|
+
this.closeHandlers.clear();
|
|
1828
|
+
for (const handler of handlers) {
|
|
1829
|
+
try {
|
|
1830
|
+
handler(reason);
|
|
1831
|
+
} catch {
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
stop(reason = "transport stopped") {
|
|
1758
1836
|
this.closed = true;
|
|
1759
1837
|
this.handlers.clear();
|
|
1838
|
+
this.notifyClosed(reason);
|
|
1760
1839
|
if (this.pendingStart !== null) {
|
|
1761
1840
|
const pending = this.pendingStart;
|
|
1762
1841
|
this.pendingStart = null;
|
|
@@ -1777,7 +1856,7 @@ var WebSocketClientTransport = class {
|
|
|
1777
1856
|
onData(data) {
|
|
1778
1857
|
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
1779
1858
|
if (text.length > this.maxMessageChars) {
|
|
1780
|
-
this.stop();
|
|
1859
|
+
this.stop(`agent message exceeds ${this.maxMessageChars} characters`);
|
|
1781
1860
|
return;
|
|
1782
1861
|
}
|
|
1783
1862
|
if (!text.trim()) return;
|
|
@@ -1946,6 +2025,10 @@ var ACPSession = class _ACPSession {
|
|
|
1946
2025
|
}
|
|
1947
2026
|
const session = new _ACPSession(opts, transport);
|
|
1948
2027
|
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
2028
|
+
transport.onClose?.((reason) => {
|
|
2029
|
+
if (!session.closed) rejectPendingRequests(session.pending, reason);
|
|
2030
|
+
void session.close();
|
|
2031
|
+
});
|
|
1949
2032
|
try {
|
|
1950
2033
|
await session.initialize();
|
|
1951
2034
|
} catch (err) {
|
|
@@ -2072,6 +2155,9 @@ var ACPSession = class _ACPSession {
|
|
|
2072
2155
|
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
2073
2156
|
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
2074
2157
|
}
|
|
2158
|
+
if (this.promptCallbackAbort) {
|
|
2159
|
+
throw new ACPSessionError("protocol_error", "prompt called while another prompt is running");
|
|
2160
|
+
}
|
|
2075
2161
|
if (signal.aborted) {
|
|
2076
2162
|
return emptyRunResult("cancelled");
|
|
2077
2163
|
}
|
|
@@ -2143,6 +2229,7 @@ var ACPSession = class _ACPSession {
|
|
|
2143
2229
|
this.sessionId = sessionId;
|
|
2144
2230
|
}
|
|
2145
2231
|
if (signal.aborted) {
|
|
2232
|
+
this.promptCallbackAbort = null;
|
|
2146
2233
|
return emptyRunResult("cancelled");
|
|
2147
2234
|
}
|
|
2148
2235
|
this.resetScratch();
|
|
@@ -2250,11 +2337,7 @@ var ACPSession = class _ACPSession {
|
|
|
2250
2337
|
} catch {
|
|
2251
2338
|
}
|
|
2252
2339
|
}
|
|
2253
|
-
|
|
2254
|
-
clearTimeout(p.timeoutHandle);
|
|
2255
|
-
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
2256
|
-
}
|
|
2257
|
-
this.pending.clear();
|
|
2340
|
+
rejectPendingRequests(this.pending);
|
|
2258
2341
|
this.transportOff?.();
|
|
2259
2342
|
this.transportOff = null;
|
|
2260
2343
|
try {
|
|
@@ -2470,6 +2553,7 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2470
2553
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
2471
2554
|
const persistent = options.persistent === true;
|
|
2472
2555
|
let shared = null;
|
|
2556
|
+
let turns = Promise.resolve();
|
|
2473
2557
|
const startSession = async () => {
|
|
2474
2558
|
return ACPSession.start({
|
|
2475
2559
|
command: options.command,
|
|
@@ -2485,10 +2569,17 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2485
2569
|
};
|
|
2486
2570
|
const runner = async (task, ctx) => {
|
|
2487
2571
|
let session;
|
|
2488
|
-
const reuse = persistent && shared !== null;
|
|
2489
2572
|
try {
|
|
2490
|
-
|
|
2491
|
-
|
|
2573
|
+
if (persistent) {
|
|
2574
|
+
shared ??= startSession();
|
|
2575
|
+
const starting = shared;
|
|
2576
|
+
session = await starting.catch((err) => {
|
|
2577
|
+
if (shared === starting) shared = null;
|
|
2578
|
+
throw err;
|
|
2579
|
+
});
|
|
2580
|
+
} else {
|
|
2581
|
+
session = await startSession();
|
|
2582
|
+
}
|
|
2492
2583
|
} catch (err) {
|
|
2493
2584
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2494
2585
|
}
|
|
@@ -2505,7 +2596,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2505
2596
|
};
|
|
2506
2597
|
let result;
|
|
2507
2598
|
try {
|
|
2508
|
-
|
|
2599
|
+
const turn = () => session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
2600
|
+
const run = persistent ? turns.then(turn, turn) : turn();
|
|
2601
|
+
if (persistent) turns = run;
|
|
2602
|
+
result = await run;
|
|
2509
2603
|
} catch (err) {
|
|
2510
2604
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2511
2605
|
} finally {
|
|
@@ -2526,10 +2620,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2526
2620
|
};
|
|
2527
2621
|
const stop = async () => {
|
|
2528
2622
|
if (shared) {
|
|
2529
|
-
const
|
|
2623
|
+
const starting = shared;
|
|
2530
2624
|
shared = null;
|
|
2531
2625
|
try {
|
|
2532
|
-
await
|
|
2626
|
+
await (await starting).close();
|
|
2533
2627
|
} catch {
|
|
2534
2628
|
}
|
|
2535
2629
|
}
|
package/dist/index.js
CHANGED
|
@@ -537,6 +537,7 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
537
537
|
modeId
|
|
538
538
|
}
|
|
539
539
|
});
|
|
540
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
540
541
|
await ctx.sendResult(id, {});
|
|
541
542
|
return false;
|
|
542
543
|
}
|
|
@@ -562,7 +563,7 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
562
563
|
configOptions: options
|
|
563
564
|
}
|
|
564
565
|
});
|
|
565
|
-
await ctx.persist(session);
|
|
566
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
566
567
|
await ctx.sendResult(id, { configOptions: options });
|
|
567
568
|
return false;
|
|
568
569
|
}
|
|
@@ -991,6 +992,16 @@ import {
|
|
|
991
992
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
992
993
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
993
994
|
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
995
|
+
function takeLines(pending, chunk) {
|
|
996
|
+
const lines = [];
|
|
997
|
+
let start = 0;
|
|
998
|
+
for (let nl = chunk.indexOf("\n"); nl !== -1; nl = chunk.indexOf("\n", start)) {
|
|
999
|
+
lines.push(pending + chunk.slice(start, nl));
|
|
1000
|
+
pending = "";
|
|
1001
|
+
start = nl + 1;
|
|
1002
|
+
}
|
|
1003
|
+
return { lines, pending: pending + chunk.slice(start) };
|
|
1004
|
+
}
|
|
994
1005
|
function positiveLimit(value, fallback) {
|
|
995
1006
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
996
1007
|
}
|
|
@@ -1078,9 +1089,8 @@ var StdioTransport = class {
|
|
|
1078
1089
|
this.claimHandlers.clear();
|
|
1079
1090
|
}
|
|
1080
1091
|
onData(chunk) {
|
|
1081
|
-
this.buffer
|
|
1082
|
-
|
|
1083
|
-
this.buffer = lines.pop() ?? "";
|
|
1092
|
+
const { lines, pending } = takeLines(this.buffer, chunk);
|
|
1093
|
+
this.buffer = pending;
|
|
1084
1094
|
if (this.buffer.length > this.maxFrameChars) {
|
|
1085
1095
|
this.stderr.write(
|
|
1086
1096
|
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
@@ -1161,6 +1171,7 @@ var ClientTransport = class {
|
|
|
1161
1171
|
buffer = "";
|
|
1162
1172
|
handlers = /* @__PURE__ */ new Set();
|
|
1163
1173
|
claimHandlers = /* @__PURE__ */ new Set();
|
|
1174
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
1164
1175
|
closed = false;
|
|
1165
1176
|
resolveRead = null;
|
|
1166
1177
|
messageQueue = [];
|
|
@@ -1268,6 +1279,7 @@ var ClientTransport = class {
|
|
|
1268
1279
|
}
|
|
1269
1280
|
send(msg) {
|
|
1270
1281
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
1282
|
+
if (this.closed) return Promise.reject(new Error("ClientTransport is closed"));
|
|
1271
1283
|
return new Promise((resolve5, reject) => {
|
|
1272
1284
|
const line = JSON.stringify(msg) + "\n";
|
|
1273
1285
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
@@ -1305,7 +1317,22 @@ var ClientTransport = class {
|
|
|
1305
1317
|
this.claimHandlers.add(handler);
|
|
1306
1318
|
return () => this.claimHandlers.delete(handler);
|
|
1307
1319
|
}
|
|
1308
|
-
|
|
1320
|
+
onClose(handler) {
|
|
1321
|
+
this.closeHandlers.add(handler);
|
|
1322
|
+
return () => this.closeHandlers.delete(handler);
|
|
1323
|
+
}
|
|
1324
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
1325
|
+
notifyClosed(reason) {
|
|
1326
|
+
const handlers = [...this.closeHandlers];
|
|
1327
|
+
this.closeHandlers.clear();
|
|
1328
|
+
for (const handler of handlers) {
|
|
1329
|
+
try {
|
|
1330
|
+
handler(reason);
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
stop(reason = "transport stopped") {
|
|
1309
1336
|
this.closed = true;
|
|
1310
1337
|
this.resolveRead?.(null);
|
|
1311
1338
|
this.resolveRead = null;
|
|
@@ -1314,19 +1341,19 @@ var ClientTransport = class {
|
|
|
1314
1341
|
this.queuedChars = 0;
|
|
1315
1342
|
this.handlers.clear();
|
|
1316
1343
|
this.claimHandlers.clear();
|
|
1344
|
+
this.notifyClosed(reason);
|
|
1317
1345
|
const child = this.child;
|
|
1318
1346
|
if (!child) return;
|
|
1319
1347
|
treeKill(child);
|
|
1320
1348
|
this.child = null;
|
|
1321
1349
|
}
|
|
1322
1350
|
onChildData(chunk) {
|
|
1323
|
-
this.buffer
|
|
1324
|
-
|
|
1325
|
-
this.buffer = lines.pop() ?? "";
|
|
1351
|
+
const { lines, pending } = takeLines(this.buffer, chunk);
|
|
1352
|
+
this.buffer = pending;
|
|
1326
1353
|
if (this.buffer.length > this.maxFrameChars) {
|
|
1327
1354
|
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
1328
1355
|
`);
|
|
1329
|
-
this.stop();
|
|
1356
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
1330
1357
|
return;
|
|
1331
1358
|
}
|
|
1332
1359
|
for (const raw of lines) {
|
|
@@ -1334,7 +1361,7 @@ var ClientTransport = class {
|
|
|
1334
1361
|
if (raw.length > this.maxFrameChars) {
|
|
1335
1362
|
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
1336
1363
|
`);
|
|
1337
|
-
this.stop();
|
|
1364
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
1338
1365
|
return;
|
|
1339
1366
|
}
|
|
1340
1367
|
try {
|
|
@@ -1360,6 +1387,7 @@ var ClientTransport = class {
|
|
|
1360
1387
|
writeErr(`[acp-child exited with code ${code}]
|
|
1361
1388
|
`);
|
|
1362
1389
|
}
|
|
1390
|
+
this.notifyClosed(`agent process exited with code ${code}`);
|
|
1363
1391
|
}
|
|
1364
1392
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
1365
1393
|
if (this.resolveRead) {
|
|
@@ -1635,16 +1663,17 @@ var WrongStackACPServer = class {
|
|
|
1635
1663
|
return;
|
|
1636
1664
|
}
|
|
1637
1665
|
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1638
|
-
|
|
1666
|
+
const chunks = [];
|
|
1639
1667
|
let bodyBytes = 0;
|
|
1640
1668
|
let tooLarge = false;
|
|
1641
1669
|
for await (const chunk of req) {
|
|
1642
1670
|
bodyBytes += chunk.length;
|
|
1643
1671
|
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1644
1672
|
tooLarge = true;
|
|
1645
|
-
|
|
1673
|
+
chunks.length = 0;
|
|
1674
|
+
if (bodyBytes > 2 * MAX_HTTP_BODY) break;
|
|
1646
1675
|
}
|
|
1647
|
-
|
|
1676
|
+
if (!tooLarge) chunks.push(chunk);
|
|
1648
1677
|
}
|
|
1649
1678
|
if (tooLarge) {
|
|
1650
1679
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
@@ -1653,7 +1682,7 @@ var WrongStackACPServer = class {
|
|
|
1653
1682
|
}
|
|
1654
1683
|
let msg;
|
|
1655
1684
|
try {
|
|
1656
|
-
msg = JSON.parse(
|
|
1685
|
+
msg = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1657
1686
|
} catch {
|
|
1658
1687
|
res.writeHead(400);
|
|
1659
1688
|
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
@@ -1887,6 +1916,16 @@ function isAuthRequiredError(err) {
|
|
|
1887
1916
|
return false;
|
|
1888
1917
|
}
|
|
1889
1918
|
|
|
1919
|
+
// src/client/acp-request-state.ts
|
|
1920
|
+
function rejectPendingRequests(pending, reason) {
|
|
1921
|
+
for (const p of pending.values()) {
|
|
1922
|
+
clearTimeout(p.timeoutHandle);
|
|
1923
|
+
const message = reason === void 0 ? "session was closed" : `agent connection closed (${reason}) during ${p.method}`;
|
|
1924
|
+
p.reject(new ACPSessionError("closed", message));
|
|
1925
|
+
}
|
|
1926
|
+
pending.clear();
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1890
1929
|
// src/client/acp-session-ops.ts
|
|
1891
1930
|
function filterMcpServers(agentCapabilities, servers) {
|
|
1892
1931
|
if (!servers || servers.length === 0) return [];
|
|
@@ -2786,11 +2825,30 @@ function makePermissionPolicy(decide) {
|
|
|
2786
2825
|
// src/client/terminal-server.ts
|
|
2787
2826
|
import { spawn } from "node:child_process";
|
|
2788
2827
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2789
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
2828
|
+
import { accessSync, realpathSync as realpathSync2 } from "node:fs";
|
|
2790
2829
|
import * as path3 from "node:path";
|
|
2791
2830
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
2792
2831
|
import { treeKill as treeKill2 } from "@wrongstack/core/utils/tree-kill";
|
|
2793
2832
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
2833
|
+
function win32BatchTarget(command) {
|
|
2834
|
+
if (/\.(?:cmd|bat)$/i.test(command)) return command;
|
|
2835
|
+
if (/[\\/]/.test(command) || path3.extname(command)) return void 0;
|
|
2836
|
+
const exts = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").toLowerCase().split(";");
|
|
2837
|
+
for (const dir of (process.env["PATH"] ?? "").split(path3.delimiter)) {
|
|
2838
|
+
if (!dir) continue;
|
|
2839
|
+
for (const ext of exts) {
|
|
2840
|
+
if (!ext) continue;
|
|
2841
|
+
const full = path3.join(dir, `${command}${ext}`);
|
|
2842
|
+
try {
|
|
2843
|
+
accessSync(full);
|
|
2844
|
+
} catch {
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
return ext === ".cmd" || ext === ".bat" ? full : void 0;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
return void 0;
|
|
2851
|
+
}
|
|
2794
2852
|
var DEBUG_DISPOSE = typeof process !== "undefined" && !!process.env?.WRONGSTACK_DEBUG && process.env.WRONGSTACK_DEBUG !== "0" && process.env.WRONGSTACK_DEBUG !== "false";
|
|
2795
2853
|
var TerminalServer = class {
|
|
2796
2854
|
terminals = /* @__PURE__ */ new Map();
|
|
@@ -2841,11 +2899,14 @@ var TerminalServer = class {
|
|
|
2841
2899
|
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
2842
2900
|
this.maxOutputByteLimit
|
|
2843
2901
|
);
|
|
2844
|
-
const
|
|
2902
|
+
const batch = process.platform === "win32" ? win32BatchTarget(params.command) : void 0;
|
|
2903
|
+
const shim = batch ? buildWin32CmdShimInvocation(batch, params.args ?? []) : void 0;
|
|
2904
|
+
const proc = spawn(shim?.command ?? params.command, shim?.args ?? params.args ?? [], {
|
|
2845
2905
|
cwd,
|
|
2846
2906
|
env: this.buildEnv(params.env),
|
|
2847
2907
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2848
|
-
windowsHide: true
|
|
2908
|
+
windowsHide: true,
|
|
2909
|
+
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
2849
2910
|
// shell: false on purpose. The terminal server is invoked with
|
|
2850
2911
|
// the agent's explicit argv; turning on shell-mode would make
|
|
2851
2912
|
// the command a single shell-parsed string, which breaks
|
|
@@ -3142,7 +3203,7 @@ function isAllowed(decision) {
|
|
|
3142
3203
|
}
|
|
3143
3204
|
function toTrustBoundaryRequest(request, options) {
|
|
3144
3205
|
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
3145
|
-
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId :
|
|
3206
|
+
const sessionId = options.actor?.sessionId ?? (typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : void 0);
|
|
3146
3207
|
return {
|
|
3147
3208
|
version: 1,
|
|
3148
3209
|
requestId: String(request.toolCall.toolCallId),
|
|
@@ -3178,6 +3239,7 @@ function makeTrustBoundaryPermissionPolicy(options) {
|
|
|
3178
3239
|
var WebSocketClientTransport = class {
|
|
3179
3240
|
ws = null;
|
|
3180
3241
|
handlers = /* @__PURE__ */ new Set();
|
|
3242
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
3181
3243
|
closed = false;
|
|
3182
3244
|
opts;
|
|
3183
3245
|
maxBufferedBytes;
|
|
@@ -3204,6 +3266,7 @@ var WebSocketClientTransport = class {
|
|
|
3204
3266
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
3205
3267
|
return new Promise((resolve5, reject) => {
|
|
3206
3268
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
3269
|
+
ws.binaryType = "arraybuffer";
|
|
3207
3270
|
this.ws = ws;
|
|
3208
3271
|
const timer = setTimeout(() => {
|
|
3209
3272
|
const pending = this.pendingStart;
|
|
@@ -3229,7 +3292,7 @@ var WebSocketClientTransport = class {
|
|
|
3229
3292
|
ws.addEventListener("error", (ev) => {
|
|
3230
3293
|
const pending = this.pendingStart;
|
|
3231
3294
|
if (pending === null) {
|
|
3232
|
-
this.stop();
|
|
3295
|
+
this.stop("WebSocket error");
|
|
3233
3296
|
return;
|
|
3234
3297
|
}
|
|
3235
3298
|
this.pendingStart = null;
|
|
@@ -3250,6 +3313,7 @@ var WebSocketClientTransport = class {
|
|
|
3250
3313
|
clearTimeout(pending.timer);
|
|
3251
3314
|
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
3252
3315
|
}
|
|
3316
|
+
this.notifyClosed("WebSocket closed");
|
|
3253
3317
|
});
|
|
3254
3318
|
ws.addEventListener("message", (ev) => {
|
|
3255
3319
|
this.onData(ev.data);
|
|
@@ -3277,9 +3341,25 @@ var WebSocketClientTransport = class {
|
|
|
3277
3341
|
this.handlers.add(handler);
|
|
3278
3342
|
return () => this.handlers.delete(handler);
|
|
3279
3343
|
}
|
|
3280
|
-
|
|
3344
|
+
onClose(handler) {
|
|
3345
|
+
this.closeHandlers.add(handler);
|
|
3346
|
+
return () => this.closeHandlers.delete(handler);
|
|
3347
|
+
}
|
|
3348
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
3349
|
+
notifyClosed(reason) {
|
|
3350
|
+
const handlers = [...this.closeHandlers];
|
|
3351
|
+
this.closeHandlers.clear();
|
|
3352
|
+
for (const handler of handlers) {
|
|
3353
|
+
try {
|
|
3354
|
+
handler(reason);
|
|
3355
|
+
} catch {
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
stop(reason = "transport stopped") {
|
|
3281
3360
|
this.closed = true;
|
|
3282
3361
|
this.handlers.clear();
|
|
3362
|
+
this.notifyClosed(reason);
|
|
3283
3363
|
if (this.pendingStart !== null) {
|
|
3284
3364
|
const pending = this.pendingStart;
|
|
3285
3365
|
this.pendingStart = null;
|
|
@@ -3300,7 +3380,7 @@ var WebSocketClientTransport = class {
|
|
|
3300
3380
|
onData(data) {
|
|
3301
3381
|
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
3302
3382
|
if (text.length > this.maxMessageChars) {
|
|
3303
|
-
this.stop();
|
|
3383
|
+
this.stop(`agent message exceeds ${this.maxMessageChars} characters`);
|
|
3304
3384
|
return;
|
|
3305
3385
|
}
|
|
3306
3386
|
if (!text.trim()) return;
|
|
@@ -3469,6 +3549,10 @@ var ACPSession = class _ACPSession {
|
|
|
3469
3549
|
}
|
|
3470
3550
|
const session = new _ACPSession(opts, transport);
|
|
3471
3551
|
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
3552
|
+
transport.onClose?.((reason) => {
|
|
3553
|
+
if (!session.closed) rejectPendingRequests(session.pending, reason);
|
|
3554
|
+
void session.close();
|
|
3555
|
+
});
|
|
3472
3556
|
try {
|
|
3473
3557
|
await session.initialize();
|
|
3474
3558
|
} catch (err) {
|
|
@@ -3595,6 +3679,9 @@ var ACPSession = class _ACPSession {
|
|
|
3595
3679
|
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
3596
3680
|
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
3597
3681
|
}
|
|
3682
|
+
if (this.promptCallbackAbort) {
|
|
3683
|
+
throw new ACPSessionError("protocol_error", "prompt called while another prompt is running");
|
|
3684
|
+
}
|
|
3598
3685
|
if (signal.aborted) {
|
|
3599
3686
|
return emptyRunResult("cancelled");
|
|
3600
3687
|
}
|
|
@@ -3666,6 +3753,7 @@ var ACPSession = class _ACPSession {
|
|
|
3666
3753
|
this.sessionId = sessionId;
|
|
3667
3754
|
}
|
|
3668
3755
|
if (signal.aborted) {
|
|
3756
|
+
this.promptCallbackAbort = null;
|
|
3669
3757
|
return emptyRunResult("cancelled");
|
|
3670
3758
|
}
|
|
3671
3759
|
this.resetScratch();
|
|
@@ -3773,11 +3861,7 @@ var ACPSession = class _ACPSession {
|
|
|
3773
3861
|
} catch {
|
|
3774
3862
|
}
|
|
3775
3863
|
}
|
|
3776
|
-
|
|
3777
|
-
clearTimeout(p.timeoutHandle);
|
|
3778
|
-
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
3779
|
-
}
|
|
3780
|
-
this.pending.clear();
|
|
3864
|
+
rejectPendingRequests(this.pending);
|
|
3781
3865
|
this.transportOff?.();
|
|
3782
3866
|
this.transportOff = null;
|
|
3783
3867
|
try {
|
|
@@ -4535,6 +4619,7 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4535
4619
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
4536
4620
|
const persistent = options.persistent === true;
|
|
4537
4621
|
let shared = null;
|
|
4622
|
+
let turns = Promise.resolve();
|
|
4538
4623
|
const startSession = async () => {
|
|
4539
4624
|
return ACPSession.start({
|
|
4540
4625
|
command: options.command,
|
|
@@ -4550,10 +4635,17 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4550
4635
|
};
|
|
4551
4636
|
const runner = async (task, ctx) => {
|
|
4552
4637
|
let session;
|
|
4553
|
-
const reuse = persistent && shared !== null;
|
|
4554
4638
|
try {
|
|
4555
|
-
|
|
4556
|
-
|
|
4639
|
+
if (persistent) {
|
|
4640
|
+
shared ??= startSession();
|
|
4641
|
+
const starting = shared;
|
|
4642
|
+
session = await starting.catch((err) => {
|
|
4643
|
+
if (shared === starting) shared = null;
|
|
4644
|
+
throw err;
|
|
4645
|
+
});
|
|
4646
|
+
} else {
|
|
4647
|
+
session = await startSession();
|
|
4648
|
+
}
|
|
4557
4649
|
} catch (err) {
|
|
4558
4650
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
4559
4651
|
}
|
|
@@ -4570,7 +4662,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4570
4662
|
};
|
|
4571
4663
|
let result;
|
|
4572
4664
|
try {
|
|
4573
|
-
|
|
4665
|
+
const turn = () => session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
4666
|
+
const run = persistent ? turns.then(turn, turn) : turn();
|
|
4667
|
+
if (persistent) turns = run;
|
|
4668
|
+
result = await run;
|
|
4574
4669
|
} catch (err) {
|
|
4575
4670
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
4576
4671
|
} finally {
|
|
@@ -4591,10 +4686,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4591
4686
|
};
|
|
4592
4687
|
const stop = async () => {
|
|
4593
4688
|
if (shared) {
|
|
4594
|
-
const
|
|
4689
|
+
const starting = shared;
|
|
4595
4690
|
shared = null;
|
|
4596
4691
|
try {
|
|
4597
|
-
await
|
|
4692
|
+
await (await starting).close();
|
|
4598
4693
|
} catch {
|
|
4599
4694
|
}
|
|
4600
4695
|
}
|
|
@@ -541,6 +541,7 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
541
541
|
modeId
|
|
542
542
|
}
|
|
543
543
|
});
|
|
544
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
544
545
|
await ctx.sendResult(id, {});
|
|
545
546
|
return false;
|
|
546
547
|
}
|
|
@@ -566,7 +567,7 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
566
567
|
configOptions: options
|
|
567
568
|
}
|
|
568
569
|
});
|
|
569
|
-
await ctx.persist(session);
|
|
570
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
570
571
|
await ctx.sendResult(id, { configOptions: options });
|
|
571
572
|
return false;
|
|
572
573
|
}
|
|
@@ -988,6 +989,16 @@ import { treeKill } from "@wrongstack/core/utils/tree-kill";
|
|
|
988
989
|
var DEFAULT_MAX_FRAME_CHARS = 20 * 1024 * 1024;
|
|
989
990
|
var DEFAULT_MAX_QUEUED_MESSAGES = 1e3;
|
|
990
991
|
var DEFAULT_MAX_QUEUED_CHARS = 32 * 1024 * 1024;
|
|
992
|
+
function takeLines(pending, chunk) {
|
|
993
|
+
const lines = [];
|
|
994
|
+
let start = 0;
|
|
995
|
+
for (let nl = chunk.indexOf("\n"); nl !== -1; nl = chunk.indexOf("\n", start)) {
|
|
996
|
+
lines.push(pending + chunk.slice(start, nl));
|
|
997
|
+
pending = "";
|
|
998
|
+
start = nl + 1;
|
|
999
|
+
}
|
|
1000
|
+
return { lines, pending: pending + chunk.slice(start) };
|
|
1001
|
+
}
|
|
991
1002
|
function positiveLimit(value, fallback) {
|
|
992
1003
|
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
993
1004
|
}
|
|
@@ -1075,9 +1086,8 @@ var StdioTransport = class {
|
|
|
1075
1086
|
this.claimHandlers.clear();
|
|
1076
1087
|
}
|
|
1077
1088
|
onData(chunk) {
|
|
1078
|
-
this.buffer
|
|
1079
|
-
|
|
1080
|
-
this.buffer = lines.pop() ?? "";
|
|
1089
|
+
const { lines, pending } = takeLines(this.buffer, chunk);
|
|
1090
|
+
this.buffer = pending;
|
|
1081
1091
|
if (this.buffer.length > this.maxFrameChars) {
|
|
1082
1092
|
this.stderr.write(
|
|
1083
1093
|
`[wstack-acp frame error] pending frame exceeds ${this.maxFrameChars} characters
|
|
@@ -1267,16 +1277,17 @@ var WrongStackACPServer = class {
|
|
|
1267
1277
|
return;
|
|
1268
1278
|
}
|
|
1269
1279
|
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1270
|
-
|
|
1280
|
+
const chunks = [];
|
|
1271
1281
|
let bodyBytes = 0;
|
|
1272
1282
|
let tooLarge = false;
|
|
1273
1283
|
for await (const chunk of req) {
|
|
1274
1284
|
bodyBytes += chunk.length;
|
|
1275
1285
|
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1276
1286
|
tooLarge = true;
|
|
1277
|
-
|
|
1287
|
+
chunks.length = 0;
|
|
1288
|
+
if (bodyBytes > 2 * MAX_HTTP_BODY) break;
|
|
1278
1289
|
}
|
|
1279
|
-
|
|
1290
|
+
if (!tooLarge) chunks.push(chunk);
|
|
1280
1291
|
}
|
|
1281
1292
|
if (tooLarge) {
|
|
1282
1293
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
@@ -1285,7 +1296,7 @@ var WrongStackACPServer = class {
|
|
|
1285
1296
|
}
|
|
1286
1297
|
let msg;
|
|
1287
1298
|
try {
|
|
1288
|
-
msg = JSON.parse(
|
|
1299
|
+
msg = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1289
1300
|
} catch {
|
|
1290
1301
|
res.writeHead(400);
|
|
1291
1302
|
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/acp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
|
|
6
6
|
"keywords": [
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@agentclientprotocol/sdk": "^1.4.0",
|
|
55
|
-
"@wrongstack/core": "1.0.
|
|
56
|
-
"@wrongstack/primitives": "1.0.
|
|
55
|
+
"@wrongstack/core": "1.0.28",
|
|
56
|
+
"@wrongstack/primitives": "1.0.28"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^26.5.1",
|