@wrongstack/acp 1.0.27 → 1.0.29
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 +39 -24
- package/dist/client/acp-request-state.d.ts +7 -0
- package/dist/client/websocket-transport.d.ts +5 -1
- package/dist/client.js +83 -28
- package/dist/index.js +92 -34
- package/dist/wrongstack-acp-agent.js +9 -6
- package/package.json +4 -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
|
@@ -294,7 +294,7 @@ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
|
|
|
294
294
|
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
295
295
|
return false;
|
|
296
296
|
}
|
|
297
|
-
const candidateCwd = persisted.cwd ??
|
|
297
|
+
const candidateCwd = loadCwd ?? persisted.cwd ?? ctx.defaultCwd;
|
|
298
298
|
const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
|
|
299
299
|
const restored = {
|
|
300
300
|
id: sessionId,
|
|
@@ -475,6 +475,7 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
475
475
|
let result;
|
|
476
476
|
const pendingNotifications = [];
|
|
477
477
|
const emit = (update) => {
|
|
478
|
+
if (ctx.sessions.get(sessionId) !== session) return;
|
|
478
479
|
const notifPromise = ctx.sendNotification({ sessionId, update });
|
|
479
480
|
pendingNotifications.push(notifPromise.catch(() => {
|
|
480
481
|
}));
|
|
@@ -534,6 +535,7 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
534
535
|
modeId
|
|
535
536
|
}
|
|
536
537
|
});
|
|
538
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
537
539
|
await ctx.sendResult(id, {});
|
|
538
540
|
return false;
|
|
539
541
|
}
|
|
@@ -559,7 +561,7 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
559
561
|
configOptions: options
|
|
560
562
|
}
|
|
561
563
|
});
|
|
562
|
-
await ctx.persist(session);
|
|
564
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
563
565
|
await ctx.sendResult(id, { configOptions: options });
|
|
564
566
|
return false;
|
|
565
567
|
}
|
|
@@ -976,6 +978,7 @@ var ACPProtocolHandler = class {
|
|
|
976
978
|
};
|
|
977
979
|
|
|
978
980
|
// src/agent/server-agent-turn.ts
|
|
981
|
+
import { parseIncomingImages } from "@wrongstack/core/utils";
|
|
979
982
|
function makeACPServerAgentTurn(opts) {
|
|
980
983
|
const agents = /* @__PURE__ */ new Map();
|
|
981
984
|
const timeouts = /* @__PURE__ */ new Map();
|
|
@@ -987,6 +990,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
987
990
|
const maxHistoryBytes = finitePositiveLimit(opts.maxHistoryBytes, 8 * 1024 * 1024);
|
|
988
991
|
const deliveryNoticeDebounceMs = finiteNonNegativeLimit(opts.deliveryNoticeDebounceMs, 1500);
|
|
989
992
|
const notifiers = /* @__PURE__ */ new Map();
|
|
993
|
+
const liveTurns = /* @__PURE__ */ new Map();
|
|
990
994
|
const stillPending = (sessionId) => {
|
|
991
995
|
if (!opts.pendingDeliveries) return true;
|
|
992
996
|
try {
|
|
@@ -1056,6 +1060,9 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1056
1060
|
return state;
|
|
1057
1061
|
};
|
|
1058
1062
|
const turn = async (input, emit, api) => {
|
|
1063
|
+
const token = Symbol(input.sessionId);
|
|
1064
|
+
liveTurns.set(input.sessionId, token);
|
|
1065
|
+
const alive = () => liveTurns.get(input.sessionId) === token;
|
|
1059
1066
|
let agent = agents.get(input.sessionId);
|
|
1060
1067
|
if (!agent) {
|
|
1061
1068
|
agent = await opts.agentFor(
|
|
@@ -1064,6 +1071,12 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1064
1071
|
api,
|
|
1065
1072
|
input.mcpServers
|
|
1066
1073
|
);
|
|
1074
|
+
if (!alive()) {
|
|
1075
|
+
const stale = agent;
|
|
1076
|
+
void Promise.resolve().then(() => stale.teardown?.()).catch(() => {
|
|
1077
|
+
});
|
|
1078
|
+
return { stopReason: "cancelled" };
|
|
1079
|
+
}
|
|
1067
1080
|
agents.set(input.sessionId, agent);
|
|
1068
1081
|
if (pendingSeed.has(input.sessionId)) {
|
|
1069
1082
|
pendingSeed.delete(input.sessionId);
|
|
@@ -1129,10 +1142,11 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1129
1142
|
content: { type: "text", text }
|
|
1130
1143
|
});
|
|
1131
1144
|
}
|
|
1145
|
+
const record = alive();
|
|
1132
1146
|
const userText = promptToText(input.prompt);
|
|
1133
1147
|
const hist = history.get(input.sessionId) ?? [];
|
|
1134
1148
|
let retainedHistoryBytes = historyBytes.get(input.sessionId) ?? 0;
|
|
1135
|
-
if (userText) {
|
|
1149
|
+
if (record && userText) {
|
|
1136
1150
|
const update = {
|
|
1137
1151
|
sessionUpdate: "user_message_chunk",
|
|
1138
1152
|
content: { type: "text", text: userText }
|
|
@@ -1140,7 +1154,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1140
1154
|
hist.push(update);
|
|
1141
1155
|
retainedHistoryBytes += replayEntryBytes(update);
|
|
1142
1156
|
}
|
|
1143
|
-
if (text) {
|
|
1157
|
+
if (record && text) {
|
|
1144
1158
|
const update = {
|
|
1145
1159
|
sessionUpdate: "agent_message_chunk",
|
|
1146
1160
|
content: { type: "text", text }
|
|
@@ -1188,6 +1202,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1188
1202
|
if (usage) result_out.usage = usage;
|
|
1189
1203
|
return result_out;
|
|
1190
1204
|
} finally {
|
|
1205
|
+
if (alive()) liveTurns.delete(input.sessionId);
|
|
1191
1206
|
clearTimeout(timer);
|
|
1192
1207
|
timeouts.delete(input.sessionId);
|
|
1193
1208
|
input.signal.removeEventListener("abort", onParentAbort);
|
|
@@ -1214,6 +1229,7 @@ function makeACPServerAgentTurn(opts) {
|
|
|
1214
1229
|
pendingSeed.add(sessionId);
|
|
1215
1230
|
};
|
|
1216
1231
|
const dispose = (sessionId) => {
|
|
1232
|
+
liveTurns.delete(sessionId);
|
|
1217
1233
|
const timer = timeouts.get(sessionId);
|
|
1218
1234
|
if (timer) clearTimeout(timer);
|
|
1219
1235
|
timeouts.delete(sessionId);
|
|
@@ -1322,26 +1338,24 @@ function isRecord(v) {
|
|
|
1322
1338
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1323
1339
|
}
|
|
1324
1340
|
function promptToAgentInput(blocks) {
|
|
1325
|
-
const
|
|
1326
|
-
if (
|
|
1341
|
+
const images = blocks.filter((b) => b.type === "image");
|
|
1342
|
+
if (images.length === 0) {
|
|
1327
1343
|
return promptToText(blocks);
|
|
1328
1344
|
}
|
|
1345
|
+
let parsed;
|
|
1346
|
+
try {
|
|
1347
|
+
parsed = parseIncomingImages(images.map((b) => ({ data: b.data, mediaType: b.mimeType })));
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
throw Object.assign(new Error(err.message), { code: -32602 });
|
|
1350
|
+
}
|
|
1329
1351
|
const out = [];
|
|
1352
|
+
let next = 0;
|
|
1330
1353
|
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}]` });
|
|
1354
|
+
if (b.type === "image") out.push(parsed[next++]);
|
|
1355
|
+
else if (b.type === "text") out.push({ type: "text", text: b.text });
|
|
1356
|
+
else {
|
|
1357
|
+
const text = promptToText([b]);
|
|
1358
|
+
if (text) out.push({ type: "text", text });
|
|
1345
1359
|
}
|
|
1346
1360
|
}
|
|
1347
1361
|
return out;
|
|
@@ -2096,16 +2110,17 @@ var WrongStackACPServer = class {
|
|
|
2096
2110
|
return;
|
|
2097
2111
|
}
|
|
2098
2112
|
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
2099
|
-
|
|
2113
|
+
const chunks = [];
|
|
2100
2114
|
let bodyBytes = 0;
|
|
2101
2115
|
let tooLarge = false;
|
|
2102
2116
|
for await (const chunk of req) {
|
|
2103
2117
|
bodyBytes += chunk.length;
|
|
2104
2118
|
if (bodyBytes > MAX_HTTP_BODY) {
|
|
2105
2119
|
tooLarge = true;
|
|
2106
|
-
|
|
2120
|
+
chunks.length = 0;
|
|
2121
|
+
if (bodyBytes > 2 * MAX_HTTP_BODY) break;
|
|
2107
2122
|
}
|
|
2108
|
-
|
|
2123
|
+
if (!tooLarge) chunks.push(chunk);
|
|
2109
2124
|
}
|
|
2110
2125
|
if (tooLarge) {
|
|
2111
2126
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
@@ -2114,7 +2129,7 @@ var WrongStackACPServer = class {
|
|
|
2114
2129
|
}
|
|
2115
2130
|
let msg;
|
|
2116
2131
|
try {
|
|
2117
|
-
msg = JSON.parse(
|
|
2132
|
+
msg = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2118
2133
|
} catch {
|
|
2119
2134
|
res.writeHead(400);
|
|
2120
2135
|
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
|
@@ -29,6 +29,7 @@ var ClientTransport = class {
|
|
|
29
29
|
buffer = "";
|
|
30
30
|
handlers = /* @__PURE__ */ new Set();
|
|
31
31
|
claimHandlers = /* @__PURE__ */ new Set();
|
|
32
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
32
33
|
closed = false;
|
|
33
34
|
resolveRead = null;
|
|
34
35
|
messageQueue = [];
|
|
@@ -136,6 +137,7 @@ var ClientTransport = class {
|
|
|
136
137
|
}
|
|
137
138
|
send(msg) {
|
|
138
139
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
140
|
+
if (this.closed) return Promise.reject(new Error("ClientTransport is closed"));
|
|
139
141
|
return new Promise((resolve3, reject) => {
|
|
140
142
|
const line = JSON.stringify(msg) + "\n";
|
|
141
143
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
@@ -173,7 +175,22 @@ var ClientTransport = class {
|
|
|
173
175
|
this.claimHandlers.add(handler);
|
|
174
176
|
return () => this.claimHandlers.delete(handler);
|
|
175
177
|
}
|
|
176
|
-
|
|
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") {
|
|
177
194
|
this.closed = true;
|
|
178
195
|
this.resolveRead?.(null);
|
|
179
196
|
this.resolveRead = null;
|
|
@@ -182,6 +199,7 @@ var ClientTransport = class {
|
|
|
182
199
|
this.queuedChars = 0;
|
|
183
200
|
this.handlers.clear();
|
|
184
201
|
this.claimHandlers.clear();
|
|
202
|
+
this.notifyClosed(reason);
|
|
185
203
|
const child = this.child;
|
|
186
204
|
if (!child) return;
|
|
187
205
|
treeKill(child);
|
|
@@ -193,7 +211,7 @@ var ClientTransport = class {
|
|
|
193
211
|
if (this.buffer.length > this.maxFrameChars) {
|
|
194
212
|
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
195
213
|
`);
|
|
196
|
-
this.stop();
|
|
214
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
197
215
|
return;
|
|
198
216
|
}
|
|
199
217
|
for (const raw of lines) {
|
|
@@ -201,7 +219,7 @@ var ClientTransport = class {
|
|
|
201
219
|
if (raw.length > this.maxFrameChars) {
|
|
202
220
|
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
203
221
|
`);
|
|
204
|
-
this.stop();
|
|
222
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
205
223
|
return;
|
|
206
224
|
}
|
|
207
225
|
try {
|
|
@@ -227,6 +245,7 @@ var ClientTransport = class {
|
|
|
227
245
|
writeErr(`[acp-child exited with code ${code}]
|
|
228
246
|
`);
|
|
229
247
|
}
|
|
248
|
+
this.notifyClosed(`agent process exited with code ${code}`);
|
|
230
249
|
}
|
|
231
250
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
232
251
|
if (this.resolveRead) {
|
|
@@ -373,6 +392,16 @@ function isAuthRequiredError(err) {
|
|
|
373
392
|
return false;
|
|
374
393
|
}
|
|
375
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
|
+
|
|
376
405
|
// src/client/acp-session-ops.ts
|
|
377
406
|
function filterMcpServers(agentCapabilities, servers) {
|
|
378
407
|
if (!servers || servers.length === 0) return [];
|
|
@@ -1650,7 +1679,7 @@ function isAllowed(decision) {
|
|
|
1650
1679
|
}
|
|
1651
1680
|
function toTrustBoundaryRequest(request, options) {
|
|
1652
1681
|
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
1653
|
-
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId :
|
|
1682
|
+
const sessionId = options.actor?.sessionId ?? (typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : void 0);
|
|
1654
1683
|
return {
|
|
1655
1684
|
version: 1,
|
|
1656
1685
|
requestId: String(request.toolCall.toolCallId),
|
|
@@ -1686,6 +1715,7 @@ function makeTrustBoundaryPermissionPolicy(options) {
|
|
|
1686
1715
|
var WebSocketClientTransport = class {
|
|
1687
1716
|
ws = null;
|
|
1688
1717
|
handlers = /* @__PURE__ */ new Set();
|
|
1718
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
1689
1719
|
closed = false;
|
|
1690
1720
|
opts;
|
|
1691
1721
|
maxBufferedBytes;
|
|
@@ -1712,6 +1742,7 @@ var WebSocketClientTransport = class {
|
|
|
1712
1742
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
1713
1743
|
return new Promise((resolve3, reject) => {
|
|
1714
1744
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
1745
|
+
ws.binaryType = "arraybuffer";
|
|
1715
1746
|
this.ws = ws;
|
|
1716
1747
|
const timer = setTimeout(() => {
|
|
1717
1748
|
const pending = this.pendingStart;
|
|
@@ -1737,7 +1768,7 @@ var WebSocketClientTransport = class {
|
|
|
1737
1768
|
ws.addEventListener("error", (ev) => {
|
|
1738
1769
|
const pending = this.pendingStart;
|
|
1739
1770
|
if (pending === null) {
|
|
1740
|
-
this.stop();
|
|
1771
|
+
this.stop("WebSocket error");
|
|
1741
1772
|
return;
|
|
1742
1773
|
}
|
|
1743
1774
|
this.pendingStart = null;
|
|
@@ -1758,6 +1789,7 @@ var WebSocketClientTransport = class {
|
|
|
1758
1789
|
clearTimeout(pending.timer);
|
|
1759
1790
|
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
1760
1791
|
}
|
|
1792
|
+
this.notifyClosed("WebSocket closed");
|
|
1761
1793
|
});
|
|
1762
1794
|
ws.addEventListener("message", (ev) => {
|
|
1763
1795
|
this.onData(ev.data);
|
|
@@ -1785,9 +1817,25 @@ var WebSocketClientTransport = class {
|
|
|
1785
1817
|
this.handlers.add(handler);
|
|
1786
1818
|
return () => this.handlers.delete(handler);
|
|
1787
1819
|
}
|
|
1788
|
-
|
|
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") {
|
|
1789
1836
|
this.closed = true;
|
|
1790
1837
|
this.handlers.clear();
|
|
1838
|
+
this.notifyClosed(reason);
|
|
1791
1839
|
if (this.pendingStart !== null) {
|
|
1792
1840
|
const pending = this.pendingStart;
|
|
1793
1841
|
this.pendingStart = null;
|
|
@@ -1808,7 +1856,7 @@ var WebSocketClientTransport = class {
|
|
|
1808
1856
|
onData(data) {
|
|
1809
1857
|
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
1810
1858
|
if (text.length > this.maxMessageChars) {
|
|
1811
|
-
this.stop();
|
|
1859
|
+
this.stop(`agent message exceeds ${this.maxMessageChars} characters`);
|
|
1812
1860
|
return;
|
|
1813
1861
|
}
|
|
1814
1862
|
if (!text.trim()) return;
|
|
@@ -1977,6 +2025,10 @@ var ACPSession = class _ACPSession {
|
|
|
1977
2025
|
}
|
|
1978
2026
|
const session = new _ACPSession(opts, transport);
|
|
1979
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
|
+
});
|
|
1980
2032
|
try {
|
|
1981
2033
|
await session.initialize();
|
|
1982
2034
|
} catch (err) {
|
|
@@ -2103,6 +2155,9 @@ var ACPSession = class _ACPSession {
|
|
|
2103
2155
|
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
2104
2156
|
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
2105
2157
|
}
|
|
2158
|
+
if (this.promptCallbackAbort) {
|
|
2159
|
+
throw new ACPSessionError("protocol_error", "prompt called while another prompt is running");
|
|
2160
|
+
}
|
|
2106
2161
|
if (signal.aborted) {
|
|
2107
2162
|
return emptyRunResult("cancelled");
|
|
2108
2163
|
}
|
|
@@ -2144,10 +2199,6 @@ var ACPSession = class _ACPSession {
|
|
|
2144
2199
|
createPromise,
|
|
2145
2200
|
new Promise((_, reject) => {
|
|
2146
2201
|
rejectCreate = reject;
|
|
2147
|
-
if (signal.aborted) {
|
|
2148
|
-
reject(new ACPSessionError("aborted", "prompt was aborted by the parent"));
|
|
2149
|
-
return;
|
|
2150
|
-
}
|
|
2151
2202
|
signal.addEventListener("abort", onCreateAbort, { once: true });
|
|
2152
2203
|
})
|
|
2153
2204
|
]);
|
|
@@ -2165,15 +2216,10 @@ var ACPSession = class _ACPSession {
|
|
|
2165
2216
|
}
|
|
2166
2217
|
signal.removeEventListener("abort", onCreateAbort);
|
|
2167
2218
|
rejectCreate = void 0;
|
|
2168
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2169
|
-
signal.removeEventListener("abort", onAbort);
|
|
2170
|
-
this.promptCallbackAbort?.abort();
|
|
2171
|
-
this.promptCallbackAbort = null;
|
|
2172
|
-
throw new ACPSessionError("protocol_error", "session/new returned no session id");
|
|
2173
|
-
}
|
|
2174
2219
|
this.sessionId = sessionId;
|
|
2175
2220
|
}
|
|
2176
2221
|
if (signal.aborted) {
|
|
2222
|
+
this.promptCallbackAbort = null;
|
|
2177
2223
|
return emptyRunResult("cancelled");
|
|
2178
2224
|
}
|
|
2179
2225
|
this.resetScratch();
|
|
@@ -2281,11 +2327,7 @@ var ACPSession = class _ACPSession {
|
|
|
2281
2327
|
} catch {
|
|
2282
2328
|
}
|
|
2283
2329
|
}
|
|
2284
|
-
|
|
2285
|
-
clearTimeout(p.timeoutHandle);
|
|
2286
|
-
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
2287
|
-
}
|
|
2288
|
-
this.pending.clear();
|
|
2330
|
+
rejectPendingRequests(this.pending);
|
|
2289
2331
|
this.transportOff?.();
|
|
2290
2332
|
this.transportOff = null;
|
|
2291
2333
|
try {
|
|
@@ -2384,6 +2426,8 @@ var ACPSession = class _ACPSession {
|
|
|
2384
2426
|
return;
|
|
2385
2427
|
}
|
|
2386
2428
|
if (msg.method === "session/update") {
|
|
2429
|
+
const updateSessionId = msg.params?.sessionId;
|
|
2430
|
+
if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
|
|
2387
2431
|
handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
|
|
2388
2432
|
return;
|
|
2389
2433
|
}
|
|
@@ -2501,6 +2545,7 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2501
2545
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
2502
2546
|
const persistent = options.persistent === true;
|
|
2503
2547
|
let shared = null;
|
|
2548
|
+
let turns = Promise.resolve();
|
|
2504
2549
|
const startSession = async () => {
|
|
2505
2550
|
return ACPSession.start({
|
|
2506
2551
|
command: options.command,
|
|
@@ -2516,10 +2561,17 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2516
2561
|
};
|
|
2517
2562
|
const runner = async (task, ctx) => {
|
|
2518
2563
|
let session;
|
|
2519
|
-
const reuse = persistent && shared !== null;
|
|
2520
2564
|
try {
|
|
2521
|
-
|
|
2522
|
-
|
|
2565
|
+
if (persistent) {
|
|
2566
|
+
shared ??= startSession();
|
|
2567
|
+
const starting = shared;
|
|
2568
|
+
session = await starting.catch((err) => {
|
|
2569
|
+
if (shared === starting) shared = null;
|
|
2570
|
+
throw err;
|
|
2571
|
+
});
|
|
2572
|
+
} else {
|
|
2573
|
+
session = await startSession();
|
|
2574
|
+
}
|
|
2523
2575
|
} catch (err) {
|
|
2524
2576
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2525
2577
|
}
|
|
@@ -2536,7 +2588,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2536
2588
|
};
|
|
2537
2589
|
let result;
|
|
2538
2590
|
try {
|
|
2539
|
-
|
|
2591
|
+
const turn = () => session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
2592
|
+
const run = persistent ? turns.then(turn, turn) : turn();
|
|
2593
|
+
if (persistent) turns = run;
|
|
2594
|
+
result = await run;
|
|
2540
2595
|
} catch (err) {
|
|
2541
2596
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
2542
2597
|
} finally {
|
|
@@ -2557,10 +2612,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
2557
2612
|
};
|
|
2558
2613
|
const stop = async () => {
|
|
2559
2614
|
if (shared) {
|
|
2560
|
-
const
|
|
2615
|
+
const starting = shared;
|
|
2561
2616
|
shared = null;
|
|
2562
2617
|
try {
|
|
2563
|
-
await
|
|
2618
|
+
await (await starting).close();
|
|
2564
2619
|
} catch {
|
|
2565
2620
|
}
|
|
2566
2621
|
}
|
package/dist/index.js
CHANGED
|
@@ -297,7 +297,7 @@ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
|
|
|
297
297
|
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
298
298
|
return false;
|
|
299
299
|
}
|
|
300
|
-
const candidateCwd = persisted.cwd ??
|
|
300
|
+
const candidateCwd = loadCwd ?? persisted.cwd ?? ctx.defaultCwd;
|
|
301
301
|
const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
|
|
302
302
|
const restored = {
|
|
303
303
|
id: sessionId,
|
|
@@ -478,6 +478,7 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
478
478
|
let result;
|
|
479
479
|
const pendingNotifications = [];
|
|
480
480
|
const emit = (update) => {
|
|
481
|
+
if (ctx.sessions.get(sessionId) !== session) return;
|
|
481
482
|
const notifPromise = ctx.sendNotification({ sessionId, update });
|
|
482
483
|
pendingNotifications.push(notifPromise.catch(() => {
|
|
483
484
|
}));
|
|
@@ -537,6 +538,7 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
537
538
|
modeId
|
|
538
539
|
}
|
|
539
540
|
});
|
|
541
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
540
542
|
await ctx.sendResult(id, {});
|
|
541
543
|
return false;
|
|
542
544
|
}
|
|
@@ -562,7 +564,7 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
562
564
|
configOptions: options
|
|
563
565
|
}
|
|
564
566
|
});
|
|
565
|
-
await ctx.persist(session);
|
|
567
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
566
568
|
await ctx.sendResult(id, { configOptions: options });
|
|
567
569
|
return false;
|
|
568
570
|
}
|
|
@@ -1170,6 +1172,7 @@ var ClientTransport = class {
|
|
|
1170
1172
|
buffer = "";
|
|
1171
1173
|
handlers = /* @__PURE__ */ new Set();
|
|
1172
1174
|
claimHandlers = /* @__PURE__ */ new Set();
|
|
1175
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
1173
1176
|
closed = false;
|
|
1174
1177
|
resolveRead = null;
|
|
1175
1178
|
messageQueue = [];
|
|
@@ -1277,6 +1280,7 @@ var ClientTransport = class {
|
|
|
1277
1280
|
}
|
|
1278
1281
|
send(msg) {
|
|
1279
1282
|
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
1283
|
+
if (this.closed) return Promise.reject(new Error("ClientTransport is closed"));
|
|
1280
1284
|
return new Promise((resolve5, reject) => {
|
|
1281
1285
|
const line = JSON.stringify(msg) + "\n";
|
|
1282
1286
|
this.child?.stdin.write(line, "utf8", (err) => {
|
|
@@ -1314,7 +1318,22 @@ var ClientTransport = class {
|
|
|
1314
1318
|
this.claimHandlers.add(handler);
|
|
1315
1319
|
return () => this.claimHandlers.delete(handler);
|
|
1316
1320
|
}
|
|
1317
|
-
|
|
1321
|
+
onClose(handler) {
|
|
1322
|
+
this.closeHandlers.add(handler);
|
|
1323
|
+
return () => this.closeHandlers.delete(handler);
|
|
1324
|
+
}
|
|
1325
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
1326
|
+
notifyClosed(reason) {
|
|
1327
|
+
const handlers = [...this.closeHandlers];
|
|
1328
|
+
this.closeHandlers.clear();
|
|
1329
|
+
for (const handler of handlers) {
|
|
1330
|
+
try {
|
|
1331
|
+
handler(reason);
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
stop(reason = "transport stopped") {
|
|
1318
1337
|
this.closed = true;
|
|
1319
1338
|
this.resolveRead?.(null);
|
|
1320
1339
|
this.resolveRead = null;
|
|
@@ -1323,6 +1342,7 @@ var ClientTransport = class {
|
|
|
1323
1342
|
this.queuedChars = 0;
|
|
1324
1343
|
this.handlers.clear();
|
|
1325
1344
|
this.claimHandlers.clear();
|
|
1345
|
+
this.notifyClosed(reason);
|
|
1326
1346
|
const child = this.child;
|
|
1327
1347
|
if (!child) return;
|
|
1328
1348
|
treeKill(child);
|
|
@@ -1334,7 +1354,7 @@ var ClientTransport = class {
|
|
|
1334
1354
|
if (this.buffer.length > this.maxFrameChars) {
|
|
1335
1355
|
writeErr(`[acp-child pending frame exceeds ${this.maxFrameChars} characters]
|
|
1336
1356
|
`);
|
|
1337
|
-
this.stop();
|
|
1357
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
1338
1358
|
return;
|
|
1339
1359
|
}
|
|
1340
1360
|
for (const raw of lines) {
|
|
@@ -1342,7 +1362,7 @@ var ClientTransport = class {
|
|
|
1342
1362
|
if (raw.length > this.maxFrameChars) {
|
|
1343
1363
|
writeErr(`[acp-child frame exceeds ${this.maxFrameChars} characters]
|
|
1344
1364
|
`);
|
|
1345
|
-
this.stop();
|
|
1365
|
+
this.stop(`agent frame exceeds ${this.maxFrameChars} characters`);
|
|
1346
1366
|
return;
|
|
1347
1367
|
}
|
|
1348
1368
|
try {
|
|
@@ -1368,6 +1388,7 @@ var ClientTransport = class {
|
|
|
1368
1388
|
writeErr(`[acp-child exited with code ${code}]
|
|
1369
1389
|
`);
|
|
1370
1390
|
}
|
|
1391
|
+
this.notifyClosed(`agent process exited with code ${code}`);
|
|
1371
1392
|
}
|
|
1372
1393
|
dispatch(msg, chars = JSON.stringify(msg).length) {
|
|
1373
1394
|
if (this.resolveRead) {
|
|
@@ -1643,16 +1664,17 @@ var WrongStackACPServer = class {
|
|
|
1643
1664
|
return;
|
|
1644
1665
|
}
|
|
1645
1666
|
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1646
|
-
|
|
1667
|
+
const chunks = [];
|
|
1647
1668
|
let bodyBytes = 0;
|
|
1648
1669
|
let tooLarge = false;
|
|
1649
1670
|
for await (const chunk of req) {
|
|
1650
1671
|
bodyBytes += chunk.length;
|
|
1651
1672
|
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1652
1673
|
tooLarge = true;
|
|
1653
|
-
|
|
1674
|
+
chunks.length = 0;
|
|
1675
|
+
if (bodyBytes > 2 * MAX_HTTP_BODY) break;
|
|
1654
1676
|
}
|
|
1655
|
-
|
|
1677
|
+
if (!tooLarge) chunks.push(chunk);
|
|
1656
1678
|
}
|
|
1657
1679
|
if (tooLarge) {
|
|
1658
1680
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
@@ -1661,7 +1683,7 @@ var WrongStackACPServer = class {
|
|
|
1661
1683
|
}
|
|
1662
1684
|
let msg;
|
|
1663
1685
|
try {
|
|
1664
|
-
msg = JSON.parse(
|
|
1686
|
+
msg = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1665
1687
|
} catch {
|
|
1666
1688
|
res.writeHead(400);
|
|
1667
1689
|
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
@@ -1895,6 +1917,16 @@ function isAuthRequiredError(err) {
|
|
|
1895
1917
|
return false;
|
|
1896
1918
|
}
|
|
1897
1919
|
|
|
1920
|
+
// src/client/acp-request-state.ts
|
|
1921
|
+
function rejectPendingRequests(pending, reason) {
|
|
1922
|
+
for (const p of pending.values()) {
|
|
1923
|
+
clearTimeout(p.timeoutHandle);
|
|
1924
|
+
const message = reason === void 0 ? "session was closed" : `agent connection closed (${reason}) during ${p.method}`;
|
|
1925
|
+
p.reject(new ACPSessionError("closed", message));
|
|
1926
|
+
}
|
|
1927
|
+
pending.clear();
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1898
1930
|
// src/client/acp-session-ops.ts
|
|
1899
1931
|
function filterMcpServers(agentCapabilities, servers) {
|
|
1900
1932
|
if (!servers || servers.length === 0) return [];
|
|
@@ -3172,7 +3204,7 @@ function isAllowed(decision) {
|
|
|
3172
3204
|
}
|
|
3173
3205
|
function toTrustBoundaryRequest(request, options) {
|
|
3174
3206
|
const rawSessionId = request.toolCall.rawInput?.sessionId;
|
|
3175
|
-
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId :
|
|
3207
|
+
const sessionId = options.actor?.sessionId ?? (typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : void 0);
|
|
3176
3208
|
return {
|
|
3177
3209
|
version: 1,
|
|
3178
3210
|
requestId: String(request.toolCall.toolCallId),
|
|
@@ -3208,6 +3240,7 @@ function makeTrustBoundaryPermissionPolicy(options) {
|
|
|
3208
3240
|
var WebSocketClientTransport = class {
|
|
3209
3241
|
ws = null;
|
|
3210
3242
|
handlers = /* @__PURE__ */ new Set();
|
|
3243
|
+
closeHandlers = /* @__PURE__ */ new Set();
|
|
3211
3244
|
closed = false;
|
|
3212
3245
|
opts;
|
|
3213
3246
|
maxBufferedBytes;
|
|
@@ -3234,6 +3267,7 @@ var WebSocketClientTransport = class {
|
|
|
3234
3267
|
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
3235
3268
|
return new Promise((resolve5, reject) => {
|
|
3236
3269
|
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
3270
|
+
ws.binaryType = "arraybuffer";
|
|
3237
3271
|
this.ws = ws;
|
|
3238
3272
|
const timer = setTimeout(() => {
|
|
3239
3273
|
const pending = this.pendingStart;
|
|
@@ -3259,7 +3293,7 @@ var WebSocketClientTransport = class {
|
|
|
3259
3293
|
ws.addEventListener("error", (ev) => {
|
|
3260
3294
|
const pending = this.pendingStart;
|
|
3261
3295
|
if (pending === null) {
|
|
3262
|
-
this.stop();
|
|
3296
|
+
this.stop("WebSocket error");
|
|
3263
3297
|
return;
|
|
3264
3298
|
}
|
|
3265
3299
|
this.pendingStart = null;
|
|
@@ -3280,6 +3314,7 @@ var WebSocketClientTransport = class {
|
|
|
3280
3314
|
clearTimeout(pending.timer);
|
|
3281
3315
|
pending.reject(new Error("WebSocket closed before the connection opened"));
|
|
3282
3316
|
}
|
|
3317
|
+
this.notifyClosed("WebSocket closed");
|
|
3283
3318
|
});
|
|
3284
3319
|
ws.addEventListener("message", (ev) => {
|
|
3285
3320
|
this.onData(ev.data);
|
|
@@ -3307,9 +3342,25 @@ var WebSocketClientTransport = class {
|
|
|
3307
3342
|
this.handlers.add(handler);
|
|
3308
3343
|
return () => this.handlers.delete(handler);
|
|
3309
3344
|
}
|
|
3310
|
-
|
|
3345
|
+
onClose(handler) {
|
|
3346
|
+
this.closeHandlers.add(handler);
|
|
3347
|
+
return () => this.closeHandlers.delete(handler);
|
|
3348
|
+
}
|
|
3349
|
+
/** Tell close listeners once; the handlers are dropped with the connection. */
|
|
3350
|
+
notifyClosed(reason) {
|
|
3351
|
+
const handlers = [...this.closeHandlers];
|
|
3352
|
+
this.closeHandlers.clear();
|
|
3353
|
+
for (const handler of handlers) {
|
|
3354
|
+
try {
|
|
3355
|
+
handler(reason);
|
|
3356
|
+
} catch {
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
stop(reason = "transport stopped") {
|
|
3311
3361
|
this.closed = true;
|
|
3312
3362
|
this.handlers.clear();
|
|
3363
|
+
this.notifyClosed(reason);
|
|
3313
3364
|
if (this.pendingStart !== null) {
|
|
3314
3365
|
const pending = this.pendingStart;
|
|
3315
3366
|
this.pendingStart = null;
|
|
@@ -3330,7 +3381,7 @@ var WebSocketClientTransport = class {
|
|
|
3330
3381
|
onData(data) {
|
|
3331
3382
|
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
3332
3383
|
if (text.length > this.maxMessageChars) {
|
|
3333
|
-
this.stop();
|
|
3384
|
+
this.stop(`agent message exceeds ${this.maxMessageChars} characters`);
|
|
3334
3385
|
return;
|
|
3335
3386
|
}
|
|
3336
3387
|
if (!text.trim()) return;
|
|
@@ -3499,6 +3550,10 @@ var ACPSession = class _ACPSession {
|
|
|
3499
3550
|
}
|
|
3500
3551
|
const session = new _ACPSession(opts, transport);
|
|
3501
3552
|
session.transportOff = transport.onMessage((msg) => session.handleMessage(msg));
|
|
3553
|
+
transport.onClose?.((reason) => {
|
|
3554
|
+
if (!session.closed) rejectPendingRequests(session.pending, reason);
|
|
3555
|
+
void session.close();
|
|
3556
|
+
});
|
|
3502
3557
|
try {
|
|
3503
3558
|
await session.initialize();
|
|
3504
3559
|
} catch (err) {
|
|
@@ -3625,6 +3680,9 @@ var ACPSession = class _ACPSession {
|
|
|
3625
3680
|
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
3626
3681
|
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
3627
3682
|
}
|
|
3683
|
+
if (this.promptCallbackAbort) {
|
|
3684
|
+
throw new ACPSessionError("protocol_error", "prompt called while another prompt is running");
|
|
3685
|
+
}
|
|
3628
3686
|
if (signal.aborted) {
|
|
3629
3687
|
return emptyRunResult("cancelled");
|
|
3630
3688
|
}
|
|
@@ -3666,10 +3724,6 @@ var ACPSession = class _ACPSession {
|
|
|
3666
3724
|
createPromise,
|
|
3667
3725
|
new Promise((_, reject) => {
|
|
3668
3726
|
rejectCreate = reject;
|
|
3669
|
-
if (signal.aborted) {
|
|
3670
|
-
reject(new ACPSessionError("aborted", "prompt was aborted by the parent"));
|
|
3671
|
-
return;
|
|
3672
|
-
}
|
|
3673
3727
|
signal.addEventListener("abort", onCreateAbort, { once: true });
|
|
3674
3728
|
})
|
|
3675
3729
|
]);
|
|
@@ -3687,15 +3741,10 @@ var ACPSession = class _ACPSession {
|
|
|
3687
3741
|
}
|
|
3688
3742
|
signal.removeEventListener("abort", onCreateAbort);
|
|
3689
3743
|
rejectCreate = void 0;
|
|
3690
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
3691
|
-
signal.removeEventListener("abort", onAbort);
|
|
3692
|
-
this.promptCallbackAbort?.abort();
|
|
3693
|
-
this.promptCallbackAbort = null;
|
|
3694
|
-
throw new ACPSessionError("protocol_error", "session/new returned no session id");
|
|
3695
|
-
}
|
|
3696
3744
|
this.sessionId = sessionId;
|
|
3697
3745
|
}
|
|
3698
3746
|
if (signal.aborted) {
|
|
3747
|
+
this.promptCallbackAbort = null;
|
|
3699
3748
|
return emptyRunResult("cancelled");
|
|
3700
3749
|
}
|
|
3701
3750
|
this.resetScratch();
|
|
@@ -3803,11 +3852,7 @@ var ACPSession = class _ACPSession {
|
|
|
3803
3852
|
} catch {
|
|
3804
3853
|
}
|
|
3805
3854
|
}
|
|
3806
|
-
|
|
3807
|
-
clearTimeout(p.timeoutHandle);
|
|
3808
|
-
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
3809
|
-
}
|
|
3810
|
-
this.pending.clear();
|
|
3855
|
+
rejectPendingRequests(this.pending);
|
|
3811
3856
|
this.transportOff?.();
|
|
3812
3857
|
this.transportOff = null;
|
|
3813
3858
|
try {
|
|
@@ -3906,6 +3951,8 @@ var ACPSession = class _ACPSession {
|
|
|
3906
3951
|
return;
|
|
3907
3952
|
}
|
|
3908
3953
|
if (msg.method === "session/update") {
|
|
3954
|
+
const updateSessionId = msg.params?.sessionId;
|
|
3955
|
+
if (typeof updateSessionId === "string" && updateSessionId !== this.sessionId) return;
|
|
3909
3956
|
handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
|
|
3910
3957
|
return;
|
|
3911
3958
|
}
|
|
@@ -4565,6 +4612,7 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4565
4612
|
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
4566
4613
|
const persistent = options.persistent === true;
|
|
4567
4614
|
let shared = null;
|
|
4615
|
+
let turns = Promise.resolve();
|
|
4568
4616
|
const startSession = async () => {
|
|
4569
4617
|
return ACPSession.start({
|
|
4570
4618
|
command: options.command,
|
|
@@ -4580,10 +4628,17 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4580
4628
|
};
|
|
4581
4629
|
const runner = async (task, ctx) => {
|
|
4582
4630
|
let session;
|
|
4583
|
-
const reuse = persistent && shared !== null;
|
|
4584
4631
|
try {
|
|
4585
|
-
|
|
4586
|
-
|
|
4632
|
+
if (persistent) {
|
|
4633
|
+
shared ??= startSession();
|
|
4634
|
+
const starting = shared;
|
|
4635
|
+
session = await starting.catch((err) => {
|
|
4636
|
+
if (shared === starting) shared = null;
|
|
4637
|
+
throw err;
|
|
4638
|
+
});
|
|
4639
|
+
} else {
|
|
4640
|
+
session = await startSession();
|
|
4641
|
+
}
|
|
4587
4642
|
} catch (err) {
|
|
4588
4643
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
4589
4644
|
}
|
|
@@ -4600,7 +4655,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4600
4655
|
};
|
|
4601
4656
|
let result;
|
|
4602
4657
|
try {
|
|
4603
|
-
|
|
4658
|
+
const turn = () => session.prompt([textContent(task.description)], ctx.signal, onProgress);
|
|
4659
|
+
const run = persistent ? turns.then(turn, turn) : turn();
|
|
4660
|
+
if (persistent) turns = run;
|
|
4661
|
+
result = await run;
|
|
4604
4662
|
} catch (err) {
|
|
4605
4663
|
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
4606
4664
|
} finally {
|
|
@@ -4621,10 +4679,10 @@ async function makeACPSubagentRunnerWithStop(options) {
|
|
|
4621
4679
|
};
|
|
4622
4680
|
const stop = async () => {
|
|
4623
4681
|
if (shared) {
|
|
4624
|
-
const
|
|
4682
|
+
const starting = shared;
|
|
4625
4683
|
shared = null;
|
|
4626
4684
|
try {
|
|
4627
|
-
await
|
|
4685
|
+
await (await starting).close();
|
|
4628
4686
|
} catch {
|
|
4629
4687
|
}
|
|
4630
4688
|
}
|
|
@@ -301,7 +301,7 @@ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
|
|
|
301
301
|
await ctx.sendError(id, -32602, `cwd must be an absolute path to an existing directory`);
|
|
302
302
|
return false;
|
|
303
303
|
}
|
|
304
|
-
const candidateCwd = persisted.cwd ??
|
|
304
|
+
const candidateCwd = loadCwd ?? persisted.cwd ?? ctx.defaultCwd;
|
|
305
305
|
const restoredCwd = await resolveSessionCwd(candidateCwd) ?? ctx.defaultCwd;
|
|
306
306
|
const restored = {
|
|
307
307
|
id: sessionId,
|
|
@@ -482,6 +482,7 @@ async function handleSessionPromptOp(ctx, id, params) {
|
|
|
482
482
|
let result;
|
|
483
483
|
const pendingNotifications = [];
|
|
484
484
|
const emit = (update) => {
|
|
485
|
+
if (ctx.sessions.get(sessionId) !== session) return;
|
|
485
486
|
const notifPromise = ctx.sendNotification({ sessionId, update });
|
|
486
487
|
pendingNotifications.push(notifPromise.catch(() => {
|
|
487
488
|
}));
|
|
@@ -541,6 +542,7 @@ async function handleSetModeOp(ctx, id, params) {
|
|
|
541
542
|
modeId
|
|
542
543
|
}
|
|
543
544
|
});
|
|
545
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
544
546
|
await ctx.sendResult(id, {});
|
|
545
547
|
return false;
|
|
546
548
|
}
|
|
@@ -566,7 +568,7 @@ async function handleSetConfigOptionOp(ctx, id, params) {
|
|
|
566
568
|
configOptions: options
|
|
567
569
|
}
|
|
568
570
|
});
|
|
569
|
-
await ctx.persist(session);
|
|
571
|
+
if (ctx.sessions.get(session.id) === session) await ctx.persist(session);
|
|
570
572
|
await ctx.sendResult(id, { configOptions: options });
|
|
571
573
|
return false;
|
|
572
574
|
}
|
|
@@ -1276,16 +1278,17 @@ var WrongStackACPServer = class {
|
|
|
1276
1278
|
return;
|
|
1277
1279
|
}
|
|
1278
1280
|
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1279
|
-
|
|
1281
|
+
const chunks = [];
|
|
1280
1282
|
let bodyBytes = 0;
|
|
1281
1283
|
let tooLarge = false;
|
|
1282
1284
|
for await (const chunk of req) {
|
|
1283
1285
|
bodyBytes += chunk.length;
|
|
1284
1286
|
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1285
1287
|
tooLarge = true;
|
|
1286
|
-
|
|
1288
|
+
chunks.length = 0;
|
|
1289
|
+
if (bodyBytes > 2 * MAX_HTTP_BODY) break;
|
|
1287
1290
|
}
|
|
1288
|
-
|
|
1291
|
+
if (!tooLarge) chunks.push(chunk);
|
|
1289
1292
|
}
|
|
1290
1293
|
if (tooLarge) {
|
|
1291
1294
|
res.writeHead(413, { "Content-Type": "application/json" });
|
|
@@ -1294,7 +1297,7 @@ var WrongStackACPServer = class {
|
|
|
1294
1297
|
}
|
|
1295
1298
|
let msg;
|
|
1296
1299
|
try {
|
|
1297
|
-
msg = JSON.parse(
|
|
1300
|
+
msg = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1298
1301
|
} catch {
|
|
1299
1302
|
res.writeHead(400);
|
|
1300
1303
|
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.29",
|
|
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.29",
|
|
56
|
+
"@wrongstack/primitives": "1.0.29"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^26.5.1",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "node ../../scripts/build-package.mjs",
|
|
68
68
|
"typecheck": "tsc --noEmit",
|
|
69
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
|
|
69
70
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
70
71
|
"test": "vitest run",
|
|
71
72
|
"smoke": "pnpm build && node --experimental-strip-types ../../scripts/acp-smoke-test.mts"
|