acpx 0.1.15 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -16
- package/dist/acp-jsonrpc-CGT_1Mel.js +55 -0
- package/dist/acp-jsonrpc-CGT_1Mel.js.map +1 -0
- package/dist/cli.d.ts +93 -86
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1275 -8138
- package/dist/cli.js.map +1 -0
- package/dist/output-BgMdEq3x.js +572 -0
- package/dist/output-BgMdEq3x.js.map +1 -0
- package/dist/output-render-Cvz0eKSb.js +140 -0
- package/dist/output-render-Cvz0eKSb.js.map +1 -0
- package/dist/queue-ipc-C8StWiZt.js +1571 -0
- package/dist/queue-ipc-C8StWiZt.js.map +1 -0
- package/dist/rolldown-runtime-CjeV3_4I.js +18 -0
- package/dist/runtime-session-id-B03l5p1Q.js +32 -0
- package/dist/runtime-session-id-B03l5p1Q.js.map +1 -0
- package/dist/session-C6nyqSfk.js +3738 -0
- package/dist/session-C6nyqSfk.js.map +1 -0
- package/package.json +58 -50
- package/skills/acpx/SKILL.md +11 -4
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
|
|
2
|
+
import { t as isAcpJsonRpcMessage } from "./acp-jsonrpc-CGT_1Mel.js";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
|
|
9
|
+
//#region src/errors.ts
|
|
10
|
+
var AcpxOperationalError = class extends Error {
|
|
11
|
+
outputCode;
|
|
12
|
+
detailCode;
|
|
13
|
+
origin;
|
|
14
|
+
retryable;
|
|
15
|
+
acp;
|
|
16
|
+
outputAlreadyEmitted;
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.name = new.target.name;
|
|
20
|
+
this.outputCode = options?.outputCode;
|
|
21
|
+
this.detailCode = options?.detailCode;
|
|
22
|
+
this.origin = options?.origin;
|
|
23
|
+
this.retryable = options?.retryable;
|
|
24
|
+
this.acp = options?.acp;
|
|
25
|
+
this.outputAlreadyEmitted = options?.outputAlreadyEmitted;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var SessionNotFoundError = class extends AcpxOperationalError {
|
|
29
|
+
sessionId;
|
|
30
|
+
constructor(sessionId) {
|
|
31
|
+
super(`Session not found: ${sessionId}`);
|
|
32
|
+
this.sessionId = sessionId;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SessionResolutionError = class extends AcpxOperationalError {};
|
|
36
|
+
var AgentSpawnError = class extends AcpxOperationalError {
|
|
37
|
+
agentCommand;
|
|
38
|
+
constructor(agentCommand, cause) {
|
|
39
|
+
super(`Failed to spawn agent command: ${agentCommand}`, { cause: cause instanceof Error ? cause : void 0 });
|
|
40
|
+
this.agentCommand = agentCommand;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var GeminiAcpStartupTimeoutError = class extends AcpxOperationalError {
|
|
44
|
+
constructor(message, options) {
|
|
45
|
+
super(message, {
|
|
46
|
+
outputCode: "TIMEOUT",
|
|
47
|
+
detailCode: "GEMINI_ACP_STARTUP_TIMEOUT",
|
|
48
|
+
origin: "acp",
|
|
49
|
+
...options
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var SessionModeReplayError = class extends AcpxOperationalError {
|
|
54
|
+
constructor(message, options) {
|
|
55
|
+
super(message, {
|
|
56
|
+
outputCode: "RUNTIME",
|
|
57
|
+
detailCode: "SESSION_MODE_REPLAY_FAILED",
|
|
58
|
+
origin: "acp",
|
|
59
|
+
...options
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var ClaudeAcpSessionCreateTimeoutError = class extends AcpxOperationalError {
|
|
64
|
+
constructor(message, options) {
|
|
65
|
+
super(message, {
|
|
66
|
+
outputCode: "TIMEOUT",
|
|
67
|
+
detailCode: "CLAUDE_ACP_SESSION_CREATE_TIMEOUT",
|
|
68
|
+
origin: "acp",
|
|
69
|
+
...options
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var CopilotAcpUnsupportedError = class extends AcpxOperationalError {
|
|
74
|
+
constructor(message, options) {
|
|
75
|
+
super(message, {
|
|
76
|
+
outputCode: "RUNTIME",
|
|
77
|
+
detailCode: "COPILOT_ACP_UNSUPPORTED",
|
|
78
|
+
origin: "acp",
|
|
79
|
+
...options
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var AuthPolicyError = class extends AcpxOperationalError {
|
|
84
|
+
constructor(message, options) {
|
|
85
|
+
super(message, {
|
|
86
|
+
outputCode: "RUNTIME",
|
|
87
|
+
detailCode: "AUTH_REQUIRED",
|
|
88
|
+
origin: "acp",
|
|
89
|
+
...options
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var QueueConnectionError = class extends AcpxOperationalError {};
|
|
94
|
+
var QueueProtocolError = class extends AcpxOperationalError {};
|
|
95
|
+
var PermissionDeniedError = class extends AcpxOperationalError {};
|
|
96
|
+
var PermissionPromptUnavailableError = class extends AcpxOperationalError {
|
|
97
|
+
constructor() {
|
|
98
|
+
super("Permission prompt unavailable in non-interactive mode");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/prompt-content.ts
|
|
104
|
+
function asRecord$3(value) {
|
|
105
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function isTextBlock(value) {
|
|
109
|
+
const record = asRecord$3(value);
|
|
110
|
+
return record?.type === "text" && typeof record.text === "string";
|
|
111
|
+
}
|
|
112
|
+
function isImageBlock(value) {
|
|
113
|
+
const record = asRecord$3(value);
|
|
114
|
+
return record?.type === "image" && typeof record.mimeType === "string" && typeof record.data === "string";
|
|
115
|
+
}
|
|
116
|
+
function isResourceLinkBlock(value) {
|
|
117
|
+
const record = asRecord$3(value);
|
|
118
|
+
return record?.type === "resource_link" && typeof record.uri === "string" && (record.title === void 0 || typeof record.title === "string") && (record.name === void 0 || typeof record.name === "string");
|
|
119
|
+
}
|
|
120
|
+
function isResourcePayload(value) {
|
|
121
|
+
const record = asRecord$3(value);
|
|
122
|
+
if (!record || typeof record.uri !== "string") return false;
|
|
123
|
+
return record.text === void 0 || typeof record.text === "string";
|
|
124
|
+
}
|
|
125
|
+
function isResourceBlock(value) {
|
|
126
|
+
const record = asRecord$3(value);
|
|
127
|
+
return record?.type === "resource" && isResourcePayload(record.resource);
|
|
128
|
+
}
|
|
129
|
+
function isContentBlock(value) {
|
|
130
|
+
return isTextBlock(value) || isImageBlock(value) || isResourceLinkBlock(value) || isResourceBlock(value);
|
|
131
|
+
}
|
|
132
|
+
function isPromptInput(value) {
|
|
133
|
+
return Array.isArray(value) && value.every((entry) => isContentBlock(entry));
|
|
134
|
+
}
|
|
135
|
+
function textPrompt(text) {
|
|
136
|
+
return [{
|
|
137
|
+
type: "text",
|
|
138
|
+
text
|
|
139
|
+
}];
|
|
140
|
+
}
|
|
141
|
+
function parseStructuredPrompt(source) {
|
|
142
|
+
if (!source.startsWith("[")) return;
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(source);
|
|
145
|
+
return isPromptInput(parsed) ? parsed : void 0;
|
|
146
|
+
} catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function parsePromptSource(source) {
|
|
151
|
+
const trimmed = source.trim();
|
|
152
|
+
const structured = parseStructuredPrompt(trimmed);
|
|
153
|
+
if (structured) return structured;
|
|
154
|
+
if (!trimmed) return [];
|
|
155
|
+
return textPrompt(trimmed);
|
|
156
|
+
}
|
|
157
|
+
function mergePromptSourceWithText(source, suffixText) {
|
|
158
|
+
const prompt = parsePromptSource(source);
|
|
159
|
+
const appended = suffixText.trim();
|
|
160
|
+
if (!appended) return prompt;
|
|
161
|
+
if (prompt.length === 0) return textPrompt(appended);
|
|
162
|
+
return [...prompt, ...textPrompt(appended)];
|
|
163
|
+
}
|
|
164
|
+
function promptToDisplayText(prompt) {
|
|
165
|
+
return prompt.map((block) => {
|
|
166
|
+
switch (block.type) {
|
|
167
|
+
case "text": return block.text;
|
|
168
|
+
case "resource_link": return block.title ?? block.name ?? block.uri;
|
|
169
|
+
case "resource": return "text" in block.resource && typeof block.resource.text === "string" ? block.resource.text : block.resource.uri;
|
|
170
|
+
case "image": return `[image] ${block.mimeType}`;
|
|
171
|
+
default: return "";
|
|
172
|
+
}
|
|
173
|
+
}).filter((entry) => entry.trim().length > 0).join("\n\n").trim();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/acp-error-shapes.ts
|
|
178
|
+
const RESOURCE_NOT_FOUND_ACP_CODES = new Set([-32001, -32002]);
|
|
179
|
+
function asRecord$2(value) {
|
|
180
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function toAcpErrorPayload(value) {
|
|
184
|
+
const record = asRecord$2(value);
|
|
185
|
+
if (!record) return;
|
|
186
|
+
if (typeof record.code !== "number" || !Number.isFinite(record.code)) return;
|
|
187
|
+
if (typeof record.message !== "string" || record.message.length === 0) return;
|
|
188
|
+
return {
|
|
189
|
+
code: record.code,
|
|
190
|
+
message: record.message,
|
|
191
|
+
data: record.data
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function extractAcpErrorInternal(value, depth) {
|
|
195
|
+
if (depth > 5) return;
|
|
196
|
+
const direct = toAcpErrorPayload(value);
|
|
197
|
+
if (direct) return direct;
|
|
198
|
+
const record = asRecord$2(value);
|
|
199
|
+
if (!record) return;
|
|
200
|
+
if ("error" in record) {
|
|
201
|
+
const nested = extractAcpErrorInternal(record.error, depth + 1);
|
|
202
|
+
if (nested) return nested;
|
|
203
|
+
}
|
|
204
|
+
if ("cause" in record) {
|
|
205
|
+
const nested = extractAcpErrorInternal(record.cause, depth + 1);
|
|
206
|
+
if (nested) return nested;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function formatUnknownErrorMessage(error) {
|
|
210
|
+
if (error instanceof Error) return error.message;
|
|
211
|
+
if (error && typeof error === "object") {
|
|
212
|
+
const maybeMessage = error.message;
|
|
213
|
+
if (typeof maybeMessage === "string" && maybeMessage.length > 0) return maybeMessage;
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(error);
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
return String(error);
|
|
219
|
+
}
|
|
220
|
+
function isSessionNotFoundText(value) {
|
|
221
|
+
if (typeof value !== "string") return false;
|
|
222
|
+
const normalized = value.toLowerCase();
|
|
223
|
+
return normalized.includes("resource_not_found") || normalized.includes("resource not found") || normalized.includes("session not found") || normalized.includes("unknown session") || normalized.includes("invalid session identifier");
|
|
224
|
+
}
|
|
225
|
+
function hasSessionNotFoundHint(value, depth = 0) {
|
|
226
|
+
if (depth > 4) return false;
|
|
227
|
+
if (isSessionNotFoundText(value)) return true;
|
|
228
|
+
if (Array.isArray(value)) return value.some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
229
|
+
const record = asRecord$2(value);
|
|
230
|
+
if (!record) return false;
|
|
231
|
+
return Object.values(record).some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
232
|
+
}
|
|
233
|
+
function extractAcpError(error) {
|
|
234
|
+
return extractAcpErrorInternal(error, 0);
|
|
235
|
+
}
|
|
236
|
+
function isAcpResourceNotFoundError(error) {
|
|
237
|
+
const acp = extractAcpError(error);
|
|
238
|
+
if (acp && RESOURCE_NOT_FOUND_ACP_CODES.has(acp.code)) return true;
|
|
239
|
+
if (acp) {
|
|
240
|
+
if (isSessionNotFoundText(acp.message)) return true;
|
|
241
|
+
if (hasSessionNotFoundHint(acp.data)) return true;
|
|
242
|
+
}
|
|
243
|
+
return isSessionNotFoundText(formatUnknownErrorMessage(error));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/types.ts
|
|
248
|
+
const EXIT_CODES = {
|
|
249
|
+
SUCCESS: 0,
|
|
250
|
+
ERROR: 1,
|
|
251
|
+
USAGE: 2,
|
|
252
|
+
TIMEOUT: 3,
|
|
253
|
+
NO_SESSION: 4,
|
|
254
|
+
PERMISSION_DENIED: 5,
|
|
255
|
+
INTERRUPTED: 130
|
|
256
|
+
};
|
|
257
|
+
const OUTPUT_FORMATS = [
|
|
258
|
+
"text",
|
|
259
|
+
"json",
|
|
260
|
+
"quiet"
|
|
261
|
+
];
|
|
262
|
+
const AUTH_POLICIES = ["skip", "fail"];
|
|
263
|
+
const NON_INTERACTIVE_PERMISSION_POLICIES = ["deny", "fail"];
|
|
264
|
+
const OUTPUT_ERROR_CODES = [
|
|
265
|
+
"NO_SESSION",
|
|
266
|
+
"TIMEOUT",
|
|
267
|
+
"PERMISSION_DENIED",
|
|
268
|
+
"PERMISSION_PROMPT_UNAVAILABLE",
|
|
269
|
+
"RUNTIME",
|
|
270
|
+
"USAGE"
|
|
271
|
+
];
|
|
272
|
+
const OUTPUT_ERROR_ORIGINS = [
|
|
273
|
+
"cli",
|
|
274
|
+
"runtime",
|
|
275
|
+
"queue",
|
|
276
|
+
"acp"
|
|
277
|
+
];
|
|
278
|
+
const SESSION_RECORD_SCHEMA = "acpx.session.v1";
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/error-normalization.ts
|
|
282
|
+
const AUTH_REQUIRED_ACP_CODES = new Set([-32e3]);
|
|
283
|
+
const QUERY_CLOSED_BEFORE_RESPONSE_DETAIL = "query closed before response received";
|
|
284
|
+
function asRecord$1(value) {
|
|
285
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
function isAuthRequiredMessage(value) {
|
|
289
|
+
if (!value) return false;
|
|
290
|
+
const normalized = value.toLowerCase();
|
|
291
|
+
return normalized.includes("auth required") || normalized.includes("authentication required") || normalized.includes("authorization required") || normalized.includes("credential required") || normalized.includes("credentials required") || normalized.includes("token required") || normalized.includes("login required");
|
|
292
|
+
}
|
|
293
|
+
function isAcpAuthRequiredPayload(acp) {
|
|
294
|
+
if (!acp) return false;
|
|
295
|
+
if (!AUTH_REQUIRED_ACP_CODES.has(acp.code)) return false;
|
|
296
|
+
if (isAuthRequiredMessage(acp.message)) return true;
|
|
297
|
+
const data = asRecord$1(acp.data);
|
|
298
|
+
if (!data) return false;
|
|
299
|
+
if (data.authRequired === true) return true;
|
|
300
|
+
const methodId = data.methodId;
|
|
301
|
+
if (typeof methodId === "string" && methodId.trim().length > 0) return true;
|
|
302
|
+
const methods = data.methods;
|
|
303
|
+
if (Array.isArray(methods) && methods.length > 0) return true;
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
function isOutputErrorCode$1(value) {
|
|
307
|
+
return typeof value === "string" && OUTPUT_ERROR_CODES.includes(value);
|
|
308
|
+
}
|
|
309
|
+
function isOutputErrorOrigin$1(value) {
|
|
310
|
+
return typeof value === "string" && OUTPUT_ERROR_ORIGINS.includes(value);
|
|
311
|
+
}
|
|
312
|
+
function readOutputErrorMeta(error) {
|
|
313
|
+
const record = asRecord$1(error);
|
|
314
|
+
if (!record) return {};
|
|
315
|
+
return {
|
|
316
|
+
outputCode: isOutputErrorCode$1(record.outputCode) ? record.outputCode : void 0,
|
|
317
|
+
detailCode: typeof record.detailCode === "string" && record.detailCode.trim().length > 0 ? record.detailCode : void 0,
|
|
318
|
+
origin: isOutputErrorOrigin$1(record.origin) ? record.origin : void 0,
|
|
319
|
+
retryable: typeof record.retryable === "boolean" ? record.retryable : void 0,
|
|
320
|
+
acp: extractAcpError(record.acp)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function isTimeoutLike(error) {
|
|
324
|
+
return error instanceof Error && error.name === "TimeoutError";
|
|
325
|
+
}
|
|
326
|
+
function isNoSessionLike(error) {
|
|
327
|
+
return error instanceof Error && error.name === "NoSessionError";
|
|
328
|
+
}
|
|
329
|
+
function isUsageLike(error) {
|
|
330
|
+
if (!(error instanceof Error)) return false;
|
|
331
|
+
return error.name === "CommanderError" || error.name === "InvalidArgumentError" || asRecord$1(error)?.code === "commander.invalidArgument";
|
|
332
|
+
}
|
|
333
|
+
function formatErrorMessage(error) {
|
|
334
|
+
return formatUnknownErrorMessage(error);
|
|
335
|
+
}
|
|
336
|
+
function isAcpQueryClosedBeforeResponseError(error) {
|
|
337
|
+
const acp = extractAcpError(error);
|
|
338
|
+
if (!acp || acp.code !== -32603) return false;
|
|
339
|
+
const details = asRecord$1(acp.data)?.details;
|
|
340
|
+
if (typeof details !== "string") return false;
|
|
341
|
+
return details.toLowerCase().includes(QUERY_CLOSED_BEFORE_RESPONSE_DETAIL);
|
|
342
|
+
}
|
|
343
|
+
function mapErrorCode(error) {
|
|
344
|
+
if (error instanceof PermissionPromptUnavailableError) return "PERMISSION_PROMPT_UNAVAILABLE";
|
|
345
|
+
if (error instanceof PermissionDeniedError) return "PERMISSION_DENIED";
|
|
346
|
+
if (isTimeoutLike(error)) return "TIMEOUT";
|
|
347
|
+
if (isNoSessionLike(error) || isAcpResourceNotFoundError(error)) return "NO_SESSION";
|
|
348
|
+
if (isUsageLike(error)) return "USAGE";
|
|
349
|
+
}
|
|
350
|
+
function normalizeOutputError(error, options = {}) {
|
|
351
|
+
const meta = readOutputErrorMeta(error);
|
|
352
|
+
let code = mapErrorCode(error) ?? options.defaultCode ?? "RUNTIME";
|
|
353
|
+
if (meta.outputCode) code = meta.outputCode;
|
|
354
|
+
if (code === "RUNTIME" && isAcpResourceNotFoundError(error)) code = "NO_SESSION";
|
|
355
|
+
const acp = options.acp ?? meta.acp ?? extractAcpError(error);
|
|
356
|
+
const detailCode = meta.detailCode ?? options.detailCode ?? (error instanceof AuthPolicyError || isAcpAuthRequiredPayload(acp) ? "AUTH_REQUIRED" : void 0);
|
|
357
|
+
return {
|
|
358
|
+
code,
|
|
359
|
+
message: formatErrorMessage(error),
|
|
360
|
+
detailCode,
|
|
361
|
+
origin: meta.origin ?? options.origin,
|
|
362
|
+
retryable: meta.retryable ?? options.retryable,
|
|
363
|
+
acp
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function exitCodeForOutputErrorCode(code) {
|
|
367
|
+
switch (code) {
|
|
368
|
+
case "USAGE": return EXIT_CODES.USAGE;
|
|
369
|
+
case "TIMEOUT": return EXIT_CODES.TIMEOUT;
|
|
370
|
+
case "NO_SESSION": return EXIT_CODES.NO_SESSION;
|
|
371
|
+
case "PERMISSION_DENIED":
|
|
372
|
+
case "PERMISSION_PROMPT_UNAVAILABLE": return EXIT_CODES.PERMISSION_DENIED;
|
|
373
|
+
default: return EXIT_CODES.ERROR;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/perf-metrics.ts
|
|
379
|
+
const counters = /* @__PURE__ */ new Map();
|
|
380
|
+
const gauges = /* @__PURE__ */ new Map();
|
|
381
|
+
const timings = /* @__PURE__ */ new Map();
|
|
382
|
+
function hrNow() {
|
|
383
|
+
return process.hrtime.bigint();
|
|
384
|
+
}
|
|
385
|
+
function durationMs(start) {
|
|
386
|
+
return Number(process.hrtime.bigint() - start) / 1e6;
|
|
387
|
+
}
|
|
388
|
+
function roundMetric(value) {
|
|
389
|
+
return Number(value.toFixed(3));
|
|
390
|
+
}
|
|
391
|
+
function incrementPerfCounter(name, delta = 1) {
|
|
392
|
+
counters.set(name, (counters.get(name) ?? 0) + delta);
|
|
393
|
+
}
|
|
394
|
+
function setPerfGauge(name, value) {
|
|
395
|
+
gauges.set(name, value);
|
|
396
|
+
}
|
|
397
|
+
function recordPerfDuration(name, durationMsValue) {
|
|
398
|
+
const next = timings.get(name) ?? {
|
|
399
|
+
count: 0,
|
|
400
|
+
totalMs: 0,
|
|
401
|
+
maxMs: 0
|
|
402
|
+
};
|
|
403
|
+
next.count += 1;
|
|
404
|
+
next.totalMs += durationMsValue;
|
|
405
|
+
next.maxMs = Math.max(next.maxMs, durationMsValue);
|
|
406
|
+
timings.set(name, next);
|
|
407
|
+
}
|
|
408
|
+
async function measurePerf(name, run) {
|
|
409
|
+
const startedAt = hrNow();
|
|
410
|
+
try {
|
|
411
|
+
return await run();
|
|
412
|
+
} finally {
|
|
413
|
+
recordPerfDuration(name, durationMs(startedAt));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function startPerfTimer(name) {
|
|
417
|
+
const startedAt = hrNow();
|
|
418
|
+
return () => {
|
|
419
|
+
const elapsedMs = durationMs(startedAt);
|
|
420
|
+
recordPerfDuration(name, elapsedMs);
|
|
421
|
+
return elapsedMs;
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function getPerfMetricsSnapshot() {
|
|
425
|
+
return {
|
|
426
|
+
counters: Object.fromEntries(counters.entries()),
|
|
427
|
+
gauges: Object.fromEntries(gauges.entries()),
|
|
428
|
+
timings: Object.fromEntries([...timings.entries()].map(([name, bucket]) => [name, {
|
|
429
|
+
count: bucket.count,
|
|
430
|
+
totalMs: roundMetric(bucket.totalMs),
|
|
431
|
+
maxMs: roundMetric(bucket.maxMs)
|
|
432
|
+
}]))
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function resetPerfMetrics() {
|
|
436
|
+
counters.clear();
|
|
437
|
+
gauges.clear();
|
|
438
|
+
timings.clear();
|
|
439
|
+
}
|
|
440
|
+
function formatPerfMetric(name, durationMsValue) {
|
|
441
|
+
return `${name}=${roundMetric(durationMsValue)}ms`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/queue-paths.ts
|
|
446
|
+
function shortHash(value, length) {
|
|
447
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
448
|
+
}
|
|
449
|
+
function queueKeyForSession(sessionId) {
|
|
450
|
+
return shortHash(sessionId, 24);
|
|
451
|
+
}
|
|
452
|
+
function queueBaseDir(homeDir = os.homedir()) {
|
|
453
|
+
return path.join(homeDir, ".acpx", "queues");
|
|
454
|
+
}
|
|
455
|
+
function queueSocketBaseDir(homeDir = os.homedir()) {
|
|
456
|
+
if (process.platform === "win32") return;
|
|
457
|
+
return path.join("/tmp", `acpx-${shortHash(homeDir, 10)}`);
|
|
458
|
+
}
|
|
459
|
+
function queueLockFilePath(sessionId, homeDir = os.homedir()) {
|
|
460
|
+
return path.join(queueBaseDir(homeDir), `${queueKeyForSession(sessionId)}.lock`);
|
|
461
|
+
}
|
|
462
|
+
function queueSocketPath(sessionId, homeDir = os.homedir()) {
|
|
463
|
+
const key = queueKeyForSession(sessionId);
|
|
464
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\acpx-${key}`;
|
|
465
|
+
return path.join(queueSocketBaseDir(homeDir) ?? "/tmp", `${key}.sock`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
//#endregion
|
|
469
|
+
//#region src/queue-lease-store.ts
|
|
470
|
+
const PROCESS_EXIT_GRACE_MS = 1500;
|
|
471
|
+
const PROCESS_POLL_MS = 50;
|
|
472
|
+
const QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
|
|
473
|
+
function parseQueueOwnerRecord(raw) {
|
|
474
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
475
|
+
const record = raw;
|
|
476
|
+
if (!Number.isInteger(record.pid) || record.pid <= 0 || typeof record.sessionId !== "string" || typeof record.socketPath !== "string" || typeof record.createdAt !== "string" || typeof record.heartbeatAt !== "string" || !Number.isInteger(record.ownerGeneration) || record.ownerGeneration <= 0 || !Number.isInteger(record.queueDepth) || record.queueDepth < 0) return null;
|
|
477
|
+
return {
|
|
478
|
+
pid: record.pid,
|
|
479
|
+
sessionId: record.sessionId,
|
|
480
|
+
socketPath: record.socketPath,
|
|
481
|
+
createdAt: record.createdAt,
|
|
482
|
+
heartbeatAt: record.heartbeatAt,
|
|
483
|
+
ownerGeneration: record.ownerGeneration,
|
|
484
|
+
queueDepth: record.queueDepth
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function createOwnerGeneration() {
|
|
488
|
+
return Date.now() * 1e3 + Math.floor(Math.random() * 1e3);
|
|
489
|
+
}
|
|
490
|
+
function nowIso() {
|
|
491
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
492
|
+
}
|
|
493
|
+
function isQueueOwnerHeartbeatStale(owner) {
|
|
494
|
+
const heartbeatMs = Date.parse(owner.heartbeatAt);
|
|
495
|
+
if (!Number.isFinite(heartbeatMs)) return true;
|
|
496
|
+
return Date.now() - heartbeatMs > QUEUE_OWNER_STALE_HEARTBEAT_MS;
|
|
497
|
+
}
|
|
498
|
+
async function ensureQueueDir() {
|
|
499
|
+
await fs.mkdir(queueBaseDir(), { recursive: true });
|
|
500
|
+
const socketDir = queueSocketBaseDir();
|
|
501
|
+
if (socketDir) await fs.mkdir(socketDir, { recursive: true });
|
|
502
|
+
}
|
|
503
|
+
async function removeSocketFile(socketPath) {
|
|
504
|
+
if (process.platform === "win32") return;
|
|
505
|
+
try {
|
|
506
|
+
await fs.unlink(socketPath);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (error.code !== "ENOENT") throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
512
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
513
|
+
while (Date.now() <= deadline) {
|
|
514
|
+
if (!isProcessAlive(pid)) return true;
|
|
515
|
+
await waitMs(PROCESS_POLL_MS);
|
|
516
|
+
}
|
|
517
|
+
return !isProcessAlive(pid);
|
|
518
|
+
}
|
|
519
|
+
async function cleanupStaleQueueOwner(sessionId, owner) {
|
|
520
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
521
|
+
await removeSocketFile(owner?.socketPath ?? queueSocketPath(sessionId)).catch(() => {});
|
|
522
|
+
await fs.unlink(lockPath).catch((error) => {
|
|
523
|
+
if (error.code !== "ENOENT") throw error;
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
async function readQueueOwnerRecord(sessionId) {
|
|
527
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
528
|
+
try {
|
|
529
|
+
const payload = await fs.readFile(lockPath, "utf8");
|
|
530
|
+
return parseQueueOwnerRecord(JSON.parse(payload)) ?? void 0;
|
|
531
|
+
} catch {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function isProcessAlive(pid) {
|
|
536
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
537
|
+
try {
|
|
538
|
+
process.kill(pid, 0);
|
|
539
|
+
return true;
|
|
540
|
+
} catch {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function terminateProcess(pid) {
|
|
545
|
+
if (!isProcessAlive(pid)) return false;
|
|
546
|
+
try {
|
|
547
|
+
process.kill(pid, "SIGTERM");
|
|
548
|
+
} catch {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) return true;
|
|
552
|
+
try {
|
|
553
|
+
process.kill(pid, "SIGKILL");
|
|
554
|
+
} catch {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
async function ensureOwnerIsUsable(sessionId, owner) {
|
|
561
|
+
const alive = isProcessAlive(owner.pid);
|
|
562
|
+
const stale = isQueueOwnerHeartbeatStale(owner);
|
|
563
|
+
if (alive && !stale) return true;
|
|
564
|
+
if (alive) await terminateProcess(owner.pid).catch(() => {});
|
|
565
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
async function readQueueOwnerStatus(sessionId) {
|
|
569
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
570
|
+
if (!owner) return;
|
|
571
|
+
const alive = await ensureOwnerIsUsable(sessionId, owner);
|
|
572
|
+
if (!alive) return;
|
|
573
|
+
return {
|
|
574
|
+
pid: owner.pid,
|
|
575
|
+
socketPath: owner.socketPath,
|
|
576
|
+
heartbeatAt: owner.heartbeatAt,
|
|
577
|
+
ownerGeneration: owner.ownerGeneration,
|
|
578
|
+
queueDepth: owner.queueDepth,
|
|
579
|
+
alive,
|
|
580
|
+
stale: isQueueOwnerHeartbeatStale(owner)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
async function tryAcquireQueueOwnerLease(sessionId, nowIsoFactory = nowIso) {
|
|
584
|
+
await ensureQueueDir();
|
|
585
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
586
|
+
const socketPath = queueSocketPath(sessionId);
|
|
587
|
+
const createdAt = nowIsoFactory();
|
|
588
|
+
const ownerGeneration = createOwnerGeneration();
|
|
589
|
+
const payload = JSON.stringify({
|
|
590
|
+
pid: process.pid,
|
|
591
|
+
sessionId,
|
|
592
|
+
socketPath,
|
|
593
|
+
createdAt,
|
|
594
|
+
heartbeatAt: createdAt,
|
|
595
|
+
ownerGeneration,
|
|
596
|
+
queueDepth: 0
|
|
597
|
+
}, null, 2);
|
|
598
|
+
try {
|
|
599
|
+
await fs.writeFile(lockPath, `${payload}\n`, {
|
|
600
|
+
encoding: "utf8",
|
|
601
|
+
flag: "wx"
|
|
602
|
+
});
|
|
603
|
+
await removeSocketFile(socketPath).catch(() => {});
|
|
604
|
+
return {
|
|
605
|
+
sessionId,
|
|
606
|
+
lockPath,
|
|
607
|
+
socketPath,
|
|
608
|
+
createdAt,
|
|
609
|
+
ownerGeneration
|
|
610
|
+
};
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (error.code !== "EEXIST") throw error;
|
|
613
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
614
|
+
if (!owner) {
|
|
615
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!isProcessAlive(owner.pid) || isQueueOwnerHeartbeatStale(owner)) {
|
|
619
|
+
if (isProcessAlive(owner.pid)) await terminateProcess(owner.pid).catch(() => {});
|
|
620
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
|
|
626
|
+
const payload = JSON.stringify({
|
|
627
|
+
pid: process.pid,
|
|
628
|
+
sessionId: lease.sessionId,
|
|
629
|
+
socketPath: lease.socketPath,
|
|
630
|
+
createdAt: lease.createdAt,
|
|
631
|
+
heartbeatAt: nowIsoFactory(),
|
|
632
|
+
ownerGeneration: lease.ownerGeneration,
|
|
633
|
+
queueDepth: Math.max(0, Math.round(options.queueDepth))
|
|
634
|
+
}, null, 2);
|
|
635
|
+
await fs.writeFile(lease.lockPath, `${payload}\n`, { encoding: "utf8" });
|
|
636
|
+
}
|
|
637
|
+
async function releaseQueueOwnerLease(lease) {
|
|
638
|
+
await removeSocketFile(lease.socketPath).catch(() => {});
|
|
639
|
+
await fs.unlink(lease.lockPath).catch((error) => {
|
|
640
|
+
if (error.code !== "ENOENT") throw error;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
async function terminateQueueOwnerForSession(sessionId) {
|
|
644
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
645
|
+
if (!owner) return;
|
|
646
|
+
if (isProcessAlive(owner.pid)) await terminateProcess(owner.pid);
|
|
647
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
648
|
+
}
|
|
649
|
+
async function waitMs(ms) {
|
|
650
|
+
await new Promise((resolve) => {
|
|
651
|
+
setTimeout(resolve, ms);
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/queue-messages.ts
|
|
657
|
+
function asRecord(value) {
|
|
658
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
659
|
+
return value;
|
|
660
|
+
}
|
|
661
|
+
function isPermissionMode(value) {
|
|
662
|
+
return value === "approve-all" || value === "approve-reads" || value === "deny-all";
|
|
663
|
+
}
|
|
664
|
+
function isNonInteractivePermissionPolicy(value) {
|
|
665
|
+
return value === "deny" || value === "fail";
|
|
666
|
+
}
|
|
667
|
+
function isOutputErrorCode(value) {
|
|
668
|
+
return typeof value === "string" && OUTPUT_ERROR_CODES.includes(value);
|
|
669
|
+
}
|
|
670
|
+
function isOutputErrorOrigin(value) {
|
|
671
|
+
return typeof value === "string" && OUTPUT_ERROR_ORIGINS.includes(value);
|
|
672
|
+
}
|
|
673
|
+
function parseAcpError(value) {
|
|
674
|
+
const record = asRecord(value);
|
|
675
|
+
if (!record) return;
|
|
676
|
+
if (typeof record.code !== "number" || !Number.isFinite(record.code)) return;
|
|
677
|
+
if (typeof record.message !== "string" || record.message.length === 0) return;
|
|
678
|
+
return {
|
|
679
|
+
code: record.code,
|
|
680
|
+
message: record.message,
|
|
681
|
+
data: record.data
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function parseOwnerGeneration(value) {
|
|
685
|
+
if (value == null) return;
|
|
686
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return null;
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
function parseQueueRequest(raw) {
|
|
690
|
+
const request = asRecord(raw);
|
|
691
|
+
if (!request) return null;
|
|
692
|
+
if (typeof request.type !== "string" || typeof request.requestId !== "string") return null;
|
|
693
|
+
const ownerGeneration = parseOwnerGeneration(request.ownerGeneration);
|
|
694
|
+
if (ownerGeneration === null) return null;
|
|
695
|
+
const timeoutRaw = request.timeoutMs;
|
|
696
|
+
const timeoutMs = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.round(timeoutRaw) : void 0;
|
|
697
|
+
if (request.type === "submit_prompt") {
|
|
698
|
+
const nonInteractivePermissions = request.nonInteractivePermissions == null ? void 0 : isNonInteractivePermissionPolicy(request.nonInteractivePermissions) ? request.nonInteractivePermissions : null;
|
|
699
|
+
const suppressSdkConsoleErrors = request.suppressSdkConsoleErrors == null ? void 0 : typeof request.suppressSdkConsoleErrors === "boolean" ? request.suppressSdkConsoleErrors : null;
|
|
700
|
+
const prompt = request.prompt == null ? void 0 : isPromptInput(request.prompt) ? request.prompt : null;
|
|
701
|
+
if (typeof request.message !== "string" || !isPermissionMode(request.permissionMode) || prompt === null || nonInteractivePermissions === null || suppressSdkConsoleErrors === null || typeof request.waitForCompletion !== "boolean") return null;
|
|
702
|
+
return {
|
|
703
|
+
type: "submit_prompt",
|
|
704
|
+
requestId: request.requestId,
|
|
705
|
+
ownerGeneration,
|
|
706
|
+
message: request.message,
|
|
707
|
+
prompt: prompt ?? textPrompt(request.message),
|
|
708
|
+
permissionMode: request.permissionMode,
|
|
709
|
+
nonInteractivePermissions,
|
|
710
|
+
timeoutMs,
|
|
711
|
+
...suppressSdkConsoleErrors !== void 0 ? { suppressSdkConsoleErrors } : {},
|
|
712
|
+
waitForCompletion: request.waitForCompletion
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
if (request.type === "cancel_prompt") return {
|
|
716
|
+
type: "cancel_prompt",
|
|
717
|
+
requestId: request.requestId,
|
|
718
|
+
ownerGeneration
|
|
719
|
+
};
|
|
720
|
+
if (request.type === "set_mode") {
|
|
721
|
+
if (typeof request.modeId !== "string" || request.modeId.trim().length === 0) return null;
|
|
722
|
+
return {
|
|
723
|
+
type: "set_mode",
|
|
724
|
+
requestId: request.requestId,
|
|
725
|
+
ownerGeneration,
|
|
726
|
+
modeId: request.modeId,
|
|
727
|
+
timeoutMs
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (request.type === "set_config_option") {
|
|
731
|
+
if (typeof request.configId !== "string" || request.configId.trim().length === 0 || typeof request.value !== "string" || request.value.trim().length === 0) return null;
|
|
732
|
+
return {
|
|
733
|
+
type: "set_config_option",
|
|
734
|
+
requestId: request.requestId,
|
|
735
|
+
ownerGeneration,
|
|
736
|
+
configId: request.configId,
|
|
737
|
+
value: request.value,
|
|
738
|
+
timeoutMs
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
function parseSessionSendResult(raw) {
|
|
744
|
+
const result = asRecord(raw);
|
|
745
|
+
if (!result) return null;
|
|
746
|
+
if (typeof result.stopReason !== "string" || typeof result.sessionId !== "string" || typeof result.resumed !== "boolean") return null;
|
|
747
|
+
const permissionStats = asRecord(result.permissionStats);
|
|
748
|
+
const record = asRecord(result.record);
|
|
749
|
+
if (!permissionStats || !record) return null;
|
|
750
|
+
if (!(typeof permissionStats.requested === "number" && typeof permissionStats.approved === "number" && typeof permissionStats.denied === "number" && typeof permissionStats.cancelled === "number")) return null;
|
|
751
|
+
if (!(typeof record.acpxRecordId === "string" && typeof record.acpSessionId === "string" && typeof record.agentCommand === "string" && typeof record.cwd === "string" && typeof record.createdAt === "string" && typeof record.lastUsedAt === "string" && Array.isArray(record.messages) && typeof record.updated_at === "string" && typeof record.lastSeq === "number" && Number.isInteger(record.lastSeq) && !!record.eventLog && typeof record.eventLog === "object")) return null;
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
function parseQueueOwnerMessage(raw) {
|
|
755
|
+
const message = asRecord(raw);
|
|
756
|
+
if (!message || typeof message.type !== "string") return null;
|
|
757
|
+
if (typeof message.requestId !== "string") return null;
|
|
758
|
+
const ownerGeneration = parseOwnerGeneration(message.ownerGeneration);
|
|
759
|
+
if (ownerGeneration === null) return null;
|
|
760
|
+
if (message.type === "accepted") return {
|
|
761
|
+
type: "accepted",
|
|
762
|
+
requestId: message.requestId,
|
|
763
|
+
ownerGeneration
|
|
764
|
+
};
|
|
765
|
+
if (message.type === "event") {
|
|
766
|
+
if (!isAcpJsonRpcMessage(message.message)) return null;
|
|
767
|
+
return {
|
|
768
|
+
type: "event",
|
|
769
|
+
requestId: message.requestId,
|
|
770
|
+
ownerGeneration,
|
|
771
|
+
message: message.message
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (message.type === "result") {
|
|
775
|
+
const parsedResult = parseSessionSendResult(message.result);
|
|
776
|
+
if (!parsedResult) return null;
|
|
777
|
+
return {
|
|
778
|
+
type: "result",
|
|
779
|
+
requestId: message.requestId,
|
|
780
|
+
ownerGeneration,
|
|
781
|
+
result: parsedResult
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
if (message.type === "cancel_result") {
|
|
785
|
+
if (typeof message.cancelled !== "boolean") return null;
|
|
786
|
+
return {
|
|
787
|
+
type: "cancel_result",
|
|
788
|
+
requestId: message.requestId,
|
|
789
|
+
ownerGeneration,
|
|
790
|
+
cancelled: message.cancelled
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (message.type === "set_mode_result") {
|
|
794
|
+
if (typeof message.modeId !== "string") return null;
|
|
795
|
+
return {
|
|
796
|
+
type: "set_mode_result",
|
|
797
|
+
requestId: message.requestId,
|
|
798
|
+
ownerGeneration,
|
|
799
|
+
modeId: message.modeId
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (message.type === "set_config_option_result") {
|
|
803
|
+
const response = asRecord(message.response);
|
|
804
|
+
if (!response || !Array.isArray(response.configOptions)) return null;
|
|
805
|
+
return {
|
|
806
|
+
type: "set_config_option_result",
|
|
807
|
+
requestId: message.requestId,
|
|
808
|
+
ownerGeneration,
|
|
809
|
+
response
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (message.type === "error") {
|
|
813
|
+
if (typeof message.message !== "string" || !isOutputErrorCode(message.code) || !isOutputErrorOrigin(message.origin)) return null;
|
|
814
|
+
const detailCode = typeof message.detailCode === "string" && message.detailCode.trim().length > 0 ? message.detailCode : void 0;
|
|
815
|
+
const retryable = typeof message.retryable === "boolean" ? message.retryable : void 0;
|
|
816
|
+
const acp = parseAcpError(message.acp);
|
|
817
|
+
const outputAlreadyEmitted = typeof message.outputAlreadyEmitted === "boolean" ? message.outputAlreadyEmitted : void 0;
|
|
818
|
+
return {
|
|
819
|
+
type: "error",
|
|
820
|
+
requestId: message.requestId,
|
|
821
|
+
ownerGeneration,
|
|
822
|
+
code: message.code,
|
|
823
|
+
detailCode,
|
|
824
|
+
origin: message.origin,
|
|
825
|
+
message: message.message,
|
|
826
|
+
retryable,
|
|
827
|
+
acp,
|
|
828
|
+
...outputAlreadyEmitted === void 0 ? {} : { outputAlreadyEmitted }
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/queue-ipc-server.ts
|
|
836
|
+
function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
|
|
837
|
+
return {
|
|
838
|
+
type: "error",
|
|
839
|
+
requestId,
|
|
840
|
+
ownerGeneration: void 0,
|
|
841
|
+
code: "RUNTIME",
|
|
842
|
+
detailCode,
|
|
843
|
+
origin: "queue",
|
|
844
|
+
retryable: options.retryable,
|
|
845
|
+
message
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options = {}) {
|
|
849
|
+
const normalized = normalizeOutputError(error, {
|
|
850
|
+
defaultCode: "RUNTIME",
|
|
851
|
+
origin: "queue",
|
|
852
|
+
detailCode,
|
|
853
|
+
retryable: options.retryable
|
|
854
|
+
});
|
|
855
|
+
return {
|
|
856
|
+
type: "error",
|
|
857
|
+
requestId,
|
|
858
|
+
code: normalized.code,
|
|
859
|
+
detailCode: normalized.detailCode,
|
|
860
|
+
origin: normalized.origin,
|
|
861
|
+
message: normalized.message,
|
|
862
|
+
retryable: normalized.retryable,
|
|
863
|
+
acp: normalized.acp
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function writeQueueMessage(socket, message) {
|
|
867
|
+
if (socket.destroyed || !socket.writable) return;
|
|
868
|
+
socket.write(`${JSON.stringify(message)}\n`);
|
|
869
|
+
}
|
|
870
|
+
var SessionQueueOwner = class SessionQueueOwner {
|
|
871
|
+
server;
|
|
872
|
+
controlHandlers;
|
|
873
|
+
ownerGeneration;
|
|
874
|
+
maxQueueDepth;
|
|
875
|
+
onQueueDepthChanged;
|
|
876
|
+
pending = [];
|
|
877
|
+
waiters = [];
|
|
878
|
+
closed = false;
|
|
879
|
+
constructor(server, controlHandlers, lease, options) {
|
|
880
|
+
this.server = server;
|
|
881
|
+
this.controlHandlers = controlHandlers;
|
|
882
|
+
this.ownerGeneration = lease.ownerGeneration;
|
|
883
|
+
this.maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth));
|
|
884
|
+
this.onQueueDepthChanged = options.onQueueDepthChanged;
|
|
885
|
+
}
|
|
886
|
+
static async start(lease, controlHandlers, options = { maxQueueDepth: 16 }) {
|
|
887
|
+
const ownerRef = { current: void 0 };
|
|
888
|
+
const server = net.createServer((socket) => {
|
|
889
|
+
ownerRef.current?.handleConnection(socket);
|
|
890
|
+
});
|
|
891
|
+
ownerRef.current = new SessionQueueOwner(server, controlHandlers, lease, options);
|
|
892
|
+
await new Promise((resolve, reject) => {
|
|
893
|
+
const onListening = () => {
|
|
894
|
+
server.off("error", onError);
|
|
895
|
+
resolve();
|
|
896
|
+
};
|
|
897
|
+
const onError = (error) => {
|
|
898
|
+
server.off("listening", onListening);
|
|
899
|
+
reject(error);
|
|
900
|
+
};
|
|
901
|
+
server.once("listening", onListening);
|
|
902
|
+
server.once("error", onError);
|
|
903
|
+
server.listen(lease.socketPath);
|
|
904
|
+
});
|
|
905
|
+
return ownerRef.current;
|
|
906
|
+
}
|
|
907
|
+
async close() {
|
|
908
|
+
if (this.closed) return;
|
|
909
|
+
this.closed = true;
|
|
910
|
+
for (const waiter of this.waiters.splice(0)) waiter(void 0);
|
|
911
|
+
for (const task of this.pending.splice(0)) {
|
|
912
|
+
if (task.waitForCompletion) task.send(makeQueueOwnerError(task.requestId, "Queue owner shutting down before prompt execution", "QUEUE_OWNER_SHUTTING_DOWN", { retryable: true }));
|
|
913
|
+
task.close();
|
|
914
|
+
}
|
|
915
|
+
this.emitQueueDepth();
|
|
916
|
+
await new Promise((resolve) => {
|
|
917
|
+
this.server.close(() => resolve());
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
async nextTask(timeoutMs) {
|
|
921
|
+
if (this.pending.length > 0) {
|
|
922
|
+
const task = this.pending.shift();
|
|
923
|
+
this.emitQueueDepth();
|
|
924
|
+
if (task) recordPerfDuration("queue.owner.wait_ms", Date.now() - task.enqueuedAt);
|
|
925
|
+
return task;
|
|
926
|
+
}
|
|
927
|
+
if (this.closed) return;
|
|
928
|
+
return await new Promise((resolve) => {
|
|
929
|
+
const timer = timeoutMs != null && setTimeout(() => {
|
|
930
|
+
const index = this.waiters.indexOf(waiter);
|
|
931
|
+
if (index >= 0) this.waiters.splice(index, 1);
|
|
932
|
+
resolve(void 0);
|
|
933
|
+
}, Math.max(0, timeoutMs));
|
|
934
|
+
const waiter = (task) => {
|
|
935
|
+
if (timer) clearTimeout(timer);
|
|
936
|
+
resolve(task);
|
|
937
|
+
};
|
|
938
|
+
this.waiters.push(waiter);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
queueDepth() {
|
|
942
|
+
return this.pending.length;
|
|
943
|
+
}
|
|
944
|
+
emitQueueDepth() {
|
|
945
|
+
this.onQueueDepthChanged?.(this.pending.length);
|
|
946
|
+
}
|
|
947
|
+
enqueue(task) {
|
|
948
|
+
if (this.closed) {
|
|
949
|
+
if (task.waitForCompletion) task.send(makeQueueOwnerError(task.requestId, "Queue owner is shutting down", "QUEUE_OWNER_SHUTTING_DOWN", { retryable: true }));
|
|
950
|
+
task.close();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const waiter = this.waiters.shift();
|
|
954
|
+
if (waiter) {
|
|
955
|
+
waiter(task);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (this.pending.length >= this.maxQueueDepth) {
|
|
959
|
+
if (task.waitForCompletion) task.send({
|
|
960
|
+
...makeQueueOwnerError(task.requestId, `Queue owner is overloaded (${this.pending.length}/${this.maxQueueDepth} queued)`, "QUEUE_OWNER_OVERLOADED", { retryable: true }),
|
|
961
|
+
ownerGeneration: this.ownerGeneration
|
|
962
|
+
});
|
|
963
|
+
task.close();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
this.pending.push(task);
|
|
967
|
+
this.emitQueueDepth();
|
|
968
|
+
}
|
|
969
|
+
handleControlRequest(options) {
|
|
970
|
+
writeQueueMessage(options.socket, {
|
|
971
|
+
type: "accepted",
|
|
972
|
+
requestId: options.requestId,
|
|
973
|
+
ownerGeneration: this.ownerGeneration
|
|
974
|
+
});
|
|
975
|
+
options.run().then((message) => {
|
|
976
|
+
writeQueueMessage(options.socket, {
|
|
977
|
+
...message,
|
|
978
|
+
ownerGeneration: this.ownerGeneration
|
|
979
|
+
});
|
|
980
|
+
}).catch((error) => {
|
|
981
|
+
writeQueueMessage(options.socket, {
|
|
982
|
+
...makeQueueOwnerErrorFromUnknown(options.requestId, error, "QUEUE_CONTROL_REQUEST_FAILED"),
|
|
983
|
+
ownerGeneration: this.ownerGeneration
|
|
984
|
+
});
|
|
985
|
+
}).finally(() => {
|
|
986
|
+
if (!options.socket.destroyed) options.socket.end();
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
handleConnection(socket) {
|
|
990
|
+
socket.setEncoding("utf8");
|
|
991
|
+
if (this.closed) {
|
|
992
|
+
writeQueueMessage(socket, makeQueueOwnerError("unknown", "Queue owner is closed", "QUEUE_OWNER_CLOSED", { retryable: true }));
|
|
993
|
+
socket.end();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
let buffer = "";
|
|
997
|
+
let handled = false;
|
|
998
|
+
const fail = (requestId, message, detailCode) => {
|
|
999
|
+
writeQueueMessage(socket, {
|
|
1000
|
+
...makeQueueOwnerError(requestId, message, detailCode, { retryable: false }),
|
|
1001
|
+
ownerGeneration: this.ownerGeneration
|
|
1002
|
+
});
|
|
1003
|
+
socket.end();
|
|
1004
|
+
};
|
|
1005
|
+
const processLine = (line) => {
|
|
1006
|
+
if (handled) return;
|
|
1007
|
+
handled = true;
|
|
1008
|
+
let parsed;
|
|
1009
|
+
try {
|
|
1010
|
+
parsed = JSON.parse(line);
|
|
1011
|
+
} catch {
|
|
1012
|
+
fail("unknown", "Invalid queue request payload", "QUEUE_REQUEST_PAYLOAD_INVALID_JSON");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const request = parseQueueRequest(parsed);
|
|
1016
|
+
if (!request) {
|
|
1017
|
+
fail("unknown", "Invalid queue request", "QUEUE_REQUEST_INVALID");
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (request.ownerGeneration !== void 0 && this.ownerGeneration !== void 0 && request.ownerGeneration !== this.ownerGeneration) {
|
|
1021
|
+
fail(request.requestId, "Queue request targeted a stale queue owner generation", "QUEUE_OWNER_GENERATION_MISMATCH");
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (request.type === "cancel_prompt") {
|
|
1025
|
+
this.handleControlRequest({
|
|
1026
|
+
socket,
|
|
1027
|
+
requestId: request.requestId,
|
|
1028
|
+
run: async () => ({
|
|
1029
|
+
type: "cancel_result",
|
|
1030
|
+
requestId: request.requestId,
|
|
1031
|
+
cancelled: await this.controlHandlers.cancelPrompt()
|
|
1032
|
+
})
|
|
1033
|
+
});
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (request.type === "set_mode") {
|
|
1037
|
+
this.handleControlRequest({
|
|
1038
|
+
socket,
|
|
1039
|
+
requestId: request.requestId,
|
|
1040
|
+
run: async () => {
|
|
1041
|
+
await this.controlHandlers.setSessionMode(request.modeId, request.timeoutMs);
|
|
1042
|
+
return {
|
|
1043
|
+
type: "set_mode_result",
|
|
1044
|
+
requestId: request.requestId,
|
|
1045
|
+
modeId: request.modeId
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (request.type === "set_config_option") {
|
|
1052
|
+
this.handleControlRequest({
|
|
1053
|
+
socket,
|
|
1054
|
+
requestId: request.requestId,
|
|
1055
|
+
run: async () => ({
|
|
1056
|
+
type: "set_config_option_result",
|
|
1057
|
+
requestId: request.requestId,
|
|
1058
|
+
response: await this.controlHandlers.setSessionConfigOption(request.configId, request.value, request.timeoutMs)
|
|
1059
|
+
})
|
|
1060
|
+
});
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const task = {
|
|
1064
|
+
requestId: request.requestId,
|
|
1065
|
+
message: request.message,
|
|
1066
|
+
prompt: request.prompt ?? textPrompt(request.message),
|
|
1067
|
+
permissionMode: request.permissionMode,
|
|
1068
|
+
nonInteractivePermissions: request.nonInteractivePermissions,
|
|
1069
|
+
timeoutMs: request.timeoutMs,
|
|
1070
|
+
suppressSdkConsoleErrors: request.suppressSdkConsoleErrors,
|
|
1071
|
+
waitForCompletion: request.waitForCompletion,
|
|
1072
|
+
enqueuedAt: Date.now(),
|
|
1073
|
+
send: (message) => {
|
|
1074
|
+
writeQueueMessage(socket, {
|
|
1075
|
+
...message,
|
|
1076
|
+
ownerGeneration: this.ownerGeneration
|
|
1077
|
+
});
|
|
1078
|
+
},
|
|
1079
|
+
close: () => {
|
|
1080
|
+
if (!socket.destroyed) socket.end();
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
writeQueueMessage(socket, {
|
|
1084
|
+
type: "accepted",
|
|
1085
|
+
requestId: request.requestId,
|
|
1086
|
+
ownerGeneration: this.ownerGeneration
|
|
1087
|
+
});
|
|
1088
|
+
if (!request.waitForCompletion) task.close();
|
|
1089
|
+
this.enqueue(task);
|
|
1090
|
+
};
|
|
1091
|
+
socket.on("data", (chunk) => {
|
|
1092
|
+
buffer += chunk;
|
|
1093
|
+
let index = buffer.indexOf("\n");
|
|
1094
|
+
while (index >= 0) {
|
|
1095
|
+
const line = buffer.slice(0, index).trim();
|
|
1096
|
+
buffer = buffer.slice(index + 1);
|
|
1097
|
+
if (line.length > 0) processLine(line);
|
|
1098
|
+
index = buffer.indexOf("\n");
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
socket.on("error", () => {});
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/queue-ipc.ts
|
|
1107
|
+
var queue_ipc_exports = /* @__PURE__ */ __exportAll({
|
|
1108
|
+
QUEUE_CONNECT_RETRY_MS: () => QUEUE_CONNECT_RETRY_MS,
|
|
1109
|
+
SessionQueueOwner: () => SessionQueueOwner,
|
|
1110
|
+
isProcessAlive: () => isProcessAlive,
|
|
1111
|
+
probeQueueOwnerHealth: () => probeQueueOwnerHealth,
|
|
1112
|
+
releaseQueueOwnerLease: () => releaseQueueOwnerLease,
|
|
1113
|
+
terminateProcess: () => terminateProcess,
|
|
1114
|
+
terminateQueueOwnerForSession: () => terminateQueueOwnerForSession,
|
|
1115
|
+
tryAcquireQueueOwnerLease: () => tryAcquireQueueOwnerLease,
|
|
1116
|
+
tryCancelOnRunningOwner: () => tryCancelOnRunningOwner,
|
|
1117
|
+
trySetConfigOptionOnRunningOwner: () => trySetConfigOptionOnRunningOwner,
|
|
1118
|
+
trySetModeOnRunningOwner: () => trySetModeOnRunningOwner,
|
|
1119
|
+
trySubmitToRunningOwner: () => trySubmitToRunningOwner,
|
|
1120
|
+
waitMs: () => waitMs
|
|
1121
|
+
});
|
|
1122
|
+
const QUEUE_CONNECT_ATTEMPTS = 40;
|
|
1123
|
+
const QUEUE_CONNECT_RETRY_MS = 50;
|
|
1124
|
+
const STALE_OWNER_PROTOCOL_DETAIL_CODES = new Set(["QUEUE_PROTOCOL_MALFORMED_MESSAGE", "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE"]);
|
|
1125
|
+
async function maybeRecoverStaleOwnerAfterProtocolMismatch(params) {
|
|
1126
|
+
if (!(params.error instanceof QueueProtocolError)) return false;
|
|
1127
|
+
const detailCode = params.error.detailCode;
|
|
1128
|
+
if (!detailCode || !STALE_OWNER_PROTOCOL_DETAIL_CODES.has(detailCode)) return false;
|
|
1129
|
+
await terminateQueueOwnerForSession(params.sessionId).catch(() => {});
|
|
1130
|
+
incrementPerfCounter("queue.owner.stale_recovered");
|
|
1131
|
+
if (params.verbose) process.stderr.write(`[acpx] dropped stale queue owner metadata after protocol mismatch for session ${params.sessionId} (${detailCode})\n`);
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
function shouldRetryQueueConnect(error) {
|
|
1135
|
+
const code = error.code;
|
|
1136
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
1137
|
+
}
|
|
1138
|
+
async function connectToSocket(socketPath) {
|
|
1139
|
+
return await new Promise((resolve, reject) => {
|
|
1140
|
+
const socket = net.createConnection(socketPath);
|
|
1141
|
+
const onConnect = () => {
|
|
1142
|
+
socket.off("error", onError);
|
|
1143
|
+
resolve(socket);
|
|
1144
|
+
};
|
|
1145
|
+
const onError = (error) => {
|
|
1146
|
+
socket.off("connect", onConnect);
|
|
1147
|
+
reject(error);
|
|
1148
|
+
};
|
|
1149
|
+
socket.once("connect", onConnect);
|
|
1150
|
+
socket.once("error", onError);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
async function connectToQueueOwner(owner, maxAttempts = QUEUE_CONNECT_ATTEMPTS) {
|
|
1154
|
+
let lastError;
|
|
1155
|
+
const attempts = Math.max(1, Math.trunc(maxAttempts));
|
|
1156
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
1157
|
+
return await measurePerf("queue.connect", async () => await connectToSocket(owner.socketPath));
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
lastError = error;
|
|
1160
|
+
if (!shouldRetryQueueConnect(error)) throw error;
|
|
1161
|
+
await waitMs(QUEUE_CONNECT_RETRY_MS);
|
|
1162
|
+
}
|
|
1163
|
+
if (lastError && !shouldRetryQueueConnect(lastError)) throw lastError;
|
|
1164
|
+
}
|
|
1165
|
+
async function probeQueueOwnerHealth(sessionId) {
|
|
1166
|
+
const ownerRecord = await readQueueOwnerRecord(sessionId);
|
|
1167
|
+
if (!ownerRecord) return {
|
|
1168
|
+
sessionId,
|
|
1169
|
+
hasLease: false,
|
|
1170
|
+
healthy: false,
|
|
1171
|
+
socketReachable: false,
|
|
1172
|
+
pidAlive: false
|
|
1173
|
+
};
|
|
1174
|
+
const owner = await readQueueOwnerStatus(sessionId);
|
|
1175
|
+
if (!owner) return {
|
|
1176
|
+
sessionId,
|
|
1177
|
+
hasLease: false,
|
|
1178
|
+
healthy: false,
|
|
1179
|
+
socketReachable: false,
|
|
1180
|
+
pidAlive: false
|
|
1181
|
+
};
|
|
1182
|
+
const pidAlive = owner.alive;
|
|
1183
|
+
let socketReachable = false;
|
|
1184
|
+
try {
|
|
1185
|
+
const socket = await connectToQueueOwner(ownerRecord, 2);
|
|
1186
|
+
if (socket) {
|
|
1187
|
+
socketReachable = true;
|
|
1188
|
+
if (!socket.destroyed) socket.end();
|
|
1189
|
+
}
|
|
1190
|
+
} catch {
|
|
1191
|
+
socketReachable = false;
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
sessionId,
|
|
1195
|
+
hasLease: true,
|
|
1196
|
+
healthy: socketReachable,
|
|
1197
|
+
socketReachable,
|
|
1198
|
+
pidAlive,
|
|
1199
|
+
pid: owner.pid,
|
|
1200
|
+
socketPath: owner.socketPath,
|
|
1201
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1202
|
+
queueDepth: owner.queueDepth
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function assertOwnerGeneration(owner, message) {
|
|
1206
|
+
if (owner.ownerGeneration !== void 0 && message.ownerGeneration !== void 0 && message.ownerGeneration !== owner.ownerGeneration) throw new QueueProtocolError("Queue owner returned mismatched generation", {
|
|
1207
|
+
detailCode: "QUEUE_OWNER_GENERATION_MISMATCH",
|
|
1208
|
+
origin: "queue",
|
|
1209
|
+
retryable: true
|
|
1210
|
+
});
|
|
1211
|
+
return message;
|
|
1212
|
+
}
|
|
1213
|
+
function makeMalformedQueueMessageError() {
|
|
1214
|
+
return new QueueProtocolError("Queue owner sent malformed message", {
|
|
1215
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1216
|
+
origin: "queue",
|
|
1217
|
+
retryable: true
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
function parseQueueOwnerResponseLine(owner, requestId, line) {
|
|
1221
|
+
let parsed;
|
|
1222
|
+
try {
|
|
1223
|
+
parsed = JSON.parse(line);
|
|
1224
|
+
} catch {
|
|
1225
|
+
throw new QueueProtocolError("Queue owner sent invalid JSON payload", {
|
|
1226
|
+
detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
|
|
1227
|
+
origin: "queue",
|
|
1228
|
+
retryable: true
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
const parsedMessage = parseQueueOwnerMessage(parsed);
|
|
1232
|
+
if (!parsedMessage) throw makeMalformedQueueMessageError();
|
|
1233
|
+
const message = assertOwnerGeneration(owner, parsedMessage);
|
|
1234
|
+
if (message.requestId !== requestId) throw makeMalformedQueueMessageError();
|
|
1235
|
+
return message;
|
|
1236
|
+
}
|
|
1237
|
+
async function runQueueOwnerRequest(options) {
|
|
1238
|
+
const socket = await connectToQueueOwner(options.owner);
|
|
1239
|
+
if (!socket) return;
|
|
1240
|
+
socket.setEncoding("utf8");
|
|
1241
|
+
return await new Promise((resolve, reject) => {
|
|
1242
|
+
let settled = false;
|
|
1243
|
+
let buffer = "";
|
|
1244
|
+
const state = { acknowledged: false };
|
|
1245
|
+
const finishResolve = (result) => {
|
|
1246
|
+
if (settled) return;
|
|
1247
|
+
settled = true;
|
|
1248
|
+
socket.removeAllListeners();
|
|
1249
|
+
if (!socket.destroyed) socket.end();
|
|
1250
|
+
resolve(result);
|
|
1251
|
+
};
|
|
1252
|
+
const finishReject = (error) => {
|
|
1253
|
+
if (settled) return;
|
|
1254
|
+
settled = true;
|
|
1255
|
+
socket.removeAllListeners();
|
|
1256
|
+
if (!socket.destroyed) socket.destroy();
|
|
1257
|
+
reject(error);
|
|
1258
|
+
};
|
|
1259
|
+
const controls = {
|
|
1260
|
+
state,
|
|
1261
|
+
resolve: finishResolve,
|
|
1262
|
+
reject: finishReject
|
|
1263
|
+
};
|
|
1264
|
+
const processLine = (line) => {
|
|
1265
|
+
let message;
|
|
1266
|
+
try {
|
|
1267
|
+
message = parseQueueOwnerResponseLine(options.owner, options.request.requestId, line);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
finishReject(error);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (message.type === "accepted") {
|
|
1273
|
+
state.acknowledged = true;
|
|
1274
|
+
options.onAccepted?.(controls);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
options.onMessage(message, controls);
|
|
1278
|
+
};
|
|
1279
|
+
socket.on("data", (chunk) => {
|
|
1280
|
+
buffer += chunk;
|
|
1281
|
+
let index = buffer.indexOf("\n");
|
|
1282
|
+
while (index >= 0) {
|
|
1283
|
+
const line = buffer.slice(0, index).trim();
|
|
1284
|
+
buffer = buffer.slice(index + 1);
|
|
1285
|
+
if (line.length > 0) processLine(line);
|
|
1286
|
+
index = buffer.indexOf("\n");
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
socket.once("error", (error) => {
|
|
1290
|
+
finishReject(error);
|
|
1291
|
+
});
|
|
1292
|
+
socket.once("close", () => {
|
|
1293
|
+
if (settled) return;
|
|
1294
|
+
options.onClose(controls);
|
|
1295
|
+
});
|
|
1296
|
+
socket.write(`${JSON.stringify(options.request)}\n`);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
async function submitToQueueOwner(owner, options) {
|
|
1300
|
+
const requestId = randomUUID();
|
|
1301
|
+
const request = {
|
|
1302
|
+
type: "submit_prompt",
|
|
1303
|
+
requestId,
|
|
1304
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1305
|
+
message: options.message,
|
|
1306
|
+
prompt: options.prompt,
|
|
1307
|
+
permissionMode: options.permissionMode,
|
|
1308
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1309
|
+
timeoutMs: options.timeoutMs,
|
|
1310
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1311
|
+
waitForCompletion: options.waitForCompletion
|
|
1312
|
+
};
|
|
1313
|
+
options.outputFormatter.setContext({ sessionId: options.sessionId });
|
|
1314
|
+
return await runQueueOwnerRequest({
|
|
1315
|
+
owner,
|
|
1316
|
+
request,
|
|
1317
|
+
onAccepted: ({ resolve }) => {
|
|
1318
|
+
options.outputFormatter.setContext({ sessionId: options.sessionId });
|
|
1319
|
+
if (!options.waitForCompletion) resolve({
|
|
1320
|
+
queued: true,
|
|
1321
|
+
sessionId: options.sessionId,
|
|
1322
|
+
requestId
|
|
1323
|
+
});
|
|
1324
|
+
},
|
|
1325
|
+
onMessage: (message, { state, resolve, reject }) => {
|
|
1326
|
+
if (message.type === "error") {
|
|
1327
|
+
options.outputFormatter.setContext({ sessionId: options.sessionId });
|
|
1328
|
+
const queueErrorAlreadyEmitted = options.errorEmissionPolicy?.queueErrorAlreadyEmitted ?? true;
|
|
1329
|
+
if (!(message.outputAlreadyEmitted === true) || !queueErrorAlreadyEmitted) {
|
|
1330
|
+
options.outputFormatter.onError({
|
|
1331
|
+
code: message.code ?? "RUNTIME",
|
|
1332
|
+
detailCode: message.detailCode,
|
|
1333
|
+
origin: message.origin ?? "queue",
|
|
1334
|
+
message: message.message,
|
|
1335
|
+
retryable: message.retryable,
|
|
1336
|
+
acp: message.acp
|
|
1337
|
+
});
|
|
1338
|
+
options.outputFormatter.flush();
|
|
1339
|
+
}
|
|
1340
|
+
reject(new QueueConnectionError(message.message, {
|
|
1341
|
+
outputCode: message.code,
|
|
1342
|
+
detailCode: message.detailCode,
|
|
1343
|
+
origin: message.origin ?? "queue",
|
|
1344
|
+
retryable: message.retryable,
|
|
1345
|
+
acp: message.acp,
|
|
1346
|
+
...queueErrorAlreadyEmitted ? { outputAlreadyEmitted: true } : {}
|
|
1347
|
+
}));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (!state.acknowledged) {
|
|
1351
|
+
reject(new QueueConnectionError("Queue owner did not acknowledge request", {
|
|
1352
|
+
detailCode: "QUEUE_ACK_MISSING",
|
|
1353
|
+
origin: "queue",
|
|
1354
|
+
retryable: true
|
|
1355
|
+
}));
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (message.type === "event") {
|
|
1359
|
+
options.outputFormatter.onAcpMessage(message.message);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (message.type === "result") {
|
|
1363
|
+
options.outputFormatter.flush();
|
|
1364
|
+
resolve(message.result);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
reject(new QueueProtocolError("Queue owner returned unexpected response", {
|
|
1368
|
+
detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
|
|
1369
|
+
origin: "queue",
|
|
1370
|
+
retryable: true
|
|
1371
|
+
}));
|
|
1372
|
+
},
|
|
1373
|
+
onClose: ({ state, resolve, reject }) => {
|
|
1374
|
+
if (!state.acknowledged) {
|
|
1375
|
+
reject(new QueueConnectionError("Queue owner disconnected before acknowledging request", {
|
|
1376
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
|
|
1377
|
+
origin: "queue",
|
|
1378
|
+
retryable: true
|
|
1379
|
+
}));
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (!options.waitForCompletion) {
|
|
1383
|
+
resolve({
|
|
1384
|
+
queued: true,
|
|
1385
|
+
sessionId: options.sessionId,
|
|
1386
|
+
requestId
|
|
1387
|
+
});
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
reject(new QueueConnectionError("Queue owner disconnected before prompt completion", {
|
|
1391
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
|
|
1392
|
+
origin: "queue",
|
|
1393
|
+
retryable: true
|
|
1394
|
+
}));
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
|
|
1399
|
+
return await runQueueOwnerRequest({
|
|
1400
|
+
owner,
|
|
1401
|
+
request,
|
|
1402
|
+
onMessage: (message, { state, resolve, reject }) => {
|
|
1403
|
+
if (message.type === "error") {
|
|
1404
|
+
reject(new QueueConnectionError(message.message, {
|
|
1405
|
+
outputCode: message.code,
|
|
1406
|
+
detailCode: message.detailCode,
|
|
1407
|
+
origin: message.origin ?? "queue",
|
|
1408
|
+
retryable: message.retryable,
|
|
1409
|
+
acp: message.acp
|
|
1410
|
+
}));
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (!state.acknowledged) {
|
|
1414
|
+
reject(new QueueConnectionError("Queue owner did not acknowledge request", {
|
|
1415
|
+
detailCode: "QUEUE_ACK_MISSING",
|
|
1416
|
+
origin: "queue",
|
|
1417
|
+
retryable: true
|
|
1418
|
+
}));
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (!isExpectedResponse(message)) {
|
|
1422
|
+
reject(new QueueProtocolError("Queue owner returned unexpected response", {
|
|
1423
|
+
detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
|
|
1424
|
+
origin: "queue",
|
|
1425
|
+
retryable: true
|
|
1426
|
+
}));
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
resolve(message);
|
|
1430
|
+
},
|
|
1431
|
+
onClose: ({ state, reject }) => {
|
|
1432
|
+
if (!state.acknowledged) {
|
|
1433
|
+
reject(new QueueConnectionError("Queue owner disconnected before acknowledging request", {
|
|
1434
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
|
|
1435
|
+
origin: "queue",
|
|
1436
|
+
retryable: true
|
|
1437
|
+
}));
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
reject(new QueueConnectionError("Queue owner disconnected before responding", {
|
|
1441
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
|
|
1442
|
+
origin: "queue",
|
|
1443
|
+
retryable: true
|
|
1444
|
+
}));
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
async function submitCancelToQueueOwner(owner) {
|
|
1449
|
+
const request = {
|
|
1450
|
+
type: "cancel_prompt",
|
|
1451
|
+
requestId: randomUUID(),
|
|
1452
|
+
ownerGeneration: owner.ownerGeneration
|
|
1453
|
+
};
|
|
1454
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "cancel_result");
|
|
1455
|
+
if (!response) return;
|
|
1456
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched cancel response", {
|
|
1457
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1458
|
+
origin: "queue",
|
|
1459
|
+
retryable: true
|
|
1460
|
+
});
|
|
1461
|
+
return response.cancelled;
|
|
1462
|
+
}
|
|
1463
|
+
async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
|
|
1464
|
+
const request = {
|
|
1465
|
+
type: "set_mode",
|
|
1466
|
+
requestId: randomUUID(),
|
|
1467
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1468
|
+
modeId,
|
|
1469
|
+
timeoutMs
|
|
1470
|
+
};
|
|
1471
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_mode_result");
|
|
1472
|
+
if (!response) return;
|
|
1473
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_mode response", {
|
|
1474
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1475
|
+
origin: "queue",
|
|
1476
|
+
retryable: true
|
|
1477
|
+
});
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
|
|
1481
|
+
const request = {
|
|
1482
|
+
type: "set_config_option",
|
|
1483
|
+
requestId: randomUUID(),
|
|
1484
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1485
|
+
configId,
|
|
1486
|
+
value,
|
|
1487
|
+
timeoutMs
|
|
1488
|
+
};
|
|
1489
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_config_option_result");
|
|
1490
|
+
if (!response) return;
|
|
1491
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_config_option response", {
|
|
1492
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1493
|
+
origin: "queue",
|
|
1494
|
+
retryable: true
|
|
1495
|
+
});
|
|
1496
|
+
return response.response;
|
|
1497
|
+
}
|
|
1498
|
+
async function trySubmitToRunningOwner(options) {
|
|
1499
|
+
const owner = await readQueueOwnerRecord(options.sessionId);
|
|
1500
|
+
if (!owner) return;
|
|
1501
|
+
let submitted;
|
|
1502
|
+
try {
|
|
1503
|
+
submitted = await submitToQueueOwner(owner, options);
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
if (await maybeRecoverStaleOwnerAfterProtocolMismatch({
|
|
1506
|
+
sessionId: options.sessionId,
|
|
1507
|
+
owner,
|
|
1508
|
+
error,
|
|
1509
|
+
verbose: options.verbose
|
|
1510
|
+
})) return;
|
|
1511
|
+
throw error;
|
|
1512
|
+
}
|
|
1513
|
+
if (submitted) {
|
|
1514
|
+
if (options.verbose) process.stderr.write(`[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
|
|
1515
|
+
return submitted;
|
|
1516
|
+
}
|
|
1517
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
1518
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting queue requests", {
|
|
1519
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1520
|
+
origin: "queue",
|
|
1521
|
+
retryable: true
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async function tryCancelOnRunningOwner(options) {
|
|
1525
|
+
const owner = await readQueueOwnerRecord(options.sessionId);
|
|
1526
|
+
if (!owner) return;
|
|
1527
|
+
const cancelled = await submitCancelToQueueOwner(owner);
|
|
1528
|
+
if (cancelled !== void 0) {
|
|
1529
|
+
if (options.verbose) process.stderr.write(`[acpx] requested cancel on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
|
|
1530
|
+
return cancelled;
|
|
1531
|
+
}
|
|
1532
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
1533
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting cancel requests", {
|
|
1534
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1535
|
+
origin: "queue",
|
|
1536
|
+
retryable: true
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
|
|
1540
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
1541
|
+
if (!owner) return;
|
|
1542
|
+
if (await submitSetModeToQueueOwner(owner, modeId, timeoutMs)) {
|
|
1543
|
+
if (verbose) process.stderr.write(`[acpx] requested session/set_mode on owner pid ${owner.pid} for session ${sessionId}\n`);
|
|
1544
|
+
return true;
|
|
1545
|
+
}
|
|
1546
|
+
if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
|
|
1547
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting set_mode requests", {
|
|
1548
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1549
|
+
origin: "queue",
|
|
1550
|
+
retryable: true
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
|
|
1554
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
1555
|
+
if (!owner) return;
|
|
1556
|
+
const response = await submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs);
|
|
1557
|
+
if (response) {
|
|
1558
|
+
if (verbose) process.stderr.write(`[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}\n`);
|
|
1559
|
+
return response;
|
|
1560
|
+
}
|
|
1561
|
+
if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
|
|
1562
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting set_config_option requests", {
|
|
1563
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1564
|
+
origin: "queue",
|
|
1565
|
+
retryable: true
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
//#endregion
|
|
1570
|
+
export { OUTPUT_FORMATS as A, ClaudeAcpSessionCreateTimeoutError as B, exitCodeForOutputErrorCode as C, AUTH_POLICIES as D, normalizeOutputError as E, parsePromptSource as F, QueueConnectionError as G, GeminiAcpStartupTimeoutError as H, promptToDisplayText as I, SessionResolutionError as J, SessionModeReplayError as K, textPrompt as L, extractAcpError as M, isAcpResourceNotFoundError as N, EXIT_CODES as O, mergePromptSourceWithText as P, AgentSpawnError as R, startPerfTimer as S, isAcpQueryClosedBeforeResponseError as T, PermissionDeniedError as U, CopilotAcpUnsupportedError as V, PermissionPromptUnavailableError as W, getPerfMetricsSnapshot as _, trySetConfigOptionOnRunningOwner as a, resetPerfMetrics as b, SessionQueueOwner as c, releaseQueueOwnerLease as d, terminateProcess as f, formatPerfMetric as g, waitMs as h, tryCancelOnRunningOwner as i, SESSION_RECORD_SCHEMA as j, NON_INTERACTIVE_PERMISSION_POLICIES as k, isProcessAlive as l, tryAcquireQueueOwnerLease as m, probeQueueOwnerHealth as n, trySetModeOnRunningOwner as o, terminateQueueOwnerForSession as p, SessionNotFoundError as q, queue_ipc_exports as r, trySubmitToRunningOwner as s, QUEUE_CONNECT_RETRY_MS as t, refreshQueueOwnerLease as u, incrementPerfCounter as v, formatErrorMessage as w, setPerfGauge as x, measurePerf as y, AuthPolicyError as z };
|
|
1571
|
+
//# sourceMappingURL=queue-ipc-C8StWiZt.js.map
|