acpx 0.16.0 → 0.17.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 +2 -0
- package/dist/{cli-BkYIxj_N.js → cli-CiyfEC7L.js} +5 -4
- package/dist/{cli-BkYIxj_N.js.map → cli-CiyfEC7L.js.map} +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +33 -26
- package/dist/cli.js.map +1 -1
- package/dist/{client-5levcBIs.d.ts → client-C6NUFqr-.d.ts} +12 -6
- package/dist/client-C6NUFqr-.d.ts.map +1 -0
- package/dist/{flows-Bxs4udrs.js → flows-CTOdIk-m.js} +6 -5
- package/dist/{flows-Bxs4udrs.js.map → flows-CTOdIk-m.js.map} +1 -1
- package/dist/flows.d.ts +2 -2
- package/dist/flows.js +1 -1
- package/dist/{invocation-options-BY4lxkn3.js → invocation-options-DbCcOAzM.js} +4 -3
- package/dist/{invocation-options-BY4lxkn3.js.map → invocation-options-DbCcOAzM.js.map} +1 -1
- package/dist/ipc-D3akHMEq.js +3350 -0
- package/dist/ipc-D3akHMEq.js.map +1 -0
- package/dist/output-BPPjvJTI.js +980 -0
- package/dist/output-BPPjvJTI.js.map +1 -0
- package/dist/{live-checkpoint-BEfxBKCh.js → queue-owner-runtime--IbbndGz.js} +3052 -1852
- package/dist/queue-owner-runtime--IbbndGz.js.map +1 -0
- package/dist/runtime.d.ts +99 -12
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +308 -69
- package/dist/runtime.js.map +1 -1
- package/dist/{session-options-t3d-F4dn.d.ts → session-options-Dp6mzNSy.d.ts} +2 -2
- package/dist/{session-options-t3d-F4dn.d.ts.map → session-options-Dp6mzNSy.d.ts.map} +1 -1
- package/dist/session-watch--UX5dh00.js +76 -0
- package/dist/session-watch--UX5dh00.js.map +1 -0
- package/dist/watch-BbBxO-kb.js +32 -0
- package/dist/watch-BbBxO-kb.js.map +1 -0
- package/package.json +1 -1
- package/skills/acpx/SKILL.md +5 -2
- package/dist/client-5levcBIs.d.ts.map +0 -1
- package/dist/live-checkpoint-BEfxBKCh.js.map +0 -1
- package/dist/output-uo5a27KB.js +0 -4711
- package/dist/output-uo5a27KB.js.map +0 -1
|
@@ -0,0 +1,3350 @@
|
|
|
1
|
+
import { n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-DU6mhZBL.js";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs$1 from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { statRegularFile, tempFile, withTempFile } from "@openclaw/fs-safe/advanced";
|
|
7
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
+
import { root } from "@openclaw/fs-safe";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
11
|
+
import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
|
|
12
|
+
import net from "node:net";
|
|
13
|
+
//#region src/errors.ts
|
|
14
|
+
var AcpxOperationalError = class extends Error {
|
|
15
|
+
outputCode;
|
|
16
|
+
detailCode;
|
|
17
|
+
origin;
|
|
18
|
+
retryable;
|
|
19
|
+
acp;
|
|
20
|
+
outputAlreadyEmitted;
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
this.name = new.target.name;
|
|
24
|
+
this.outputCode = options?.outputCode;
|
|
25
|
+
this.detailCode = options?.detailCode;
|
|
26
|
+
this.origin = options?.origin;
|
|
27
|
+
this.retryable = options?.retryable;
|
|
28
|
+
this.acp = options?.acp;
|
|
29
|
+
this.outputAlreadyEmitted = options?.outputAlreadyEmitted;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var SessionNotFoundError = class extends AcpxOperationalError {
|
|
33
|
+
sessionId;
|
|
34
|
+
constructor(sessionId) {
|
|
35
|
+
super(`Session not found: ${sessionId}`);
|
|
36
|
+
this.sessionId = sessionId;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var SessionResolutionError = class extends AcpxOperationalError {};
|
|
40
|
+
var AgentSpawnError = class extends AcpxOperationalError {
|
|
41
|
+
agentCommand;
|
|
42
|
+
constructor(agentCommand, cause) {
|
|
43
|
+
const spawnEnoent = (cause instanceof Error ? cause.code : void 0) === "ENOENT";
|
|
44
|
+
const message = spawnEnoent ? `Failed to spawn agent command: ${agentCommand}. The agent process could not start because a required executable, interpreter, working directory, or other launch path was not found. Check the command, effective PATH, and working directory, or verify the custom agent's configured argv.` : `Failed to spawn agent command: ${agentCommand}`;
|
|
45
|
+
super(message, {
|
|
46
|
+
cause: cause instanceof Error ? cause : void 0,
|
|
47
|
+
...spawnEnoent ? { detailCode: "AGENT_SPAWN_ENOENT" } : {}
|
|
48
|
+
});
|
|
49
|
+
this.agentCommand = agentCommand;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var AgentStartupError = class extends AcpxOperationalError {
|
|
53
|
+
agentCommand;
|
|
54
|
+
exitCode;
|
|
55
|
+
signal;
|
|
56
|
+
stderrSummary;
|
|
57
|
+
constructor(params) {
|
|
58
|
+
const exitSummary = `exit=${params.exitCode ?? "null"}, signal=${params.signal ?? "null"}`;
|
|
59
|
+
const stderrSuffix = typeof params.stderrSummary === "string" && params.stderrSummary.trim().length > 0 ? `: ${params.stderrSummary.trim()}` : "";
|
|
60
|
+
super(`ACP agent exited before initialize completed (${exitSummary})${stderrSuffix}`, {
|
|
61
|
+
cause: params.cause instanceof Error ? params.cause : void 0,
|
|
62
|
+
outputCode: "RUNTIME",
|
|
63
|
+
detailCode: "AGENT_STARTUP_FAILED",
|
|
64
|
+
origin: "acp"
|
|
65
|
+
});
|
|
66
|
+
this.agentCommand = params.agentCommand;
|
|
67
|
+
this.exitCode = params.exitCode;
|
|
68
|
+
this.signal = params.signal;
|
|
69
|
+
this.stderrSummary = params.stderrSummary?.trim() || void 0;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var AgentDisconnectedError = class extends AcpxOperationalError {
|
|
73
|
+
reason;
|
|
74
|
+
exitCode;
|
|
75
|
+
signal;
|
|
76
|
+
constructor(reason, exitCode, signal, options) {
|
|
77
|
+
super(`ACP agent disconnected during request (${reason}, exit=${exitCode ?? "null"}, signal=${signal ?? "null"})`, {
|
|
78
|
+
outputCode: "RUNTIME",
|
|
79
|
+
detailCode: "AGENT_DISCONNECTED",
|
|
80
|
+
origin: "acp",
|
|
81
|
+
...options
|
|
82
|
+
});
|
|
83
|
+
this.reason = reason;
|
|
84
|
+
this.exitCode = exitCode;
|
|
85
|
+
this.signal = signal;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var UnsupportedPromptContentError = class extends AcpxOperationalError {
|
|
89
|
+
constructor(message) {
|
|
90
|
+
super(message, {
|
|
91
|
+
outputCode: "USAGE",
|
|
92
|
+
detailCode: "UNSUPPORTED_PROMPT_CONTENT",
|
|
93
|
+
origin: "acp"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var SessionResumeRequiredError = class extends AcpxOperationalError {
|
|
98
|
+
constructor(message, options) {
|
|
99
|
+
super(message, {
|
|
100
|
+
outputCode: "RUNTIME",
|
|
101
|
+
detailCode: "SESSION_RESUME_REQUIRED",
|
|
102
|
+
origin: "acp",
|
|
103
|
+
retryable: true,
|
|
104
|
+
...options
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var GeminiAcpStartupTimeoutError = class extends AcpxOperationalError {
|
|
109
|
+
constructor(message, options) {
|
|
110
|
+
super(message, {
|
|
111
|
+
outputCode: "TIMEOUT",
|
|
112
|
+
detailCode: "GEMINI_ACP_STARTUP_TIMEOUT",
|
|
113
|
+
origin: "acp",
|
|
114
|
+
...options
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var SessionModeReplayError = class extends AcpxOperationalError {
|
|
119
|
+
constructor(message, options) {
|
|
120
|
+
super(message, {
|
|
121
|
+
outputCode: "RUNTIME",
|
|
122
|
+
detailCode: "SESSION_MODE_REPLAY_FAILED",
|
|
123
|
+
origin: "acp",
|
|
124
|
+
...options
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var SessionModelReplayError = class extends AcpxOperationalError {
|
|
129
|
+
constructor(message, options) {
|
|
130
|
+
super(message, {
|
|
131
|
+
outputCode: "RUNTIME",
|
|
132
|
+
detailCode: "SESSION_MODEL_REPLAY_FAILED",
|
|
133
|
+
origin: "acp",
|
|
134
|
+
...options
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var SessionConfigOptionReplayError = class extends AcpxOperationalError {
|
|
139
|
+
constructor(message, options) {
|
|
140
|
+
super(message, {
|
|
141
|
+
outputCode: "RUNTIME",
|
|
142
|
+
detailCode: "SESSION_CONFIG_OPTION_REPLAY_FAILED",
|
|
143
|
+
origin: "acp",
|
|
144
|
+
...options
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var ClaudeAcpSessionCreateTimeoutError = class extends AcpxOperationalError {
|
|
149
|
+
constructor(message, options) {
|
|
150
|
+
super(message, {
|
|
151
|
+
outputCode: "TIMEOUT",
|
|
152
|
+
detailCode: "CLAUDE_ACP_SESSION_CREATE_TIMEOUT",
|
|
153
|
+
origin: "acp",
|
|
154
|
+
...options
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var CopilotAcpUnsupportedError = class extends AcpxOperationalError {
|
|
159
|
+
constructor(message, options) {
|
|
160
|
+
super(message, {
|
|
161
|
+
outputCode: "RUNTIME",
|
|
162
|
+
detailCode: "COPILOT_ACP_UNSUPPORTED",
|
|
163
|
+
origin: "acp",
|
|
164
|
+
...options
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var AuthPolicyError = class extends AcpxOperationalError {
|
|
169
|
+
constructor(message, options) {
|
|
170
|
+
super(message, {
|
|
171
|
+
outputCode: "RUNTIME",
|
|
172
|
+
detailCode: "AUTH_REQUIRED",
|
|
173
|
+
origin: "acp",
|
|
174
|
+
...options
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var QueueConnectionError = class extends AcpxOperationalError {};
|
|
179
|
+
var QueueProtocolError = class extends AcpxOperationalError {};
|
|
180
|
+
var PermissionDeniedError = class extends AcpxOperationalError {};
|
|
181
|
+
var PermissionPromptUnavailableError = class extends AcpxOperationalError {
|
|
182
|
+
constructor() {
|
|
183
|
+
super("Permission prompt unavailable in non-interactive mode");
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/types.ts
|
|
188
|
+
const EXIT_CODES = {
|
|
189
|
+
SUCCESS: 0,
|
|
190
|
+
ERROR: 1,
|
|
191
|
+
USAGE: 2,
|
|
192
|
+
TIMEOUT: 3,
|
|
193
|
+
NO_SESSION: 4,
|
|
194
|
+
PERMISSION_DENIED: 5,
|
|
195
|
+
INTERRUPTED: 130
|
|
196
|
+
};
|
|
197
|
+
const OUTPUT_FORMATS = [
|
|
198
|
+
"text",
|
|
199
|
+
"json",
|
|
200
|
+
"quiet"
|
|
201
|
+
];
|
|
202
|
+
const PERMISSION_MODES = [
|
|
203
|
+
"approve-all",
|
|
204
|
+
"approve-reads",
|
|
205
|
+
"deny-all"
|
|
206
|
+
];
|
|
207
|
+
const AUTH_POLICIES = ["skip", "fail"];
|
|
208
|
+
const NON_INTERACTIVE_PERMISSION_POLICIES = ["deny", "fail"];
|
|
209
|
+
const PERMISSION_POLICY_ACTIONS = [
|
|
210
|
+
"approve",
|
|
211
|
+
"deny",
|
|
212
|
+
"escalate"
|
|
213
|
+
];
|
|
214
|
+
const OUTPUT_ERROR_CODES = [
|
|
215
|
+
"NO_SESSION",
|
|
216
|
+
"TIMEOUT",
|
|
217
|
+
"PERMISSION_DENIED",
|
|
218
|
+
"PERMISSION_PROMPT_UNAVAILABLE",
|
|
219
|
+
"RUNTIME",
|
|
220
|
+
"USAGE"
|
|
221
|
+
];
|
|
222
|
+
const OUTPUT_ERROR_ORIGINS = [
|
|
223
|
+
"cli",
|
|
224
|
+
"runtime",
|
|
225
|
+
"queue",
|
|
226
|
+
"acp"
|
|
227
|
+
];
|
|
228
|
+
const SESSION_RECORD_SCHEMA = "acpx.session.v1";
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/acp/error-shapes.ts
|
|
231
|
+
const RESOURCE_NOT_FOUND_ACP_CODES = /* @__PURE__ */ new Set([-32001, -32002]);
|
|
232
|
+
function asRecord$5(value) {
|
|
233
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
function toAcpErrorPayload(value) {
|
|
237
|
+
const record = asRecord$5(value);
|
|
238
|
+
if (!record) return;
|
|
239
|
+
if (typeof record.code !== "number" || !Number.isFinite(record.code)) return;
|
|
240
|
+
if (typeof record.message !== "string" || record.message.length === 0) return;
|
|
241
|
+
return {
|
|
242
|
+
code: record.code,
|
|
243
|
+
message: record.message,
|
|
244
|
+
data: record.data
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function extractAcpErrorInternal(value, depth) {
|
|
248
|
+
if (depth > 5) return;
|
|
249
|
+
const direct = toAcpErrorPayload(value);
|
|
250
|
+
if (direct) return direct;
|
|
251
|
+
const record = asRecord$5(value);
|
|
252
|
+
if (!record) return;
|
|
253
|
+
return extractNestedAcpError(record, depth);
|
|
254
|
+
}
|
|
255
|
+
function extractNestedAcpError(record, depth) {
|
|
256
|
+
for (const key of [
|
|
257
|
+
"error",
|
|
258
|
+
"acp",
|
|
259
|
+
"cause"
|
|
260
|
+
]) if (key in record) {
|
|
261
|
+
const nested = extractAcpErrorInternal(record[key], depth + 1);
|
|
262
|
+
if (nested) return nested;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function formatUnknownErrorMessage(error) {
|
|
266
|
+
if (error instanceof Error) return error.message;
|
|
267
|
+
if (error && typeof error === "object") {
|
|
268
|
+
const maybeMessage = error.message;
|
|
269
|
+
if (typeof maybeMessage === "string" && maybeMessage.length > 0) return maybeMessage;
|
|
270
|
+
try {
|
|
271
|
+
return JSON.stringify(error);
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
return String(error);
|
|
275
|
+
}
|
|
276
|
+
const SESSION_NOT_FOUND_PATTERN = /session\s+["'\w-]+\s+not found/i;
|
|
277
|
+
function isSessionNotFoundText(value) {
|
|
278
|
+
if (typeof value !== "string") return false;
|
|
279
|
+
const normalized = value.toLowerCase();
|
|
280
|
+
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") || SESSION_NOT_FOUND_PATTERN.test(value);
|
|
281
|
+
}
|
|
282
|
+
function hasSessionNotFoundHint(value, depth = 0) {
|
|
283
|
+
if (depth > 4) return false;
|
|
284
|
+
if (isSessionNotFoundText(value)) return true;
|
|
285
|
+
if (Array.isArray(value)) return value.some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
286
|
+
const record = asRecord$5(value);
|
|
287
|
+
if (!record) return false;
|
|
288
|
+
return Object.values(record).some((entry) => hasSessionNotFoundHint(entry, depth + 1));
|
|
289
|
+
}
|
|
290
|
+
function extractAcpError(error) {
|
|
291
|
+
return extractAcpErrorInternal(error, 0);
|
|
292
|
+
}
|
|
293
|
+
function isAcpResourceNotFoundError(error) {
|
|
294
|
+
const acp = extractAcpError(error);
|
|
295
|
+
if (acp && RESOURCE_NOT_FOUND_ACP_CODES.has(acp.code)) return true;
|
|
296
|
+
if (acp) {
|
|
297
|
+
if (isSessionNotFoundText(acp.message)) return true;
|
|
298
|
+
if (hasSessionNotFoundHint(acp.data)) return true;
|
|
299
|
+
}
|
|
300
|
+
return isSessionNotFoundText(formatUnknownErrorMessage(error));
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/process-liveness.ts
|
|
304
|
+
function isProcessAlive(pid) {
|
|
305
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
306
|
+
try {
|
|
307
|
+
process.kill(pid, 0);
|
|
308
|
+
return true;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region \0@oxc-project+runtime@0.149.0/helpers/esm/usingCtx.js
|
|
315
|
+
function _usingCtx() {
|
|
316
|
+
var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
|
|
317
|
+
var n = Error();
|
|
318
|
+
return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
|
|
319
|
+
}, e = {}, n = [];
|
|
320
|
+
function using(r, e) {
|
|
321
|
+
if (null != e) {
|
|
322
|
+
if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
|
|
323
|
+
if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
|
|
324
|
+
if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
|
|
325
|
+
if ("function" != typeof o) throw new TypeError("Object is not disposable.");
|
|
326
|
+
t && (o = function o() {
|
|
327
|
+
try {
|
|
328
|
+
t.call(e);
|
|
329
|
+
} catch (r) {
|
|
330
|
+
return Promise.reject(r);
|
|
331
|
+
}
|
|
332
|
+
}), n.push({
|
|
333
|
+
v: e,
|
|
334
|
+
d: o,
|
|
335
|
+
a: r
|
|
336
|
+
});
|
|
337
|
+
} else r && n.push({
|
|
338
|
+
d: e,
|
|
339
|
+
a: r
|
|
340
|
+
});
|
|
341
|
+
return e;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
e,
|
|
345
|
+
u: using.bind(null, !1),
|
|
346
|
+
a: using.bind(null, !0),
|
|
347
|
+
d: function d() {
|
|
348
|
+
var o, t = this.e, s = 0;
|
|
349
|
+
function next() {
|
|
350
|
+
for (; o = n.pop();) try {
|
|
351
|
+
if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
|
|
352
|
+
if (o.d) {
|
|
353
|
+
var r = o.d.call(o.v);
|
|
354
|
+
if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
|
|
355
|
+
} else s |= 1;
|
|
356
|
+
} catch (r) {
|
|
357
|
+
return err(r);
|
|
358
|
+
}
|
|
359
|
+
if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
|
|
360
|
+
if (t !== e) throw t;
|
|
361
|
+
}
|
|
362
|
+
function err(n) {
|
|
363
|
+
return t = t !== e ? new r(n, t) : n, next();
|
|
364
|
+
}
|
|
365
|
+
return next();
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/state-files.ts
|
|
371
|
+
async function writePrivateFile(filePath, content, options = {}) {
|
|
372
|
+
try {
|
|
373
|
+
var _usingCtx$1 = _usingCtx();
|
|
374
|
+
const privateDirectory = options.privateDirectory !== false;
|
|
375
|
+
await fs$1.mkdir(path.dirname(filePath), {
|
|
376
|
+
recursive: true,
|
|
377
|
+
mode: privateDirectory ? 448 : void 0
|
|
378
|
+
});
|
|
379
|
+
const directory = await fs$1.realpath(path.dirname(filePath));
|
|
380
|
+
if (privateDirectory && process.platform !== "win32") await fs$1.chmod(directory, 448);
|
|
381
|
+
const temporary = _usingCtx$1.a(await tempFile({
|
|
382
|
+
rootDir: directory,
|
|
383
|
+
prefix: "acpx-write",
|
|
384
|
+
fileName: "record.json"
|
|
385
|
+
}));
|
|
386
|
+
const handle = await fs$1.open(temporary.path, "wx", 384);
|
|
387
|
+
try {
|
|
388
|
+
await handle.writeFile(content, "utf8");
|
|
389
|
+
await handle.chmod(384);
|
|
390
|
+
} finally {
|
|
391
|
+
await handle.close();
|
|
392
|
+
}
|
|
393
|
+
await fs$1.rename(temporary.path, path.join(directory, path.basename(filePath)));
|
|
394
|
+
} catch (_) {
|
|
395
|
+
_usingCtx$1.e = _;
|
|
396
|
+
} finally {
|
|
397
|
+
await _usingCtx$1.d();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function writePrivateJsonFile(filePath, value) {
|
|
401
|
+
await writePrivateFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/session/event-log.ts
|
|
405
|
+
const DEFAULT_EVENT_SEGMENT_MAX_BYTES = 67108864;
|
|
406
|
+
function sessionBaseDir() {
|
|
407
|
+
return path.join(os.homedir(), ".acpx", "sessions");
|
|
408
|
+
}
|
|
409
|
+
function safeSessionId(sessionId) {
|
|
410
|
+
return encodeURIComponent(sessionId);
|
|
411
|
+
}
|
|
412
|
+
function sessionEventActivePath(sessionId) {
|
|
413
|
+
return path.join(sessionBaseDir(), `${safeSessionId(sessionId)}.stream.ndjson`);
|
|
414
|
+
}
|
|
415
|
+
function sessionEventSegmentPath(sessionId, segment) {
|
|
416
|
+
return path.join(sessionBaseDir(), `${safeSessionId(sessionId)}.stream.${segment}.ndjson`);
|
|
417
|
+
}
|
|
418
|
+
function sessionEventLockPath(sessionId) {
|
|
419
|
+
return path.join(sessionBaseDir(), `${safeSessionId(sessionId)}.stream.lock`);
|
|
420
|
+
}
|
|
421
|
+
function defaultSessionEventLog(sessionId) {
|
|
422
|
+
return {
|
|
423
|
+
active_path: sessionEventActivePath(sessionId),
|
|
424
|
+
segment_count: 5,
|
|
425
|
+
max_segment_bytes: DEFAULT_EVENT_SEGMENT_MAX_BYTES,
|
|
426
|
+
max_segments: 5,
|
|
427
|
+
last_write_at: void 0,
|
|
428
|
+
last_write_error: null
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/acp/jsonrpc.ts
|
|
433
|
+
function asRecord$4(value) {
|
|
434
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
function isAcpMessageObject(value) {
|
|
438
|
+
return asRecord$4(value) !== null;
|
|
439
|
+
}
|
|
440
|
+
function hasValidId(value) {
|
|
441
|
+
return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
442
|
+
}
|
|
443
|
+
function isErrorObject(value) {
|
|
444
|
+
const record = asRecord$4(value);
|
|
445
|
+
return !!record && typeof record.code === "number" && Number.isFinite(record.code) && typeof record.message === "string";
|
|
446
|
+
}
|
|
447
|
+
function hasResultOrError(value) {
|
|
448
|
+
const hasResult = Object.hasOwn(value, "result");
|
|
449
|
+
const hasError = Object.hasOwn(value, "error");
|
|
450
|
+
if (hasResult && hasError) return false;
|
|
451
|
+
if (!hasResult && !hasError) return false;
|
|
452
|
+
if (hasError && !isErrorObject(value.error)) return false;
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
function hasMethod(value) {
|
|
456
|
+
return typeof value.method === "string" && value.method.length > 0;
|
|
457
|
+
}
|
|
458
|
+
function isJsonRpcRequest(value) {
|
|
459
|
+
return hasMethod(value) && Object.hasOwn(value, "id") && hasValidId(value.id);
|
|
460
|
+
}
|
|
461
|
+
function isJsonRpcNotificationRecord(value) {
|
|
462
|
+
return hasMethod(value) && !Object.hasOwn(value, "id");
|
|
463
|
+
}
|
|
464
|
+
function isJsonRpcResponse(value) {
|
|
465
|
+
if (hasMethod(value) || !Object.hasOwn(value, "id") || !hasValidId(value.id)) return false;
|
|
466
|
+
return hasResultOrError(value);
|
|
467
|
+
}
|
|
468
|
+
function isAcpJsonRpcMessage(value) {
|
|
469
|
+
const record = asRecord$4(value);
|
|
470
|
+
if (!record || record.jsonrpc !== "2.0") return false;
|
|
471
|
+
return isJsonRpcNotificationRecord(record) || isJsonRpcRequest(record) || isJsonRpcResponse(record);
|
|
472
|
+
}
|
|
473
|
+
function isJsonRpcNotification(message) {
|
|
474
|
+
return Object.hasOwn(message, "method") && typeof message.method === "string" && !Object.hasOwn(message, "id");
|
|
475
|
+
}
|
|
476
|
+
function isSessionUpdateNotification(message) {
|
|
477
|
+
return isJsonRpcNotification(message) && message.method === "session/update";
|
|
478
|
+
}
|
|
479
|
+
function extractSessionUpdateNotification(message) {
|
|
480
|
+
if (!isSessionUpdateNotification(message)) return;
|
|
481
|
+
const params = asRecord$4(message.params);
|
|
482
|
+
if (!params) return;
|
|
483
|
+
const sessionId = typeof params.sessionId === "string" ? params.sessionId : null;
|
|
484
|
+
if (!sessionId) return;
|
|
485
|
+
const update = asRecord$4(params.update);
|
|
486
|
+
if (!update || typeof update.sessionUpdate !== "string") return;
|
|
487
|
+
return {
|
|
488
|
+
sessionId,
|
|
489
|
+
update
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function parsePromptStopReason(message) {
|
|
493
|
+
if (!Object.hasOwn(message, "id") || !Object.hasOwn(message, "result")) return;
|
|
494
|
+
const record = asRecord$4(message.result);
|
|
495
|
+
if (!record) return;
|
|
496
|
+
return typeof record.stopReason === "string" ? record.stopReason : void 0;
|
|
497
|
+
}
|
|
498
|
+
function parsePermissionNotice(message) {
|
|
499
|
+
if (!Object.hasOwn(message, "id") || !Object.hasOwn(message, "result")) return;
|
|
500
|
+
const notice = asRecord$4(asRecord$4(asRecord$4(message.result)?._meta)?.acpx)?.permissionNotice;
|
|
501
|
+
return typeof notice === "string" ? notice : void 0;
|
|
502
|
+
}
|
|
503
|
+
function parseJsonRpcErrorMessage(message) {
|
|
504
|
+
if (!Object.hasOwn(message, "error")) return;
|
|
505
|
+
const errorRecord = asRecord$4(message.error);
|
|
506
|
+
if (!errorRecord || typeof errorRecord.message !== "string") return;
|
|
507
|
+
return errorRecord.message;
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/perf-metrics.ts
|
|
511
|
+
const counters = /* @__PURE__ */ new Map();
|
|
512
|
+
const gauges = /* @__PURE__ */ new Map();
|
|
513
|
+
const timings = /* @__PURE__ */ new Map();
|
|
514
|
+
function hrNow() {
|
|
515
|
+
return process.hrtime.bigint();
|
|
516
|
+
}
|
|
517
|
+
function durationMs(start) {
|
|
518
|
+
return Number(process.hrtime.bigint() - start) / 1e6;
|
|
519
|
+
}
|
|
520
|
+
function roundMetric(value) {
|
|
521
|
+
return Number(value.toFixed(3));
|
|
522
|
+
}
|
|
523
|
+
function incrementPerfCounter(name, delta = 1) {
|
|
524
|
+
counters.set(name, (counters.get(name) ?? 0) + delta);
|
|
525
|
+
}
|
|
526
|
+
function setPerfGauge(name, value) {
|
|
527
|
+
gauges.set(name, value);
|
|
528
|
+
}
|
|
529
|
+
function recordPerfDuration(name, durationMsValue) {
|
|
530
|
+
const next = timings.get(name) ?? {
|
|
531
|
+
count: 0,
|
|
532
|
+
totalMs: 0,
|
|
533
|
+
maxMs: 0
|
|
534
|
+
};
|
|
535
|
+
next.count += 1;
|
|
536
|
+
next.totalMs += durationMsValue;
|
|
537
|
+
next.maxMs = Math.max(next.maxMs, durationMsValue);
|
|
538
|
+
timings.set(name, next);
|
|
539
|
+
}
|
|
540
|
+
async function measurePerf(name, run) {
|
|
541
|
+
const startedAt = hrNow();
|
|
542
|
+
try {
|
|
543
|
+
return await run();
|
|
544
|
+
} finally {
|
|
545
|
+
recordPerfDuration(name, durationMs(startedAt));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function startPerfTimer(name) {
|
|
549
|
+
const startedAt = hrNow();
|
|
550
|
+
return () => {
|
|
551
|
+
const elapsedMs = durationMs(startedAt);
|
|
552
|
+
recordPerfDuration(name, elapsedMs);
|
|
553
|
+
return elapsedMs;
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function getPerfMetricsSnapshot() {
|
|
557
|
+
return {
|
|
558
|
+
counters: Object.fromEntries(counters.entries()),
|
|
559
|
+
gauges: Object.fromEntries(gauges.entries()),
|
|
560
|
+
timings: Object.fromEntries([...timings.entries()].map(([name, bucket]) => [name, {
|
|
561
|
+
count: bucket.count,
|
|
562
|
+
totalMs: roundMetric(bucket.totalMs),
|
|
563
|
+
maxMs: roundMetric(bucket.maxMs)
|
|
564
|
+
}]))
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function resetPerfMetrics() {
|
|
568
|
+
counters.clear();
|
|
569
|
+
gauges.clear();
|
|
570
|
+
timings.clear();
|
|
571
|
+
}
|
|
572
|
+
function formatPerfMetric(name, durationMsValue) {
|
|
573
|
+
return `${name}=${roundMetric(durationMsValue)}ms`;
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/session/journal.ts
|
|
577
|
+
const SESSION_JOURNAL_SCHEMA = "acpx.session.journal.v1";
|
|
578
|
+
const sequenceSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
579
|
+
const resultSchema = z.union([z.object({
|
|
580
|
+
status: z.enum(["completed", "cancelled"]),
|
|
581
|
+
stopReason: z.string().optional(),
|
|
582
|
+
_meta: z.record(z.string(), z.unknown()).nullable().optional()
|
|
583
|
+
}), z.object({
|
|
584
|
+
status: z.literal("failed"),
|
|
585
|
+
error: z.object({
|
|
586
|
+
message: z.string(),
|
|
587
|
+
code: z.string().optional(),
|
|
588
|
+
detailCode: z.string().optional(),
|
|
589
|
+
retryable: z.boolean().optional()
|
|
590
|
+
})
|
|
591
|
+
})]);
|
|
592
|
+
const markerSchema = z.discriminatedUnion("type", [
|
|
593
|
+
z.object({
|
|
594
|
+
schema: z.literal(SESSION_JOURNAL_SCHEMA),
|
|
595
|
+
type: z.literal("segment"),
|
|
596
|
+
record_id: z.string(),
|
|
597
|
+
sequence: sequenceSchema,
|
|
598
|
+
message_sequence: sequenceSchema,
|
|
599
|
+
request_id: z.string().nullable()
|
|
600
|
+
}),
|
|
601
|
+
z.object({
|
|
602
|
+
schema: z.literal(SESSION_JOURNAL_SCHEMA),
|
|
603
|
+
type: z.literal("turn_started"),
|
|
604
|
+
request_id: z.string().min(1)
|
|
605
|
+
}),
|
|
606
|
+
z.object({
|
|
607
|
+
schema: z.literal(SESSION_JOURNAL_SCHEMA),
|
|
608
|
+
type: z.literal("turn_result"),
|
|
609
|
+
request_id: z.string().min(1),
|
|
610
|
+
result: resultSchema
|
|
611
|
+
})
|
|
612
|
+
]);
|
|
613
|
+
const WATCH_PAGE_BYTES = 1048576;
|
|
614
|
+
function createJournalPage(options = {}) {
|
|
615
|
+
const maxBytes = options.maxBytes ?? Infinity;
|
|
616
|
+
if (!(maxBytes > 0)) throw new Error("Session journal page size must be positive");
|
|
617
|
+
return {
|
|
618
|
+
events: [],
|
|
619
|
+
after: options.after ?? -1,
|
|
620
|
+
bytes: 0,
|
|
621
|
+
maxBytes
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function pageFull(page) {
|
|
625
|
+
return page !== void 0 && page.bytes >= page.maxBytes;
|
|
626
|
+
}
|
|
627
|
+
function eventSink(page, sequence, bytes) {
|
|
628
|
+
if (!page || sequence <= page.after) return;
|
|
629
|
+
page.bytes += bytes;
|
|
630
|
+
return page.events;
|
|
631
|
+
}
|
|
632
|
+
var SessionWatchError = class extends AcpxOperationalError {
|
|
633
|
+
code;
|
|
634
|
+
constructor(code, message) {
|
|
635
|
+
super(message, {
|
|
636
|
+
outputCode: code.startsWith("WATCH_CURSOR_") ? "USAGE" : "RUNTIME",
|
|
637
|
+
detailCode: code,
|
|
638
|
+
origin: "runtime",
|
|
639
|
+
retryable: false
|
|
640
|
+
});
|
|
641
|
+
this.code = code;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function corrupt(message) {
|
|
645
|
+
throw new SessionWatchError("WATCH_JOURNAL_CORRUPT", message);
|
|
646
|
+
}
|
|
647
|
+
function sessionWatchCursor(recordId, sequence) {
|
|
648
|
+
return Buffer.from(JSON.stringify([recordId, sequence])).toString("base64url");
|
|
649
|
+
}
|
|
650
|
+
function parseCursor(recordId, cursor) {
|
|
651
|
+
let value;
|
|
652
|
+
try {
|
|
653
|
+
value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
654
|
+
} catch {
|
|
655
|
+
throw new SessionWatchError("WATCH_CURSOR_INVALID", "Invalid session watch cursor");
|
|
656
|
+
}
|
|
657
|
+
const parsed = z.tuple([z.string(), sequenceSchema]).safeParse(value);
|
|
658
|
+
if (!parsed.success || sessionWatchCursor(...parsed.data) !== cursor) throw new SessionWatchError("WATCH_CURSOR_INVALID", "Invalid session watch cursor");
|
|
659
|
+
if (parsed.data[0] !== recordId) throw new SessionWatchError("WATCH_CURSOR_FOREIGN", "Cursor belongs to another session");
|
|
660
|
+
return parsed.data[1];
|
|
661
|
+
}
|
|
662
|
+
function fileIdentity(stat) {
|
|
663
|
+
return `${stat.dev}:${stat.ino}:${stat.birthtimeMs}`;
|
|
664
|
+
}
|
|
665
|
+
function emptySegment() {
|
|
666
|
+
return {
|
|
667
|
+
offset: 0,
|
|
668
|
+
pending: Buffer.alloc(0),
|
|
669
|
+
sequence: 0,
|
|
670
|
+
messageSequence: 0,
|
|
671
|
+
requestId: null
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function retryableSnapshotError(error) {
|
|
675
|
+
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "not-found" || error.code === "path-mismatch");
|
|
676
|
+
}
|
|
677
|
+
var SessionJournalReader = class {
|
|
678
|
+
record;
|
|
679
|
+
states = /* @__PURE__ */ new Map();
|
|
680
|
+
constructor(record) {
|
|
681
|
+
this.record = record;
|
|
682
|
+
}
|
|
683
|
+
paths() {
|
|
684
|
+
const paths = [];
|
|
685
|
+
for (let index = this.record.eventLog.max_segments; index > 0; index -= 1) paths.push(sessionEventSegmentPath(this.record.acpxRecordId, index));
|
|
686
|
+
paths.push(sessionEventActivePath(this.record.acpxRecordId));
|
|
687
|
+
return paths;
|
|
688
|
+
}
|
|
689
|
+
async openSnapshot(signal) {
|
|
690
|
+
for (;;) {
|
|
691
|
+
signal?.throwIfAborted();
|
|
692
|
+
const segments = [];
|
|
693
|
+
try {
|
|
694
|
+
const paths = await this.openPresentSegments(segments);
|
|
695
|
+
const current = await Promise.all(paths.map((filePath) => statRegularFile(filePath)));
|
|
696
|
+
const identities = new Map(segments.map((entry) => [entry.filePath, entry.identity]));
|
|
697
|
+
if (paths.every((filePath, index) => {
|
|
698
|
+
const entry = current[index];
|
|
699
|
+
return (entry.missing ? void 0 : fileIdentity(entry.stat)) === identities.get(filePath);
|
|
700
|
+
})) return segments;
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (!retryableSnapshotError(error)) {
|
|
703
|
+
await this.closeSnapshot(segments);
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
await this.closeSnapshot(segments);
|
|
708
|
+
await setTimeout$1(5, void 0, { signal });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async openPresentSegments(segments) {
|
|
712
|
+
const paths = this.paths();
|
|
713
|
+
const present = await Promise.all(paths.map((filePath) => statRegularFile(filePath)));
|
|
714
|
+
if (present.every((entry) => entry.missing)) return paths;
|
|
715
|
+
const directory = await root(path.dirname(paths[0]));
|
|
716
|
+
const results = await Promise.allSettled(paths.filter((_, index) => !present[index].missing).map(async (filePath) => {
|
|
717
|
+
const opened = await directory.open(path.basename(filePath), {
|
|
718
|
+
symlinks: "reject",
|
|
719
|
+
nonBlockingRead: true
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
filePath,
|
|
723
|
+
opened,
|
|
724
|
+
identity: fileIdentity(opened.stat)
|
|
725
|
+
};
|
|
726
|
+
}));
|
|
727
|
+
let failure;
|
|
728
|
+
for (const result of results) if (result.status === "fulfilled") segments.push(result.value);
|
|
729
|
+
else failure ??= result.reason;
|
|
730
|
+
if (failure) throw failure;
|
|
731
|
+
return paths;
|
|
732
|
+
}
|
|
733
|
+
async closeSnapshot(segments) {
|
|
734
|
+
await Promise.all(segments.map(({ opened }) => opened[Symbol.asyncDispose]()));
|
|
735
|
+
}
|
|
736
|
+
async readAcpMessages() {
|
|
737
|
+
const segments = await this.openSnapshot();
|
|
738
|
+
const messages = [];
|
|
739
|
+
try {
|
|
740
|
+
for (const { opened } of segments) {
|
|
741
|
+
const payload = await opened.handle.readFile("utf8");
|
|
742
|
+
for (const line of payload.split("\n")) try {
|
|
743
|
+
const value = JSON.parse(line);
|
|
744
|
+
if (isAcpJsonRpcMessage(value)) messages.push(value);
|
|
745
|
+
} catch {}
|
|
746
|
+
}
|
|
747
|
+
return messages;
|
|
748
|
+
} finally {
|
|
749
|
+
await this.closeSnapshot(segments);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async readTail() {
|
|
753
|
+
const segments = await this.openSnapshot();
|
|
754
|
+
let tail = emptySegment();
|
|
755
|
+
let active = emptySegment();
|
|
756
|
+
try {
|
|
757
|
+
for (const segment of segments.toReversed()) {
|
|
758
|
+
const state = emptySegment();
|
|
759
|
+
await this.readSegment(segment.opened.handle, segment.opened.stat.size, state, void 0);
|
|
760
|
+
if (segment.filePath === sessionEventActivePath(this.record.acpxRecordId)) active = state;
|
|
761
|
+
if (state.firstSequence !== void 0) {
|
|
762
|
+
tail = state;
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
sequence: tail.sequence,
|
|
768
|
+
messageSequence: tail.firstSequence === void 0 ? void 0 : tail.messageSequence,
|
|
769
|
+
requestId: tail.requestId,
|
|
770
|
+
activeSize: active.offset,
|
|
771
|
+
activeAnchored: active.firstSequence !== void 0,
|
|
772
|
+
activePartial: active.pending.length > 0
|
|
773
|
+
};
|
|
774
|
+
} finally {
|
|
775
|
+
await this.closeSnapshot(segments);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async read(signal, options) {
|
|
779
|
+
const page = createJournalPage(options);
|
|
780
|
+
const segments = await this.openSnapshot(signal);
|
|
781
|
+
let hasMore = false;
|
|
782
|
+
let firstSequence;
|
|
783
|
+
let tail = emptySegment();
|
|
784
|
+
let active = emptySegment();
|
|
785
|
+
try {
|
|
786
|
+
for (const segment of segments) {
|
|
787
|
+
const state = this.states.get(segment.identity) ?? emptySegment();
|
|
788
|
+
await this.readSegment(segment.opened.handle, segment.opened.stat.size, state, page, signal);
|
|
789
|
+
this.states.set(segment.identity, state);
|
|
790
|
+
if (state.firstSequence !== void 0) {
|
|
791
|
+
if (firstSequence === void 0) firstSequence = state.firstSequence;
|
|
792
|
+
else if (state.firstSequence !== tail.sequence) corrupt("Session journal segments are not contiguous");
|
|
793
|
+
tail = state;
|
|
794
|
+
}
|
|
795
|
+
if (segment.filePath === sessionEventActivePath(this.record.acpxRecordId)) active = state;
|
|
796
|
+
if (pageFull(page)) {
|
|
797
|
+
hasMore = true;
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
this.pruneStates(segments);
|
|
802
|
+
return {
|
|
803
|
+
events: page.events,
|
|
804
|
+
hasMore,
|
|
805
|
+
firstSequence,
|
|
806
|
+
sequence: tail.sequence,
|
|
807
|
+
messageSequence: tail.messageSequence,
|
|
808
|
+
requestId: tail.requestId,
|
|
809
|
+
activeSize: active.offset,
|
|
810
|
+
activeAnchored: active.firstSequence !== void 0,
|
|
811
|
+
activePartial: active.pending.length > 0
|
|
812
|
+
};
|
|
813
|
+
} finally {
|
|
814
|
+
await this.closeSnapshot(segments);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
pruneStates(segments) {
|
|
818
|
+
const retained = new Set(segments.map((entry) => entry.identity));
|
|
819
|
+
for (const identity of this.states.keys()) if (!retained.has(identity)) this.states.delete(identity);
|
|
820
|
+
}
|
|
821
|
+
async readSegment(handle, size, state, page, signal) {
|
|
822
|
+
if (size < state.offset) corrupt("Session journal was truncated while watching");
|
|
823
|
+
const buffer = Buffer.alloc(Math.min(65536, size - state.offset));
|
|
824
|
+
for (;;) {
|
|
825
|
+
signal?.throwIfAborted();
|
|
826
|
+
this.consumePending(state, page);
|
|
827
|
+
if (pageFull(page) || state.offset >= size) return;
|
|
828
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, size - state.offset), state.offset);
|
|
829
|
+
if (bytesRead === 0) corrupt("Session journal changed while reading");
|
|
830
|
+
state.offset += bytesRead;
|
|
831
|
+
state.pending = Buffer.concat([state.pending, buffer.subarray(0, bytesRead)]);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
consumePending(state, page) {
|
|
835
|
+
const pending = state.pending;
|
|
836
|
+
let start = 0;
|
|
837
|
+
for (let end = pending.indexOf(10); end >= 0; end = pending.indexOf(10, start)) {
|
|
838
|
+
this.consumeLine(pending.subarray(start, end).toString("utf8"), state, page, end - start + 1);
|
|
839
|
+
start = end + 1;
|
|
840
|
+
if (pageFull(page)) break;
|
|
841
|
+
}
|
|
842
|
+
state.pending = Buffer.from(pending.subarray(start));
|
|
843
|
+
}
|
|
844
|
+
consumeLine(line, state, page, bytes) {
|
|
845
|
+
const entry = decodeJournalLine(line, state.firstSequence !== void 0);
|
|
846
|
+
if (!entry) return;
|
|
847
|
+
if (entry.type === "marker") {
|
|
848
|
+
this.consumeMarker(entry.marker, state, page, bytes);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (state.firstSequence === void 0) return;
|
|
852
|
+
this.advance(state);
|
|
853
|
+
state.messageSequence += 1;
|
|
854
|
+
eventSink(page, state.sequence, bytes)?.push({
|
|
855
|
+
...this.eventPosition(state),
|
|
856
|
+
type: "message",
|
|
857
|
+
requestId: state.requestId,
|
|
858
|
+
message: entry.message
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
advance(state) {
|
|
862
|
+
state.sequence += 1;
|
|
863
|
+
if (!Number.isSafeInteger(state.sequence)) corrupt("Session journal sequence exceeds its supported range");
|
|
864
|
+
}
|
|
865
|
+
eventPosition(state) {
|
|
866
|
+
return {
|
|
867
|
+
sequence: state.sequence,
|
|
868
|
+
cursor: sessionWatchCursor(this.record.acpxRecordId, state.sequence)
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
consumeMarker(marker, state, page, bytes) {
|
|
872
|
+
if (marker.type === "segment") {
|
|
873
|
+
this.acceptAnchor(marker, state);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (state.firstSequence === void 0) corrupt("Session journal lifecycle record has no segment anchor");
|
|
877
|
+
this.advance(state);
|
|
878
|
+
if (marker.type === "turn_started") {
|
|
879
|
+
state.requestId = marker.request_id;
|
|
880
|
+
eventSink(page, state.sequence, bytes)?.push({
|
|
881
|
+
...this.eventPosition(state),
|
|
882
|
+
type: "turn_started",
|
|
883
|
+
requestId: state.requestId
|
|
884
|
+
});
|
|
885
|
+
} else {
|
|
886
|
+
if (state.requestId !== marker.request_id) corrupt("Session journal result does not match the active request");
|
|
887
|
+
eventSink(page, state.sequence, bytes)?.push({
|
|
888
|
+
...this.eventPosition(state),
|
|
889
|
+
type: "turn_result",
|
|
890
|
+
requestId: state.requestId,
|
|
891
|
+
result: marker.result
|
|
892
|
+
});
|
|
893
|
+
state.requestId = null;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
acceptAnchor(anchor, state) {
|
|
897
|
+
if (state.firstSequence !== void 0 || anchor.record_id !== this.record.acpxRecordId) corrupt("Invalid session journal segment anchor");
|
|
898
|
+
state.firstSequence = anchor.sequence;
|
|
899
|
+
state.sequence = anchor.sequence;
|
|
900
|
+
state.messageSequence = anchor.message_sequence;
|
|
901
|
+
state.requestId = anchor.request_id;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
function decodeJournalLine(line, anchored) {
|
|
905
|
+
let value;
|
|
906
|
+
try {
|
|
907
|
+
value = JSON.parse(line);
|
|
908
|
+
} catch {
|
|
909
|
+
if (anchored) corrupt("Invalid complete line in session journal");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (isAcpJsonRpcMessage(value)) return {
|
|
913
|
+
type: "message",
|
|
914
|
+
message: value
|
|
915
|
+
};
|
|
916
|
+
const marker = markerSchema.safeParse(value);
|
|
917
|
+
if (marker.success) return {
|
|
918
|
+
type: "marker",
|
|
919
|
+
marker: marker.data
|
|
920
|
+
};
|
|
921
|
+
if (anchored || hasJournalSchema(value)) corrupt("Invalid record in session journal");
|
|
922
|
+
}
|
|
923
|
+
function hasJournalSchema(value) {
|
|
924
|
+
return value !== null && typeof value === "object" && "schema" in value;
|
|
925
|
+
}
|
|
926
|
+
function watchSession(options) {
|
|
927
|
+
return { [Symbol.asyncIterator]() {
|
|
928
|
+
const stopped = new AbortController();
|
|
929
|
+
const signal = options.signal ? AbortSignal.any([options.signal, stopped.signal]) : stopped.signal;
|
|
930
|
+
const iterator = iterateSession({
|
|
931
|
+
...options,
|
|
932
|
+
signal
|
|
933
|
+
});
|
|
934
|
+
return {
|
|
935
|
+
next: () => iterator.next(),
|
|
936
|
+
return: async () => {
|
|
937
|
+
stopped.abort();
|
|
938
|
+
return await iterator.return();
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
} };
|
|
942
|
+
}
|
|
943
|
+
async function* iterateSession(options) {
|
|
944
|
+
const { signal } = options;
|
|
945
|
+
if (signal.aborted) return;
|
|
946
|
+
const record = await settleUnlessAborted(Promise.resolve(options.record), signal);
|
|
947
|
+
if (!record) return;
|
|
948
|
+
yield* iterateJournal(record, options);
|
|
949
|
+
}
|
|
950
|
+
async function* iterateJournal(record, options) {
|
|
951
|
+
const { signal } = options;
|
|
952
|
+
let after = options.cursor === void 0 ? -1 : parseCursor(record.acpxRecordId, options.cursor);
|
|
953
|
+
const reader = new SessionJournalReader(record);
|
|
954
|
+
let first = true;
|
|
955
|
+
for (;;) {
|
|
956
|
+
if (signal.aborted) return;
|
|
957
|
+
const snapshot = await settleUnlessAborted(reader.read(signal, {
|
|
958
|
+
after,
|
|
959
|
+
maxBytes: WATCH_PAGE_BYTES
|
|
960
|
+
}), signal);
|
|
961
|
+
if (!snapshot) return;
|
|
962
|
+
assertRetainedCursor(after, snapshot, first);
|
|
963
|
+
first = false;
|
|
964
|
+
for (const { sequence, ...event } of snapshot.events) {
|
|
965
|
+
if (signal.aborted) return;
|
|
966
|
+
after = sequence;
|
|
967
|
+
yield event;
|
|
968
|
+
}
|
|
969
|
+
if (!await continueAfterPage(record, snapshot, options)) return;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function continueAfterPage(record, snapshot, options) {
|
|
973
|
+
if (!snapshot.hasMore && options.continueWatching) {
|
|
974
|
+
if (!await settleUnlessAborted(options.continueWatching(record, snapshot.requestId), options.signal)) return false;
|
|
975
|
+
}
|
|
976
|
+
await settleUnlessAborted(setTimeout$1(snapshot.hasMore ? 0 : 100, void 0, { signal: options.signal }), options.signal);
|
|
977
|
+
return !options.signal.aborted;
|
|
978
|
+
}
|
|
979
|
+
function assertRetainedCursor(after, snapshot, first) {
|
|
980
|
+
if (after < 0) return;
|
|
981
|
+
if (after > snapshot.sequence) throw new SessionWatchError(first ? "WATCH_CURSOR_FUTURE" : "WATCH_CURSOR_EXPIRED", "Cursor is beyond the available session journal");
|
|
982
|
+
if (snapshot.firstSequence !== void 0 && after < snapshot.firstSequence) throw new SessionWatchError("WATCH_CURSOR_EXPIRED", "Cursor is older than retained session history");
|
|
983
|
+
}
|
|
984
|
+
async function settleUnlessAborted(operation, signal) {
|
|
985
|
+
try {
|
|
986
|
+
return await operation;
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (signal?.aborted) return;
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region src/acp/agent-session-id.ts
|
|
994
|
+
const AGENT_SESSION_ID_META_KEYS = ["agentSessionId", "sessionId"];
|
|
995
|
+
function normalizeAgentSessionId(value) {
|
|
996
|
+
if (typeof value !== "string") return;
|
|
997
|
+
const trimmed = value.trim();
|
|
998
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
999
|
+
}
|
|
1000
|
+
function asMetaRecord(meta) {
|
|
1001
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return;
|
|
1002
|
+
return meta;
|
|
1003
|
+
}
|
|
1004
|
+
function extractAgentSessionId(meta) {
|
|
1005
|
+
const record = asMetaRecord(meta);
|
|
1006
|
+
if (!record) return;
|
|
1007
|
+
for (const key of AGENT_SESSION_ID_META_KEYS) {
|
|
1008
|
+
const normalized = normalizeAgentSessionId(record[key]);
|
|
1009
|
+
if (normalized) return normalized;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/session/persistence/serialize.ts
|
|
1014
|
+
function serializeSessionRecordForDisk(record) {
|
|
1015
|
+
return {
|
|
1016
|
+
schema: SESSION_RECORD_SCHEMA,
|
|
1017
|
+
acpx_record_id: record.acpxRecordId,
|
|
1018
|
+
acp_session_id: record.acpSessionId,
|
|
1019
|
+
agent_session_id: normalizeAgentSessionId(record.agentSessionId),
|
|
1020
|
+
agent_command: record.agentCommand,
|
|
1021
|
+
agent_argv: record.agentArgv,
|
|
1022
|
+
cwd: record.cwd,
|
|
1023
|
+
name: record.name,
|
|
1024
|
+
created_at: record.createdAt,
|
|
1025
|
+
last_used_at: record.lastUsedAt,
|
|
1026
|
+
last_seq: record.lastSeq,
|
|
1027
|
+
last_request_id: record.lastRequestId,
|
|
1028
|
+
event_log: record.eventLog,
|
|
1029
|
+
closed: record.closed,
|
|
1030
|
+
closed_at: record.closedAt,
|
|
1031
|
+
pid: record.pid,
|
|
1032
|
+
agent_started_at: record.agentStartedAt,
|
|
1033
|
+
last_prompt_at: record.lastPromptAt,
|
|
1034
|
+
last_agent_exit_code: record.lastAgentExitCode,
|
|
1035
|
+
last_agent_exit_signal: record.lastAgentExitSignal,
|
|
1036
|
+
last_agent_exit_at: record.lastAgentExitAt,
|
|
1037
|
+
last_agent_disconnect_reason: record.lastAgentDisconnectReason,
|
|
1038
|
+
protocol_version: record.protocolVersion,
|
|
1039
|
+
agent_capabilities: record.agentCapabilities,
|
|
1040
|
+
title: record.title,
|
|
1041
|
+
messages: record.messages,
|
|
1042
|
+
updated_at: record.updated_at,
|
|
1043
|
+
cumulative_token_usage: record.cumulative_token_usage,
|
|
1044
|
+
cumulative_cost: record.cumulative_cost,
|
|
1045
|
+
request_token_usage: record.request_token_usage,
|
|
1046
|
+
acpx: record.acpx,
|
|
1047
|
+
imported_from: record.importedFrom ? {
|
|
1048
|
+
record_id: record.importedFrom.recordId,
|
|
1049
|
+
cwd_original: record.importedFrom.cwdOriginal,
|
|
1050
|
+
exported_by: record.importedFrom.exportedBy,
|
|
1051
|
+
exported_at: record.importedFrom.exportedAt
|
|
1052
|
+
} : void 0
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/acp/builtin-command-migration.ts
|
|
1057
|
+
const LEGACY_AGENT_COMMANDS = {
|
|
1058
|
+
pi: [
|
|
1059
|
+
"npx pi-acp",
|
|
1060
|
+
"npx pi-acp@^0.0.22",
|
|
1061
|
+
"npx pi-acp@^0.0.26"
|
|
1062
|
+
],
|
|
1063
|
+
codex: [
|
|
1064
|
+
"npx @zed-industries/codex-acp",
|
|
1065
|
+
"npx @zed-industries/codex-acp@^0.9.5",
|
|
1066
|
+
"npx @zed-industries/codex-acp@^0.10.0",
|
|
1067
|
+
"npx @zed-industries/codex-acp@^0.11.1",
|
|
1068
|
+
"npx @zed-industries/codex-acp@^0.12.0",
|
|
1069
|
+
"npx -y @agentclientprotocol/codex-acp@^0.0.44",
|
|
1070
|
+
"npx -y @agentclientprotocol/codex-acp@^1.1.4"
|
|
1071
|
+
],
|
|
1072
|
+
claude: [
|
|
1073
|
+
"npx @zed-industries/claude-agent-acp",
|
|
1074
|
+
"npx -y @zed-industries/claude-agent-acp",
|
|
1075
|
+
"npx -y @zed-industries/claude-agent-acp@^0.21.0",
|
|
1076
|
+
"npx -y @zed-industries/claude-agent-acp@^0.23.1",
|
|
1077
|
+
"npx -y @zed-industries/claude-agent-acp@^0.24.2",
|
|
1078
|
+
"npx -y @zed-industries/claude-agent-acp@^0.25.0",
|
|
1079
|
+
"npx -y @zed-industries/claude-agent-acp@^0.31.0",
|
|
1080
|
+
"npx -y @agentclientprotocol/claude-agent-acp@^0.36.1",
|
|
1081
|
+
"npx -y @agentclientprotocol/claude-agent-acp@^0.37.0",
|
|
1082
|
+
"npm exec @agentclientprotocol/claude-agent-acp@^0.36.1",
|
|
1083
|
+
"npm exec @agentclientprotocol/claude-agent-acp@^0.37.0"
|
|
1084
|
+
],
|
|
1085
|
+
gemini: ["gemini", "gemini --experimental-acp"],
|
|
1086
|
+
kiro: ["kiro-cli acp"],
|
|
1087
|
+
mux: ["npx -y mux@^0.27.0 acp"],
|
|
1088
|
+
opencode: ["npx opencode-ai"]
|
|
1089
|
+
};
|
|
1090
|
+
function currentArgv(name) {
|
|
1091
|
+
const argv = AGENT_ARGV_REGISTRY[name];
|
|
1092
|
+
return argv ? [...argv] : void 0;
|
|
1093
|
+
}
|
|
1094
|
+
function resolveAgentArgvForCommand(agentCommand) {
|
|
1095
|
+
for (const [name, command] of Object.entries(AGENT_REGISTRY)) if (command === agentCommand) return currentArgv(name);
|
|
1096
|
+
for (const [name, spec] of Object.entries(BUILT_IN_AGENT_PACKAGES)) if ((spec.legacyFallbackCommands ?? []).includes(agentCommand)) return currentArgv(name);
|
|
1097
|
+
for (const [name, commands] of Object.entries(LEGACY_AGENT_COMMANDS)) if (commands.includes(agentCommand)) return currentArgv(name);
|
|
1098
|
+
}
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/session/persistence/parse.ts
|
|
1101
|
+
function asRecord$3(value) {
|
|
1102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1103
|
+
return value;
|
|
1104
|
+
}
|
|
1105
|
+
function hasOwn(source, key) {
|
|
1106
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
1107
|
+
}
|
|
1108
|
+
function isStringArray(value) {
|
|
1109
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1110
|
+
}
|
|
1111
|
+
function parseOptionalAgentArgv(value) {
|
|
1112
|
+
return isStringArray(value) && value.length > 0 && value[0]?.length > 0 ? value : void 0;
|
|
1113
|
+
}
|
|
1114
|
+
function parsePersistedAgentArgv(record) {
|
|
1115
|
+
return parseOptionalAgentArgv(record.agent_argv) ?? (typeof record.agent_command === "string" ? resolveAgentArgvForCommand(record.agent_command) : void 0);
|
|
1116
|
+
}
|
|
1117
|
+
function hasModelConfigOption(options) {
|
|
1118
|
+
if (!Array.isArray(options)) return false;
|
|
1119
|
+
return options.some((entry) => {
|
|
1120
|
+
const option = asRecord$3(entry);
|
|
1121
|
+
return option?.category === "model" || option?.id === "model";
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function parseConfigOptions(raw) {
|
|
1125
|
+
if (!Array.isArray(raw) || !raw.every((entry) => asRecord$3(entry) !== void 0)) return;
|
|
1126
|
+
return raw;
|
|
1127
|
+
}
|
|
1128
|
+
function parseAvailableCommand(raw) {
|
|
1129
|
+
if (typeof raw === "string") {
|
|
1130
|
+
const name = raw.trim();
|
|
1131
|
+
return name ? { name } : void 0;
|
|
1132
|
+
}
|
|
1133
|
+
const record = asRecord$3(raw);
|
|
1134
|
+
if (!record) return;
|
|
1135
|
+
const name = parseNonEmptyString$1(record.name);
|
|
1136
|
+
if (!name) return;
|
|
1137
|
+
const description = parseNonEmptyString$1(record.description);
|
|
1138
|
+
return {
|
|
1139
|
+
name,
|
|
1140
|
+
...description ? { description } : {},
|
|
1141
|
+
...typeof record.has_input === "boolean" ? { has_input: record.has_input } : {}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
function parseAvailableCommands(raw) {
|
|
1145
|
+
if (!Array.isArray(raw)) return;
|
|
1146
|
+
const commands = raw.map((entry) => parseAvailableCommand(entry)).filter((entry) => entry !== void 0);
|
|
1147
|
+
return commands.length > 0 ? commands : void 0;
|
|
1148
|
+
}
|
|
1149
|
+
function parseNonEmptyString$1(value) {
|
|
1150
|
+
if (typeof value !== "string") return;
|
|
1151
|
+
const trimmed = value.trim();
|
|
1152
|
+
return trimmed ? trimmed : void 0;
|
|
1153
|
+
}
|
|
1154
|
+
function parseTokenUsage(raw) {
|
|
1155
|
+
if (raw === void 0 || raw === null) return;
|
|
1156
|
+
const record = asRecord$3(raw);
|
|
1157
|
+
if (!record) return null;
|
|
1158
|
+
const usage = {};
|
|
1159
|
+
for (const field of [
|
|
1160
|
+
"input_tokens",
|
|
1161
|
+
"output_tokens",
|
|
1162
|
+
"cache_creation_input_tokens",
|
|
1163
|
+
"cache_read_input_tokens",
|
|
1164
|
+
"thought_tokens",
|
|
1165
|
+
"total_tokens"
|
|
1166
|
+
]) {
|
|
1167
|
+
const value = record[field];
|
|
1168
|
+
if (value === void 0) continue;
|
|
1169
|
+
if (!isNonNegativeFiniteNumber(value)) return null;
|
|
1170
|
+
usage[field] = value;
|
|
1171
|
+
}
|
|
1172
|
+
return usage;
|
|
1173
|
+
}
|
|
1174
|
+
function isNonNegativeFiniteNumber(value) {
|
|
1175
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
1176
|
+
}
|
|
1177
|
+
function parseUsageCost(raw) {
|
|
1178
|
+
if (raw === void 0 || raw === null) return;
|
|
1179
|
+
const record = asRecord$3(raw);
|
|
1180
|
+
if (!record) return null;
|
|
1181
|
+
return parseUsageCostRecord(record);
|
|
1182
|
+
}
|
|
1183
|
+
function parseUsageCostRecord(record) {
|
|
1184
|
+
const amount = parseCostAmount(record.amount);
|
|
1185
|
+
const currency = parseCostCurrency(record.currency);
|
|
1186
|
+
if (amount === null || currency === null) return null;
|
|
1187
|
+
const cost = {
|
|
1188
|
+
...amount !== void 0 ? { amount } : {},
|
|
1189
|
+
...currency !== void 0 ? { currency } : {}
|
|
1190
|
+
};
|
|
1191
|
+
return Object.keys(cost).length > 0 ? cost : void 0;
|
|
1192
|
+
}
|
|
1193
|
+
function parseCostAmount(value) {
|
|
1194
|
+
if (value === void 0) return;
|
|
1195
|
+
return isNonNegativeFiniteNumber(value) ? value : null;
|
|
1196
|
+
}
|
|
1197
|
+
function parseCostCurrency(value) {
|
|
1198
|
+
if (value === void 0) return;
|
|
1199
|
+
if (typeof value !== "string") return null;
|
|
1200
|
+
const currency = value.trim();
|
|
1201
|
+
return currency.length > 0 ? currency : void 0;
|
|
1202
|
+
}
|
|
1203
|
+
function parseRequestTokenUsage(raw) {
|
|
1204
|
+
if (raw === void 0 || raw === null) return;
|
|
1205
|
+
const record = asRecord$3(raw);
|
|
1206
|
+
if (!record) return null;
|
|
1207
|
+
const usage = {};
|
|
1208
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1209
|
+
const parsed = parseTokenUsage(value);
|
|
1210
|
+
if (parsed == null) return null;
|
|
1211
|
+
usage[key] = parsed;
|
|
1212
|
+
}
|
|
1213
|
+
return usage;
|
|
1214
|
+
}
|
|
1215
|
+
function isSessionMessageImage(raw) {
|
|
1216
|
+
const record = asRecord$3(raw);
|
|
1217
|
+
if (!record || typeof record.source !== "string") return false;
|
|
1218
|
+
if (record.size === void 0 || record.size === null) return true;
|
|
1219
|
+
const size = asRecord$3(record.size);
|
|
1220
|
+
return !!size && isFiniteNumber(size.width) && isFiniteNumber(size.height);
|
|
1221
|
+
}
|
|
1222
|
+
function isSessionMessageAudio(raw) {
|
|
1223
|
+
const record = asRecord$3(raw);
|
|
1224
|
+
return !!record && typeof record.source === "string" && typeof record.mime_type === "string";
|
|
1225
|
+
}
|
|
1226
|
+
function isFiniteNumber(value) {
|
|
1227
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1228
|
+
}
|
|
1229
|
+
function isUserContent(raw) {
|
|
1230
|
+
const record = asRecord$3(raw);
|
|
1231
|
+
if (!record) return false;
|
|
1232
|
+
if (typeof record.Text === "string") return true;
|
|
1233
|
+
if (record.Mention !== void 0) {
|
|
1234
|
+
const mention = asRecord$3(record.Mention);
|
|
1235
|
+
return !!mention && typeof mention.uri === "string" && typeof mention.content === "string";
|
|
1236
|
+
}
|
|
1237
|
+
if (record.Image !== void 0) return isSessionMessageImage(record.Image);
|
|
1238
|
+
if (record.Audio !== void 0) return isSessionMessageAudio(record.Audio);
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
function isToolUse(raw) {
|
|
1242
|
+
const record = asRecord$3(raw);
|
|
1243
|
+
return !!record && hasStringFields$1(record, [
|
|
1244
|
+
"id",
|
|
1245
|
+
"name",
|
|
1246
|
+
"raw_input"
|
|
1247
|
+
]) && hasOwn(record, "input") && typeof record.is_input_complete === "boolean" && isOptionalString(record.thought_signature);
|
|
1248
|
+
}
|
|
1249
|
+
function hasStringFields$1(record, keys) {
|
|
1250
|
+
return keys.every((key) => typeof record[key] === "string");
|
|
1251
|
+
}
|
|
1252
|
+
function isOptionalString(value) {
|
|
1253
|
+
return value === void 0 || value === null || typeof value === "string";
|
|
1254
|
+
}
|
|
1255
|
+
function isToolResultContent(raw) {
|
|
1256
|
+
const record = asRecord$3(raw);
|
|
1257
|
+
if (!record) return false;
|
|
1258
|
+
if (typeof record.Text === "string") return true;
|
|
1259
|
+
if (record.Image !== void 0) return isSessionMessageImage(record.Image);
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
function isToolResult(raw) {
|
|
1263
|
+
const record = asRecord$3(raw);
|
|
1264
|
+
return !!record && typeof record.tool_use_id === "string" && typeof record.tool_name === "string" && typeof record.is_error === "boolean" && isToolResultContent(record.content);
|
|
1265
|
+
}
|
|
1266
|
+
function isAgentContent(raw) {
|
|
1267
|
+
const record = asRecord$3(raw);
|
|
1268
|
+
if (!record) return false;
|
|
1269
|
+
if (typeof record.Text === "string") return true;
|
|
1270
|
+
if (record.Thinking !== void 0) return isThinkingContent(record.Thinking);
|
|
1271
|
+
if (typeof record.RedactedThinking === "string") return true;
|
|
1272
|
+
if (record.ToolUse !== void 0) return isToolUse(record.ToolUse);
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
function isThinkingContent(raw) {
|
|
1276
|
+
const thinking = asRecord$3(raw);
|
|
1277
|
+
return !!thinking && typeof thinking.text === "string" && isOptionalString(thinking.signature);
|
|
1278
|
+
}
|
|
1279
|
+
function isUserMessage(raw) {
|
|
1280
|
+
const record = asRecord$3(raw);
|
|
1281
|
+
if (!record || record.User === void 0) return false;
|
|
1282
|
+
const user = asRecord$3(record.User);
|
|
1283
|
+
return !!user && typeof user.id === "string" && Array.isArray(user.content) && user.content.every((entry) => isUserContent(entry));
|
|
1284
|
+
}
|
|
1285
|
+
function isAgentMessage(raw) {
|
|
1286
|
+
const record = asRecord$3(raw);
|
|
1287
|
+
if (!record || record.Agent === void 0) return false;
|
|
1288
|
+
const agent = asRecord$3(record.Agent);
|
|
1289
|
+
if (!agent || !Array.isArray(agent.content) || !agent.content.every(isAgentContent)) return false;
|
|
1290
|
+
const toolResults = asRecord$3(agent.tool_results);
|
|
1291
|
+
if (!toolResults) return false;
|
|
1292
|
+
return Object.values(toolResults).every(isToolResult);
|
|
1293
|
+
}
|
|
1294
|
+
function isConversationMessage(raw) {
|
|
1295
|
+
return raw === "Resume" || isUserMessage(raw) || isAgentMessage(raw);
|
|
1296
|
+
}
|
|
1297
|
+
function parseConversationRecord(record) {
|
|
1298
|
+
if (!hasValidConversationCore(record)) return;
|
|
1299
|
+
const title = parseConversationTitle(record.title);
|
|
1300
|
+
if (title === INVALID_VALUE) return;
|
|
1301
|
+
const cumulativeTokenUsage = parseTokenUsage(record.cumulative_token_usage);
|
|
1302
|
+
const cumulativeCost = parseUsageCost(record.cumulative_cost);
|
|
1303
|
+
const requestTokenUsage = parseRequestTokenUsage(record.request_token_usage);
|
|
1304
|
+
if (cumulativeTokenUsage === null || cumulativeCost === null || requestTokenUsage === null) return;
|
|
1305
|
+
return {
|
|
1306
|
+
title,
|
|
1307
|
+
messages: record.messages,
|
|
1308
|
+
updated_at: record.updated_at,
|
|
1309
|
+
cumulative_token_usage: cumulativeTokenUsage ?? {},
|
|
1310
|
+
cumulative_cost: cumulativeCost,
|
|
1311
|
+
request_token_usage: requestTokenUsage ?? {}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
const INVALID_VALUE = Symbol("invalid");
|
|
1315
|
+
function parseConversationTitle(value) {
|
|
1316
|
+
if (value === void 0 || value === null || typeof value === "string") return value;
|
|
1317
|
+
return INVALID_VALUE;
|
|
1318
|
+
}
|
|
1319
|
+
function hasValidConversationCore(record) {
|
|
1320
|
+
return Array.isArray(record.messages) && record.messages.every(isConversationMessage) && typeof record.updated_at === "string";
|
|
1321
|
+
}
|
|
1322
|
+
function parseAcpxState(raw) {
|
|
1323
|
+
const record = asRecord$3(raw);
|
|
1324
|
+
if (!record) return;
|
|
1325
|
+
const state = {};
|
|
1326
|
+
if (record.reset_on_next_ensure === true) state.reset_on_next_ensure = true;
|
|
1327
|
+
assignStringState(state, "current_mode_id", record.current_mode_id);
|
|
1328
|
+
assignStringState(state, "desired_mode_id", record.desired_mode_id);
|
|
1329
|
+
const desiredConfigOptions = parseStringMap(record.desired_config_options);
|
|
1330
|
+
if (desiredConfigOptions) state.desired_config_options = desiredConfigOptions;
|
|
1331
|
+
const modelNames = parseStringMap(record.available_model_names, true);
|
|
1332
|
+
if (modelNames) state.available_model_names = modelNames;
|
|
1333
|
+
assignParsedModelState(state, record);
|
|
1334
|
+
const availableCommands = parseAvailableCommands(record.available_commands);
|
|
1335
|
+
if (availableCommands) state.available_commands = availableCommands;
|
|
1336
|
+
assignParsedSessionOptions(state, record.session_options);
|
|
1337
|
+
return state;
|
|
1338
|
+
}
|
|
1339
|
+
function assignParsedModelState(state, record) {
|
|
1340
|
+
assignStringState(state, "current_model_id", record.current_model_id);
|
|
1341
|
+
if (isStringArray(record.available_models)) state.available_models = [...record.available_models];
|
|
1342
|
+
if (record.model_control === "config_option" || record.model_control === "legacy_set_model") state.model_control = record.model_control;
|
|
1343
|
+
const configOptions = parseConfigOptions(record.config_options);
|
|
1344
|
+
if (configOptions) state.config_options = configOptions;
|
|
1345
|
+
if (state.model_control === void 0 && state.available_models !== void 0) state.model_control = hasModelConfigOption(state.config_options) ? "config_option" : "legacy_set_model";
|
|
1346
|
+
}
|
|
1347
|
+
function assignStringState(state, key, value) {
|
|
1348
|
+
if (typeof value === "string") state[key] = value;
|
|
1349
|
+
}
|
|
1350
|
+
function parseStringMap(raw, preserveEmpty = false) {
|
|
1351
|
+
const record = asRecord$3(raw);
|
|
1352
|
+
if (!record) return;
|
|
1353
|
+
const parsed = Object.fromEntries(Object.entries(record).filter((entry) => typeof entry[1] === "string"));
|
|
1354
|
+
return Object.keys(parsed).length > 0 || preserveEmpty ? parsed : void 0;
|
|
1355
|
+
}
|
|
1356
|
+
function assignParsedSessionOptions(state, raw) {
|
|
1357
|
+
const sessionOptions = asRecord$3(raw);
|
|
1358
|
+
if (!sessionOptions) return;
|
|
1359
|
+
const parsedSessionOptions = {};
|
|
1360
|
+
if (typeof sessionOptions.model === "string") parsedSessionOptions.model = sessionOptions.model;
|
|
1361
|
+
if (isStringArray(sessionOptions.allowed_tools)) parsedSessionOptions.allowed_tools = [...sessionOptions.allowed_tools];
|
|
1362
|
+
if (isPositiveInteger$1(sessionOptions.max_turns)) parsedSessionOptions.max_turns = sessionOptions.max_turns;
|
|
1363
|
+
assignSessionOptionSystemPrompt(parsedSessionOptions, sessionOptions.system_prompt);
|
|
1364
|
+
const env = parseStringMap(sessionOptions.env);
|
|
1365
|
+
if (env) parsedSessionOptions.env = env;
|
|
1366
|
+
if (Object.keys(parsedSessionOptions).length > 0) state.session_options = parsedSessionOptions;
|
|
1367
|
+
}
|
|
1368
|
+
function assignSessionOptionSystemPrompt(options, value) {
|
|
1369
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1370
|
+
options.system_prompt = value;
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const appendRecord = asRecord$3(value);
|
|
1374
|
+
if (appendRecord && typeof appendRecord.append === "string" && appendRecord.append.length > 0) options.system_prompt = { append: appendRecord.append };
|
|
1375
|
+
}
|
|
1376
|
+
function parseEventLog(raw, sessionId) {
|
|
1377
|
+
const record = asRecord$3(raw);
|
|
1378
|
+
if (!record || !hasValidEventLogCore(record)) return defaultSessionEventLog(sessionId);
|
|
1379
|
+
return {
|
|
1380
|
+
active_path: record.active_path,
|
|
1381
|
+
segment_count: record.segment_count,
|
|
1382
|
+
max_segment_bytes: record.max_segment_bytes,
|
|
1383
|
+
max_segments: record.max_segments,
|
|
1384
|
+
last_write_at: typeof record.last_write_at === "string" ? record.last_write_at : void 0,
|
|
1385
|
+
last_write_error: record.last_write_error == null || typeof record.last_write_error === "string" ? record.last_write_error : null
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function hasValidEventLogCore(record) {
|
|
1389
|
+
return typeof record.active_path === "string" && isPositiveInteger$1(record.segment_count) && isPositiveInteger$1(record.max_segment_bytes) && isPositiveInteger$1(record.max_segments);
|
|
1390
|
+
}
|
|
1391
|
+
function isPositiveInteger$1(value) {
|
|
1392
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
1393
|
+
}
|
|
1394
|
+
function parseImportedFrom(raw) {
|
|
1395
|
+
if (raw == null) return;
|
|
1396
|
+
const record = asRecord$3(raw);
|
|
1397
|
+
if (!record || typeof record.record_id !== "string" || typeof record.cwd_original !== "string" || typeof record.exported_by !== "string" || typeof record.exported_at !== "string") return null;
|
|
1398
|
+
return {
|
|
1399
|
+
recordId: record.record_id,
|
|
1400
|
+
cwdOriginal: record.cwd_original,
|
|
1401
|
+
exportedBy: record.exported_by,
|
|
1402
|
+
exportedAt: record.exported_at
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function parseSessionRecordMetadata(record) {
|
|
1406
|
+
const lastRequestId = normalizeOptionalString(record.last_request_id);
|
|
1407
|
+
if (lastRequestId === null) return null;
|
|
1408
|
+
const importedFrom = parseImportedFrom(record.imported_from);
|
|
1409
|
+
if (importedFrom === null) return null;
|
|
1410
|
+
return {
|
|
1411
|
+
lastRequestId,
|
|
1412
|
+
importedFrom
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function normalizeOptionalName(value) {
|
|
1416
|
+
if (value == null) return;
|
|
1417
|
+
if (typeof value !== "string") return null;
|
|
1418
|
+
const trimmed = value.trim();
|
|
1419
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1420
|
+
}
|
|
1421
|
+
function normalizeOptionalPid(value) {
|
|
1422
|
+
if (value == null) return;
|
|
1423
|
+
return isPositiveInteger$1(value) ? value : null;
|
|
1424
|
+
}
|
|
1425
|
+
function normalizeOptionalBoolean(value, fallback = false) {
|
|
1426
|
+
if (value == null) return fallback;
|
|
1427
|
+
return typeof value === "boolean" ? value : null;
|
|
1428
|
+
}
|
|
1429
|
+
function normalizeOptionalString(value) {
|
|
1430
|
+
if (value == null) return;
|
|
1431
|
+
return typeof value === "string" ? value : null;
|
|
1432
|
+
}
|
|
1433
|
+
function normalizeOptionalExitCode(value) {
|
|
1434
|
+
if (value === void 0) return;
|
|
1435
|
+
if (value === null) return null;
|
|
1436
|
+
if (Number.isInteger(value)) return value;
|
|
1437
|
+
return Symbol("invalid");
|
|
1438
|
+
}
|
|
1439
|
+
function normalizeOptionalSignal(value) {
|
|
1440
|
+
if (value === void 0) return;
|
|
1441
|
+
if (value === null) return null;
|
|
1442
|
+
if (typeof value === "string") return value;
|
|
1443
|
+
return Symbol("invalid");
|
|
1444
|
+
}
|
|
1445
|
+
function parseSessionRecord(raw) {
|
|
1446
|
+
const record = asRecord$3(raw);
|
|
1447
|
+
if (!record) return null;
|
|
1448
|
+
if (record.schema !== "acpx.session.v1") return null;
|
|
1449
|
+
const optionals = validSessionOptionals({
|
|
1450
|
+
name: normalizeOptionalName(record.name),
|
|
1451
|
+
pid: normalizeOptionalPid(record.pid),
|
|
1452
|
+
closed: normalizeOptionalBoolean(record.closed, false),
|
|
1453
|
+
closedAt: normalizeOptionalString(record.closed_at),
|
|
1454
|
+
agentStartedAt: normalizeOptionalString(record.agent_started_at),
|
|
1455
|
+
lastPromptAt: normalizeOptionalString(record.last_prompt_at),
|
|
1456
|
+
lastAgentExitCode: normalizeOptionalExitCode(record.last_agent_exit_code),
|
|
1457
|
+
lastAgentExitSignal: normalizeOptionalSignal(record.last_agent_exit_signal),
|
|
1458
|
+
lastAgentExitAt: normalizeOptionalString(record.last_agent_exit_at),
|
|
1459
|
+
lastAgentDisconnectReason: normalizeOptionalString(record.last_agent_disconnect_reason)
|
|
1460
|
+
});
|
|
1461
|
+
if (!hasValidSessionRecordCore(record) || !optionals) return null;
|
|
1462
|
+
const conversation = parseConversationRecord(record);
|
|
1463
|
+
if (!conversation) return null;
|
|
1464
|
+
const eventLog = parseEventLog(record.event_log, record.acpx_record_id);
|
|
1465
|
+
const metadata = parseSessionRecordMetadata(record);
|
|
1466
|
+
if (!metadata) return null;
|
|
1467
|
+
return {
|
|
1468
|
+
schema: SESSION_RECORD_SCHEMA,
|
|
1469
|
+
acpxRecordId: record.acpx_record_id,
|
|
1470
|
+
acpSessionId: record.acp_session_id,
|
|
1471
|
+
agentSessionId: normalizeAgentSessionId(record.agent_session_id),
|
|
1472
|
+
agentCommand: record.agent_command,
|
|
1473
|
+
agentArgv: parsePersistedAgentArgv(record),
|
|
1474
|
+
cwd: record.cwd,
|
|
1475
|
+
name: optionals.name,
|
|
1476
|
+
createdAt: record.created_at,
|
|
1477
|
+
lastUsedAt: record.last_used_at,
|
|
1478
|
+
lastSeq: record.last_seq,
|
|
1479
|
+
lastRequestId: metadata.lastRequestId,
|
|
1480
|
+
eventLog,
|
|
1481
|
+
closed: optionals.closed,
|
|
1482
|
+
closedAt: optionals.closedAt,
|
|
1483
|
+
pid: optionals.pid,
|
|
1484
|
+
agentStartedAt: optionals.agentStartedAt,
|
|
1485
|
+
lastPromptAt: optionals.lastPromptAt,
|
|
1486
|
+
lastAgentExitCode: optionals.lastAgentExitCode,
|
|
1487
|
+
lastAgentExitSignal: optionals.lastAgentExitSignal,
|
|
1488
|
+
lastAgentExitAt: optionals.lastAgentExitAt,
|
|
1489
|
+
lastAgentDisconnectReason: optionals.lastAgentDisconnectReason,
|
|
1490
|
+
protocolVersion: typeof record.protocol_version === "number" ? record.protocol_version : void 0,
|
|
1491
|
+
agentCapabilities: asRecord$3(record.agent_capabilities),
|
|
1492
|
+
title: conversation.title,
|
|
1493
|
+
messages: conversation.messages,
|
|
1494
|
+
updated_at: conversation.updated_at,
|
|
1495
|
+
cumulative_token_usage: conversation.cumulative_token_usage,
|
|
1496
|
+
cumulative_cost: conversation.cumulative_cost,
|
|
1497
|
+
request_token_usage: conversation.request_token_usage,
|
|
1498
|
+
acpx: parseAcpxState(record.acpx),
|
|
1499
|
+
importedFrom: metadata.importedFrom
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
function hasValidSessionRecordCore(record) {
|
|
1503
|
+
return hasStringFields$1(record, [
|
|
1504
|
+
"acpx_record_id",
|
|
1505
|
+
"acp_session_id",
|
|
1506
|
+
"agent_command",
|
|
1507
|
+
"cwd",
|
|
1508
|
+
"created_at",
|
|
1509
|
+
"last_used_at"
|
|
1510
|
+
]) && typeof record.last_seq === "number" && Number.isInteger(record.last_seq) && record.last_seq >= 0;
|
|
1511
|
+
}
|
|
1512
|
+
function validSessionOptionals(options) {
|
|
1513
|
+
if (hasNullOptionalSessionFields(options) || hasInvalidExitStatus(options)) return null;
|
|
1514
|
+
return options;
|
|
1515
|
+
}
|
|
1516
|
+
function hasNullOptionalSessionFields(options) {
|
|
1517
|
+
return [
|
|
1518
|
+
options.name,
|
|
1519
|
+
options.pid,
|
|
1520
|
+
options.closed,
|
|
1521
|
+
options.closedAt,
|
|
1522
|
+
options.agentStartedAt,
|
|
1523
|
+
options.lastPromptAt,
|
|
1524
|
+
options.lastAgentExitAt,
|
|
1525
|
+
options.lastAgentDisconnectReason
|
|
1526
|
+
].some((value) => value === null);
|
|
1527
|
+
}
|
|
1528
|
+
function hasInvalidExitStatus(options) {
|
|
1529
|
+
return typeof options.lastAgentExitCode === "symbol" || typeof options.lastAgentExitSignal === "symbol";
|
|
1530
|
+
}
|
|
1531
|
+
//#endregion
|
|
1532
|
+
//#region src/persisted-key-policy.ts
|
|
1533
|
+
const SNAKE_CASE_KEY = /^[a-z][a-z0-9_]*$/;
|
|
1534
|
+
const ZED_TAG_KEYS = /* @__PURE__ */ new Set([
|
|
1535
|
+
"User",
|
|
1536
|
+
"Agent",
|
|
1537
|
+
"Resume",
|
|
1538
|
+
"Text",
|
|
1539
|
+
"Mention",
|
|
1540
|
+
"Image",
|
|
1541
|
+
"Audio",
|
|
1542
|
+
"Thinking",
|
|
1543
|
+
"RedactedThinking",
|
|
1544
|
+
"ToolUse"
|
|
1545
|
+
]);
|
|
1546
|
+
const MAP_OBJECT_PATHS = /* @__PURE__ */ new Set([
|
|
1547
|
+
"request_token_usage",
|
|
1548
|
+
"messages.Agent.tool_results",
|
|
1549
|
+
"acpx.session_options.env",
|
|
1550
|
+
"acpx.available_model_names"
|
|
1551
|
+
]);
|
|
1552
|
+
const OPAQUE_VALUE_PATHS = /* @__PURE__ */ new Set([
|
|
1553
|
+
"agent_capabilities",
|
|
1554
|
+
"messages.Agent.content.ToolUse.input",
|
|
1555
|
+
"acpx.desired_config_options",
|
|
1556
|
+
"acpx.config_options"
|
|
1557
|
+
]);
|
|
1558
|
+
function isRecord(value) {
|
|
1559
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1560
|
+
}
|
|
1561
|
+
function joinPath(path) {
|
|
1562
|
+
return path.join(".");
|
|
1563
|
+
}
|
|
1564
|
+
function shouldSkipKeyRule(path) {
|
|
1565
|
+
return MAP_OBJECT_PATHS.has(joinPath(path));
|
|
1566
|
+
}
|
|
1567
|
+
function shouldSkipDescend(path) {
|
|
1568
|
+
return OPAQUE_VALUE_PATHS.has(joinPath(path)) || isToolResultOutputPath(path);
|
|
1569
|
+
}
|
|
1570
|
+
function isToolResultOutputTail(path, toolResultsIndex) {
|
|
1571
|
+
return toolResultsIndex !== -1 && toolResultsIndex + 2 === path.length - 1;
|
|
1572
|
+
}
|
|
1573
|
+
function isToolResultOutputPath(path) {
|
|
1574
|
+
if (path.length < 5 || path[path.length - 1] !== "output") return false;
|
|
1575
|
+
const toolResultsIndex = path.lastIndexOf("tool_results");
|
|
1576
|
+
if (!isToolResultOutputTail(path, toolResultsIndex)) return false;
|
|
1577
|
+
return path.slice(0, toolResultsIndex + 1).join(".") === "messages.Agent.tool_results";
|
|
1578
|
+
}
|
|
1579
|
+
function collectViolations(value, path, violations) {
|
|
1580
|
+
if (Array.isArray(value)) {
|
|
1581
|
+
for (const entry of value) collectViolations(entry, path, violations);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (!isRecord(value)) return;
|
|
1585
|
+
const skipKeyRule = shouldSkipKeyRule(path);
|
|
1586
|
+
for (const [key, child] of Object.entries(value)) collectKeyViolation(child, key, path, skipKeyRule, violations);
|
|
1587
|
+
}
|
|
1588
|
+
function collectKeyViolation(child, key, path, skipKeyRule, violations) {
|
|
1589
|
+
if (!skipKeyRule && !SNAKE_CASE_KEY.test(key) && !ZED_TAG_KEYS.has(key)) violations.push(`${joinPath(path)}.${key}`.replace(/^\./, ""));
|
|
1590
|
+
const childPath = [...path, key];
|
|
1591
|
+
if (!shouldSkipDescend(childPath)) collectViolations(child, childPath, violations);
|
|
1592
|
+
}
|
|
1593
|
+
function findPersistedKeyPolicyViolations(value) {
|
|
1594
|
+
const violations = [];
|
|
1595
|
+
collectViolations(value, [], violations);
|
|
1596
|
+
return violations;
|
|
1597
|
+
}
|
|
1598
|
+
function assertPersistedKeyPolicy(value) {
|
|
1599
|
+
const violations = findPersistedKeyPolicyViolations(value);
|
|
1600
|
+
if (violations.length === 0) return;
|
|
1601
|
+
throw new Error(`Persisted key policy violation (expected snake_case keys): ${violations.join(", ")}`);
|
|
1602
|
+
}
|
|
1603
|
+
//#endregion
|
|
1604
|
+
//#region src/session/persistence/index.ts
|
|
1605
|
+
const SESSION_INDEX_SCHEMA = "acpx.session-index.v1";
|
|
1606
|
+
function asRecord$2(value) {
|
|
1607
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1608
|
+
return value;
|
|
1609
|
+
}
|
|
1610
|
+
function parseIndexEntry(raw) {
|
|
1611
|
+
const record = asRecord$2(raw);
|
|
1612
|
+
if (!record) return;
|
|
1613
|
+
if (!hasRequiredIndexEntryFields(record)) return;
|
|
1614
|
+
if (record.name !== void 0 && typeof record.name !== "string") return;
|
|
1615
|
+
return {
|
|
1616
|
+
file: record.file,
|
|
1617
|
+
acpxRecordId: record.acpxRecordId,
|
|
1618
|
+
acpSessionId: record.acpSessionId,
|
|
1619
|
+
agentCommand: record.agentCommand,
|
|
1620
|
+
cwd: record.cwd,
|
|
1621
|
+
name: record.name,
|
|
1622
|
+
closed: record.closed,
|
|
1623
|
+
lastUsedAt: record.lastUsedAt
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function hasRequiredIndexEntryFields(record) {
|
|
1627
|
+
return [
|
|
1628
|
+
"file",
|
|
1629
|
+
"acpxRecordId",
|
|
1630
|
+
"acpSessionId",
|
|
1631
|
+
"agentCommand",
|
|
1632
|
+
"cwd",
|
|
1633
|
+
"lastUsedAt"
|
|
1634
|
+
].every((key) => typeof record[key] === "string") && typeof record.closed === "boolean";
|
|
1635
|
+
}
|
|
1636
|
+
function sessionIndexPath(sessionDir) {
|
|
1637
|
+
return path.join(sessionDir, "index.json");
|
|
1638
|
+
}
|
|
1639
|
+
function toSessionIndexEntry(record, fileName) {
|
|
1640
|
+
return {
|
|
1641
|
+
file: fileName,
|
|
1642
|
+
acpxRecordId: record.acpxRecordId,
|
|
1643
|
+
acpSessionId: record.acpSessionId,
|
|
1644
|
+
agentCommand: record.agentCommand,
|
|
1645
|
+
cwd: record.cwd,
|
|
1646
|
+
name: record.name,
|
|
1647
|
+
closed: record.closed === true,
|
|
1648
|
+
lastUsedAt: record.lastUsedAt
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
async function readSessionIndex(sessionDir) {
|
|
1652
|
+
const filePath = sessionIndexPath(sessionDir);
|
|
1653
|
+
try {
|
|
1654
|
+
const payload = await fs$1.readFile(filePath, "utf8");
|
|
1655
|
+
const record = asRecord$2(JSON.parse(payload));
|
|
1656
|
+
if (!record || record.schema !== SESSION_INDEX_SCHEMA || !Array.isArray(record.files)) return;
|
|
1657
|
+
const files = record.files.filter((entry) => typeof entry === "string");
|
|
1658
|
+
if (files.length !== record.files.length || !Array.isArray(record.entries)) return;
|
|
1659
|
+
const entries = record.entries.map((entry) => parseIndexEntry(entry)).filter((entry) => Boolean(entry));
|
|
1660
|
+
if (entries.length !== record.entries.length) return;
|
|
1661
|
+
return {
|
|
1662
|
+
schema: SESSION_INDEX_SCHEMA,
|
|
1663
|
+
files,
|
|
1664
|
+
entries
|
|
1665
|
+
};
|
|
1666
|
+
} catch {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
async function writeSessionIndex(sessionDir, index) {
|
|
1671
|
+
await writePrivateJsonFile(sessionIndexPath(sessionDir), {
|
|
1672
|
+
schema: SESSION_INDEX_SCHEMA,
|
|
1673
|
+
files: index.files.toSorted(),
|
|
1674
|
+
entries: index.entries.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
async function rebuildSessionIndex(sessionDir) {
|
|
1678
|
+
const index = await scanSessionIndex(sessionDir);
|
|
1679
|
+
await writeSessionIndex(sessionDir, index);
|
|
1680
|
+
return index;
|
|
1681
|
+
}
|
|
1682
|
+
async function scanSessionIndex(sessionDir) {
|
|
1683
|
+
const files = await listSessionFiles(sessionDir).catch((error) => {
|
|
1684
|
+
if (error.code === "ENOENT") return [];
|
|
1685
|
+
throw error;
|
|
1686
|
+
});
|
|
1687
|
+
const indexEntries = [];
|
|
1688
|
+
for (const file of files) try {
|
|
1689
|
+
const payload = await fs$1.readFile(path.join(sessionDir, file), "utf8");
|
|
1690
|
+
const parsed = parseSessionRecord(JSON.parse(payload));
|
|
1691
|
+
if (!parsed) continue;
|
|
1692
|
+
indexEntries.push(toSessionIndexEntry(parsed, file));
|
|
1693
|
+
} catch {}
|
|
1694
|
+
return {
|
|
1695
|
+
schema: SESSION_INDEX_SCHEMA,
|
|
1696
|
+
files,
|
|
1697
|
+
entries: indexEntries.toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
async function loadOrRebuildSessionIndex(sessionDir) {
|
|
1701
|
+
const files = await listSessionFiles(sessionDir);
|
|
1702
|
+
const existing = await readSessionIndex(sessionDir);
|
|
1703
|
+
if (existing && existing.files.length === files.length && existing.files.every((file, index) => file === files[index])) return existing;
|
|
1704
|
+
return await rebuildSessionIndex(sessionDir);
|
|
1705
|
+
}
|
|
1706
|
+
async function listSessionFiles(sessionDir) {
|
|
1707
|
+
return (await fs$1.readdir(sessionDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json").map((entry) => entry.name).toSorted();
|
|
1708
|
+
}
|
|
1709
|
+
//#endregion
|
|
1710
|
+
//#region src/session/persistence/repository.ts
|
|
1711
|
+
const DEFAULT_HISTORY_LIMIT = 20;
|
|
1712
|
+
function sessionFilePath(acpxRecordId) {
|
|
1713
|
+
const safeId = safeSessionId(acpxRecordId);
|
|
1714
|
+
return path.join(sessionBaseDir(), `${safeId}.json`);
|
|
1715
|
+
}
|
|
1716
|
+
async function ensureSessionDir() {
|
|
1717
|
+
await fs$1.mkdir(sessionBaseDir(), {
|
|
1718
|
+
recursive: true,
|
|
1719
|
+
mode: 448
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
async function loadRecordFromIndexEntry(entry) {
|
|
1723
|
+
try {
|
|
1724
|
+
const payload = await fs$1.readFile(path.join(sessionBaseDir(), entry.file), "utf8");
|
|
1725
|
+
return parseSessionRecord(JSON.parse(payload)) ?? void 0;
|
|
1726
|
+
} catch {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
async function loadSessionIndexEntries(readOnly = false) {
|
|
1731
|
+
if (!readOnly) await ensureSessionDir();
|
|
1732
|
+
return (await measurePerf("session.index_load", async () => {
|
|
1733
|
+
return await (readOnly ? scanSessionIndex : loadOrRebuildSessionIndex)(sessionBaseDir());
|
|
1734
|
+
})).entries;
|
|
1735
|
+
}
|
|
1736
|
+
function matchesSessionEntry(session, normalizedCwd, normalizedName, includeClosed = false) {
|
|
1737
|
+
if (session.cwd !== normalizedCwd) return false;
|
|
1738
|
+
if (!includeClosed && session.closed) return false;
|
|
1739
|
+
if (normalizedName == null) return session.name == null;
|
|
1740
|
+
return session.name === normalizedName;
|
|
1741
|
+
}
|
|
1742
|
+
async function writeSessionRecord(record) {
|
|
1743
|
+
await measurePerf("session.write_record", async () => {
|
|
1744
|
+
const persisted = serializeSessionRecordForDisk(record);
|
|
1745
|
+
assertPersistedKeyPolicy(persisted);
|
|
1746
|
+
const file = sessionFilePath(record.acpxRecordId);
|
|
1747
|
+
await writePrivateJsonFile(file, persisted);
|
|
1748
|
+
const sessionDir = sessionBaseDir();
|
|
1749
|
+
const index = await loadOrRebuildSessionIndex(sessionDir);
|
|
1750
|
+
const fileName = path.basename(file);
|
|
1751
|
+
const entries = index.entries.filter((entry) => entry.file !== fileName);
|
|
1752
|
+
entries.push(toSessionIndexEntry(record, fileName));
|
|
1753
|
+
await writeSessionIndex(sessionDir, {
|
|
1754
|
+
files: [.../* @__PURE__ */ new Set([...index.files.filter((entry) => entry !== fileName), fileName])],
|
|
1755
|
+
entries
|
|
1756
|
+
});
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
async function resolveSessionRecord(sessionId) {
|
|
1760
|
+
await ensureSessionDir();
|
|
1761
|
+
try {
|
|
1762
|
+
const directRecord = await measurePerf("session.resolve_direct", async () => await readSessionRecord(sessionId));
|
|
1763
|
+
if (directRecord) return directRecord;
|
|
1764
|
+
} catch {}
|
|
1765
|
+
const entries = await loadSessionIndexEntries();
|
|
1766
|
+
const exactEntries = entries.filter((entry) => entry.acpxRecordId === sessionId || entry.acpSessionId === sessionId);
|
|
1767
|
+
const exactRecords = (await Promise.all(exactEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
|
|
1768
|
+
if (exactRecords.length === 1) return exactRecords[0];
|
|
1769
|
+
if (exactRecords.length > 1) throw new SessionResolutionError(`Multiple sessions match id: ${sessionId}`);
|
|
1770
|
+
const suffixEntries = entries.filter((entry) => entry.acpxRecordId.endsWith(sessionId) || entry.acpSessionId.endsWith(sessionId));
|
|
1771
|
+
const suffixRecords = (await Promise.all(suffixEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
|
|
1772
|
+
if (suffixRecords.length === 1) return suffixRecords[0];
|
|
1773
|
+
if (suffixRecords.length > 1) throw new SessionResolutionError(`Session id is ambiguous: ${sessionId}`);
|
|
1774
|
+
incrementPerfCounter("session.resolve_miss");
|
|
1775
|
+
throw new SessionNotFoundError(sessionId);
|
|
1776
|
+
}
|
|
1777
|
+
/** Reads a canonical record without creating directories or rebuilding the lookup index. */
|
|
1778
|
+
async function readSessionRecord(sessionId) {
|
|
1779
|
+
let payload;
|
|
1780
|
+
try {
|
|
1781
|
+
payload = await fs$1.readFile(sessionFilePath(sessionId), "utf8");
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
if (error.code === "ENOENT") return;
|
|
1784
|
+
throw error;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
return parseSessionRecord(JSON.parse(payload)) ?? void 0;
|
|
1788
|
+
} catch {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function hasGitDirectory(dir) {
|
|
1793
|
+
const gitPath = path.join(dir, ".git");
|
|
1794
|
+
try {
|
|
1795
|
+
return statSync(gitPath).isDirectory();
|
|
1796
|
+
} catch {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function isWithinBoundary(boundary, target) {
|
|
1801
|
+
const relative = path.relative(boundary, target);
|
|
1802
|
+
return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
1803
|
+
}
|
|
1804
|
+
function absolutePath(value) {
|
|
1805
|
+
return path.resolve(value);
|
|
1806
|
+
}
|
|
1807
|
+
function findGitRepositoryRoot(startDir) {
|
|
1808
|
+
let current = absolutePath(startDir);
|
|
1809
|
+
const root = path.parse(current).root;
|
|
1810
|
+
for (;;) {
|
|
1811
|
+
if (hasGitDirectory(current)) return current;
|
|
1812
|
+
if (current === root) return;
|
|
1813
|
+
const parent = path.dirname(current);
|
|
1814
|
+
if (parent === current) return;
|
|
1815
|
+
current = parent;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
function normalizeName(value) {
|
|
1819
|
+
if (value == null) return;
|
|
1820
|
+
const trimmed = value.trim();
|
|
1821
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1822
|
+
}
|
|
1823
|
+
function isoNow() {
|
|
1824
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1825
|
+
}
|
|
1826
|
+
async function listSessions() {
|
|
1827
|
+
await ensureSessionDir();
|
|
1828
|
+
const entries = await loadSessionIndexEntries();
|
|
1829
|
+
const records = [];
|
|
1830
|
+
for (const entry of entries) {
|
|
1831
|
+
const parsed = await loadRecordFromIndexEntry(entry);
|
|
1832
|
+
if (parsed) records.push(parsed);
|
|
1833
|
+
}
|
|
1834
|
+
records.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1835
|
+
return records;
|
|
1836
|
+
}
|
|
1837
|
+
async function listSessionsForAgent(agentCommand) {
|
|
1838
|
+
const entries = (await loadSessionIndexEntries()).filter((session) => session.agentCommand === agentCommand);
|
|
1839
|
+
return (await Promise.all(entries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry)).toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
1840
|
+
}
|
|
1841
|
+
async function findSession(options) {
|
|
1842
|
+
const normalizedCwd = absolutePath(options.cwd);
|
|
1843
|
+
const normalizedName = normalizeName(options.name);
|
|
1844
|
+
const match = (await loadSessionIndexEntries(options.readOnly)).find((session) => session.agentCommand === options.agentCommand && matchesSessionEntry(session, normalizedCwd, normalizedName, options.includeClosed));
|
|
1845
|
+
if (!match) return;
|
|
1846
|
+
return await loadRecordFromIndexEntry(match);
|
|
1847
|
+
}
|
|
1848
|
+
async function findSessionByDirectoryWalk(options) {
|
|
1849
|
+
const normalizedName = normalizeName(options.name);
|
|
1850
|
+
const normalizedStart = absolutePath(options.cwd);
|
|
1851
|
+
const normalizedBoundary = absolutePath(options.boundary ?? normalizedStart);
|
|
1852
|
+
const walkBoundary = isWithinBoundary(normalizedBoundary, normalizedStart) ? normalizedBoundary : normalizedStart;
|
|
1853
|
+
const sessions = (await loadSessionIndexEntries()).filter((session) => session.agentCommand === options.agentCommand);
|
|
1854
|
+
let current = normalizedStart;
|
|
1855
|
+
const walkRoot = path.parse(current).root;
|
|
1856
|
+
for (;;) {
|
|
1857
|
+
const match = sessions.find((session) => matchesSessionEntry(session, current, normalizedName));
|
|
1858
|
+
if (match) return await loadRecordFromIndexEntry(match);
|
|
1859
|
+
const parent = nextWalkParent(current, walkBoundary, walkRoot);
|
|
1860
|
+
if (!parent) return;
|
|
1861
|
+
current = parent;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function nextWalkParent(current, walkBoundary, walkRoot) {
|
|
1865
|
+
if (current === walkBoundary || current === walkRoot) return;
|
|
1866
|
+
const parent = path.dirname(current);
|
|
1867
|
+
if (parent === current || !isWithinBoundary(walkBoundary, parent)) return;
|
|
1868
|
+
return parent;
|
|
1869
|
+
}
|
|
1870
|
+
function closedAtOrLastUsedAt(record) {
|
|
1871
|
+
return record.closedAt ?? record.lastUsedAt;
|
|
1872
|
+
}
|
|
1873
|
+
function isSessionStreamFile(fileName, safeId) {
|
|
1874
|
+
return fileName === `${safeId}.stream.ndjson` || fileName === `${safeId}.stream.lock` || fileName.startsWith(`${safeId}.stream.`);
|
|
1875
|
+
}
|
|
1876
|
+
async function pruneSessions(options = {}) {
|
|
1877
|
+
await ensureSessionDir();
|
|
1878
|
+
const records = await loadPrunableRecords(filterPruneCandidates(await loadSessionIndexEntries(), options.agentCommand), options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0));
|
|
1879
|
+
if (options.dryRun) return {
|
|
1880
|
+
pruned: records,
|
|
1881
|
+
bytesFreed: 0,
|
|
1882
|
+
dryRun: true
|
|
1883
|
+
};
|
|
1884
|
+
const sessionDir = sessionBaseDir();
|
|
1885
|
+
let bytesFreed = 0;
|
|
1886
|
+
let dirEntries = [];
|
|
1887
|
+
if (options.includeHistory) try {
|
|
1888
|
+
dirEntries = await fs$1.readdir(sessionDir);
|
|
1889
|
+
} catch {}
|
|
1890
|
+
for (const record of records) bytesFreed += await pruneSessionFiles(record, sessionDir, dirEntries, options.includeHistory === true);
|
|
1891
|
+
await rebuildSessionIndex(sessionDir).catch(() => {});
|
|
1892
|
+
return {
|
|
1893
|
+
pruned: records,
|
|
1894
|
+
bytesFreed,
|
|
1895
|
+
dryRun: false
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
function filterPruneCandidates(entries, agentCommand) {
|
|
1899
|
+
return entries.filter((entry) => entry.closed && (!agentCommand || entry.agentCommand === agentCommand));
|
|
1900
|
+
}
|
|
1901
|
+
async function loadPrunableRecords(entries, cutoff) {
|
|
1902
|
+
const records = [];
|
|
1903
|
+
const cutoffIso = cutoff?.toISOString();
|
|
1904
|
+
for (const entry of entries) {
|
|
1905
|
+
const record = await loadRecordFromIndexEntry(entry);
|
|
1906
|
+
if (record && isBeforeCutoff(record, cutoffIso)) records.push(record);
|
|
1907
|
+
}
|
|
1908
|
+
return records;
|
|
1909
|
+
}
|
|
1910
|
+
function isBeforeCutoff(record, cutoffIso) {
|
|
1911
|
+
return !cutoffIso || closedAtOrLastUsedAt(record) < cutoffIso;
|
|
1912
|
+
}
|
|
1913
|
+
async function pruneSessionFiles(record, sessionDir, dirEntries, includeHistory) {
|
|
1914
|
+
const safeId = safeSessionId(record.acpxRecordId);
|
|
1915
|
+
let bytesFreed = await unlinkCountingBytes(path.join(sessionDir, `${safeId}.json`));
|
|
1916
|
+
if (includeHistory) for (const name of dirEntries.filter((entry) => isSessionStreamFile(entry, safeId))) bytesFreed += await unlinkCountingBytes(path.join(sessionDir, name));
|
|
1917
|
+
return bytesFreed;
|
|
1918
|
+
}
|
|
1919
|
+
async function unlinkCountingBytes(filePath) {
|
|
1920
|
+
let bytes = 0;
|
|
1921
|
+
try {
|
|
1922
|
+
bytes = (await fs$1.stat(filePath)).size;
|
|
1923
|
+
} catch {}
|
|
1924
|
+
await fs$1.unlink(filePath).catch(() => void 0);
|
|
1925
|
+
return bytes;
|
|
1926
|
+
}
|
|
1927
|
+
//#endregion
|
|
1928
|
+
//#region src/prompt-content.ts
|
|
1929
|
+
var PromptInputValidationError = class extends Error {
|
|
1930
|
+
constructor(message) {
|
|
1931
|
+
super(message);
|
|
1932
|
+
this.name = "PromptInputValidationError";
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
function asRecord$1(value) {
|
|
1936
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1937
|
+
return value;
|
|
1938
|
+
}
|
|
1939
|
+
function isNonEmptyString(value) {
|
|
1940
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1941
|
+
}
|
|
1942
|
+
function isBase64Data(value) {
|
|
1943
|
+
if (value.length === 0 || value.length % 4 !== 0) return false;
|
|
1944
|
+
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
|
|
1945
|
+
}
|
|
1946
|
+
function isImageMimeType(value) {
|
|
1947
|
+
return /^image\/[A-Za-z0-9.+-]+$/i.test(value);
|
|
1948
|
+
}
|
|
1949
|
+
function isAudioMimeType(value) {
|
|
1950
|
+
return /^audio\/[A-Za-z0-9.+-]+$/i.test(value);
|
|
1951
|
+
}
|
|
1952
|
+
function isTextBlock(value) {
|
|
1953
|
+
const record = asRecord$1(value);
|
|
1954
|
+
return record?.type === "text" && typeof record.text === "string";
|
|
1955
|
+
}
|
|
1956
|
+
function isImageBlock(value) {
|
|
1957
|
+
const record = asRecord$1(value);
|
|
1958
|
+
return record?.type === "image" && isNonEmptyString(record.mimeType) && isImageMimeType(record.mimeType) && typeof record.data === "string" && isBase64Data(record.data);
|
|
1959
|
+
}
|
|
1960
|
+
function isAudioBlock(value) {
|
|
1961
|
+
const record = asRecord$1(value);
|
|
1962
|
+
return record?.type === "audio" && isNonEmptyString(record.mimeType) && isAudioMimeType(record.mimeType) && typeof record.data === "string" && isBase64Data(record.data);
|
|
1963
|
+
}
|
|
1964
|
+
function isResourceLinkBlock(value) {
|
|
1965
|
+
const record = asRecord$1(value);
|
|
1966
|
+
return record?.type === "resource_link" && isNonEmptyString(record.uri) && (record.title === void 0 || typeof record.title === "string") && (record.name === void 0 || typeof record.name === "string");
|
|
1967
|
+
}
|
|
1968
|
+
function isResourcePayload(value) {
|
|
1969
|
+
const record = asRecord$1(value);
|
|
1970
|
+
if (!record || !isNonEmptyString(record.uri)) return false;
|
|
1971
|
+
return record.text === void 0 || typeof record.text === "string";
|
|
1972
|
+
}
|
|
1973
|
+
function isResourceBlock(value) {
|
|
1974
|
+
const record = asRecord$1(value);
|
|
1975
|
+
return record?.type === "resource" && isResourcePayload(record.resource);
|
|
1976
|
+
}
|
|
1977
|
+
const CONTENT_BLOCK_VALIDATORS = [
|
|
1978
|
+
isTextBlock,
|
|
1979
|
+
isImageBlock,
|
|
1980
|
+
isAudioBlock,
|
|
1981
|
+
isResourceLinkBlock,
|
|
1982
|
+
isResourceBlock
|
|
1983
|
+
];
|
|
1984
|
+
function isContentBlock(value) {
|
|
1985
|
+
return CONTENT_BLOCK_VALIDATORS.some((validator) => validator(value));
|
|
1986
|
+
}
|
|
1987
|
+
const CONTENT_BLOCK_ERROR_VALIDATORS = {
|
|
1988
|
+
text: validateTextContentBlock,
|
|
1989
|
+
image: validateImageContentBlock,
|
|
1990
|
+
audio: validateAudioContentBlock,
|
|
1991
|
+
resource_link: validateResourceLinkContentBlock,
|
|
1992
|
+
resource: validateResourceContentBlock
|
|
1993
|
+
};
|
|
1994
|
+
function contentBlockErrorValidator(type) {
|
|
1995
|
+
return Object.hasOwn(CONTENT_BLOCK_ERROR_VALIDATORS, type) ? CONTENT_BLOCK_ERROR_VALIDATORS[type] : void 0;
|
|
1996
|
+
}
|
|
1997
|
+
function validateTextContentBlock(record, index) {
|
|
1998
|
+
return typeof record.text === "string" ? void 0 : `prompt[${index}] text block must include a string text field`;
|
|
1999
|
+
}
|
|
2000
|
+
function validateImageContentBlock(record, index) {
|
|
2001
|
+
if (!isNonEmptyString(record.mimeType)) return `prompt[${index}] image block must include a non-empty mimeType`;
|
|
2002
|
+
if (!isImageMimeType(record.mimeType)) return `prompt[${index}] image block mimeType must start with image/`;
|
|
2003
|
+
if (typeof record.data !== "string" || record.data.length === 0) return `prompt[${index}] image block must include non-empty base64 data`;
|
|
2004
|
+
return isBase64Data(record.data) ? void 0 : `prompt[${index}] image block data must be valid base64`;
|
|
2005
|
+
}
|
|
2006
|
+
function validateAudioContentBlock(record, index) {
|
|
2007
|
+
if (!isNonEmptyString(record.mimeType)) return `prompt[${index}] audio block must include a non-empty mimeType`;
|
|
2008
|
+
if (!isAudioMimeType(record.mimeType)) return `prompt[${index}] audio block mimeType must start with audio/`;
|
|
2009
|
+
if (typeof record.data !== "string" || record.data.length === 0) return `prompt[${index}] audio block must include non-empty base64 data`;
|
|
2010
|
+
return isBase64Data(record.data) ? void 0 : `prompt[${index}] audio block data must be valid base64`;
|
|
2011
|
+
}
|
|
2012
|
+
function validateResourceLinkContentBlock(record, index) {
|
|
2013
|
+
if (!isNonEmptyString(record.uri)) return `prompt[${index}] resource_link block must include a non-empty uri`;
|
|
2014
|
+
if (record.title !== void 0 && typeof record.title !== "string") return `prompt[${index}] resource_link block title must be a string when present`;
|
|
2015
|
+
if (record.name !== void 0 && typeof record.name !== "string") return `prompt[${index}] resource_link block name must be a string when present`;
|
|
2016
|
+
}
|
|
2017
|
+
function validateResourceContentBlock(record, index) {
|
|
2018
|
+
if (!asRecord$1(record.resource)) return `prompt[${index}] resource block must include a resource object`;
|
|
2019
|
+
return isResourcePayload(record.resource) ? void 0 : `prompt[${index}] resource block resource must include a non-empty uri and optional text`;
|
|
2020
|
+
}
|
|
2021
|
+
function getContentBlockValidationError(value, index) {
|
|
2022
|
+
const record = asRecord$1(value);
|
|
2023
|
+
if (!record || typeof record.type !== "string") return `prompt[${index}] must be an ACP content block object`;
|
|
2024
|
+
const validator = contentBlockErrorValidator(record.type);
|
|
2025
|
+
return validator ? validator(record, index) : `prompt[${index}] has unsupported content block type ${JSON.stringify(record.type)}`;
|
|
2026
|
+
}
|
|
2027
|
+
function isPromptInput(value) {
|
|
2028
|
+
return Array.isArray(value) && value.every((entry) => isContentBlock(entry));
|
|
2029
|
+
}
|
|
2030
|
+
function promptCapabilityRequirement(block) {
|
|
2031
|
+
switch (block.type) {
|
|
2032
|
+
case "image": return {
|
|
2033
|
+
blockType: "image",
|
|
2034
|
+
capability: "image"
|
|
2035
|
+
};
|
|
2036
|
+
case "audio": return {
|
|
2037
|
+
blockType: "audio",
|
|
2038
|
+
capability: "audio"
|
|
2039
|
+
};
|
|
2040
|
+
case "resource": return {
|
|
2041
|
+
blockType: "resource",
|
|
2042
|
+
capability: "embeddedContext"
|
|
2043
|
+
};
|
|
2044
|
+
default: return;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
function getUnsupportedPromptContentMessage(prompt, agentCapabilities) {
|
|
2048
|
+
for (const [index, block] of prompt.entries()) {
|
|
2049
|
+
const requirement = promptCapabilityRequirement(block);
|
|
2050
|
+
if (!requirement) continue;
|
|
2051
|
+
if (agentCapabilities?.promptCapabilities?.[requirement.capability] === true) continue;
|
|
2052
|
+
return `prompt[${index}] ${requirement.blockType} content requires agentCapabilities.promptCapabilities.${requirement.capability}`;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
function textPrompt(text) {
|
|
2056
|
+
return [{
|
|
2057
|
+
type: "text",
|
|
2058
|
+
text
|
|
2059
|
+
}];
|
|
2060
|
+
}
|
|
2061
|
+
function parseStructuredPrompt(source) {
|
|
2062
|
+
if (!source.startsWith("[")) return;
|
|
2063
|
+
try {
|
|
2064
|
+
const parsed = JSON.parse(source);
|
|
2065
|
+
if (isPromptInput(parsed)) return parsed;
|
|
2066
|
+
if (Array.isArray(parsed)) throw new PromptInputValidationError(parsed.map((entry, index) => getContentBlockValidationError(entry, index)).find((message) => message !== void 0) ?? "Structured prompt JSON must be an array of valid ACP content blocks");
|
|
2067
|
+
return;
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
if (error instanceof PromptInputValidationError) throw error;
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function parsePromptSource(source) {
|
|
2074
|
+
const trimmed = source.trim();
|
|
2075
|
+
const structured = parseStructuredPrompt(trimmed);
|
|
2076
|
+
if (structured) return structured;
|
|
2077
|
+
if (!trimmed) return [];
|
|
2078
|
+
return textPrompt(trimmed);
|
|
2079
|
+
}
|
|
2080
|
+
function mergePromptSourceWithText(source, suffixText) {
|
|
2081
|
+
const prompt = parsePromptSource(source);
|
|
2082
|
+
const appended = suffixText.trim();
|
|
2083
|
+
if (!appended) return prompt;
|
|
2084
|
+
if (prompt.length === 0) return textPrompt(appended);
|
|
2085
|
+
return [...prompt, ...textPrompt(appended)];
|
|
2086
|
+
}
|
|
2087
|
+
function promptToDisplayText(prompt) {
|
|
2088
|
+
return prompt.map((block) => contentBlockDisplayText(block)).filter((entry) => entry.trim().length > 0).join("\n\n").trim();
|
|
2089
|
+
}
|
|
2090
|
+
function contentBlockDisplayText(block) {
|
|
2091
|
+
switch (block.type) {
|
|
2092
|
+
case "text": return block.text;
|
|
2093
|
+
case "resource_link": return block.title ?? block.name ?? block.uri;
|
|
2094
|
+
case "resource": return resourceBlockDisplayText(block);
|
|
2095
|
+
case "image": return `[image] ${block.mimeType}`;
|
|
2096
|
+
case "audio": return `[audio] ${block.mimeType}`;
|
|
2097
|
+
default: return "";
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function resourceBlockDisplayText(block) {
|
|
2101
|
+
return "text" in block.resource && typeof block.resource.text === "string" ? block.resource.text : block.resource.uri;
|
|
2102
|
+
}
|
|
2103
|
+
//#endregion
|
|
2104
|
+
//#region src/session/queue/paths.ts
|
|
2105
|
+
function shortHash(value, length) {
|
|
2106
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
2107
|
+
}
|
|
2108
|
+
function queueKeyForSession(sessionId) {
|
|
2109
|
+
return shortHash(sessionId, 24);
|
|
2110
|
+
}
|
|
2111
|
+
function queueBaseDir(homeDir = os.homedir()) {
|
|
2112
|
+
return path.join(homeDir, ".acpx", "queues");
|
|
2113
|
+
}
|
|
2114
|
+
function queueSocketBaseDir(homeDir = os.homedir()) {
|
|
2115
|
+
if (process.platform === "win32") return;
|
|
2116
|
+
return path.join("/tmp", `acpx-${shortHash(homeDir, 10)}`);
|
|
2117
|
+
}
|
|
2118
|
+
function queueLockFilePath(sessionId, homeDir = os.homedir()) {
|
|
2119
|
+
return path.join(queueBaseDir(homeDir), `${queueKeyForSession(sessionId)}.lock`);
|
|
2120
|
+
}
|
|
2121
|
+
function queueSocketPath(sessionId, homeDir = os.homedir()) {
|
|
2122
|
+
const key = queueKeyForSession(sessionId);
|
|
2123
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\acpx-${key}`;
|
|
2124
|
+
return path.join(queueSocketBaseDir(homeDir) ?? "/tmp", `${key}.sock`);
|
|
2125
|
+
}
|
|
2126
|
+
//#endregion
|
|
2127
|
+
//#region src/session/queue/lease-store.ts
|
|
2128
|
+
const PROCESS_SIGTERM_GRACE_MS = process.platform === "win32" ? 4e3 : 12e3;
|
|
2129
|
+
const PROCESS_SIGKILL_GRACE_MS = 1500;
|
|
2130
|
+
const PROCESS_POLL_MS = 50;
|
|
2131
|
+
const QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
|
|
2132
|
+
function parseQueueOwnerRecord(raw) {
|
|
2133
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2134
|
+
const record = raw;
|
|
2135
|
+
if (!hasValidQueueOwnerRecordFields(record)) return null;
|
|
2136
|
+
return {
|
|
2137
|
+
pid: record.pid,
|
|
2138
|
+
sessionId: record.sessionId,
|
|
2139
|
+
socketPath: record.socketPath,
|
|
2140
|
+
createdAt: record.createdAt,
|
|
2141
|
+
heartbeatAt: record.heartbeatAt,
|
|
2142
|
+
ownerGeneration: record.ownerGeneration,
|
|
2143
|
+
queueDepth: record.queueDepth,
|
|
2144
|
+
...record.sharedRuntime === true ? { sharedRuntime: true } : {},
|
|
2145
|
+
...record.sessionWatch === true ? { sessionWatch: true } : {},
|
|
2146
|
+
...typeof record.mcpConfigPath === "string" ? { mcpConfigPath: record.mcpConfigPath } : {},
|
|
2147
|
+
...typeof record.mcpConfigFingerprint === "string" ? { mcpConfigFingerprint: record.mcpConfigFingerprint } : {}
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function hasValidQueueOwnerRecordFields(record) {
|
|
2151
|
+
return !Array.isArray(record) && isPositiveInteger(record.pid) && typeof record.sessionId === "string" && typeof record.socketPath === "string" && typeof record.createdAt === "string" && typeof record.heartbeatAt === "string" && isPositiveInteger(record.ownerGeneration) && isNonNegativeInteger(record.queueDepth);
|
|
2152
|
+
}
|
|
2153
|
+
function isPositiveInteger(value) {
|
|
2154
|
+
return Number.isInteger(value) && value > 0;
|
|
2155
|
+
}
|
|
2156
|
+
function isNonNegativeInteger(value) {
|
|
2157
|
+
return Number.isInteger(value) && value >= 0;
|
|
2158
|
+
}
|
|
2159
|
+
function createOwnerGeneration() {
|
|
2160
|
+
return randomInt(1, 2 ** 48);
|
|
2161
|
+
}
|
|
2162
|
+
function nowIso() {
|
|
2163
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2164
|
+
}
|
|
2165
|
+
function isQueueOwnerHeartbeatStale(owner) {
|
|
2166
|
+
const heartbeatMs = Date.parse(owner.heartbeatAt);
|
|
2167
|
+
if (!Number.isFinite(heartbeatMs)) return true;
|
|
2168
|
+
return Date.now() - heartbeatMs > QUEUE_OWNER_STALE_HEARTBEAT_MS;
|
|
2169
|
+
}
|
|
2170
|
+
async function ensureQueueDir() {
|
|
2171
|
+
const baseDir = queueBaseDir();
|
|
2172
|
+
try {
|
|
2173
|
+
await fs$1.mkdir(baseDir, {
|
|
2174
|
+
recursive: true,
|
|
2175
|
+
mode: 448
|
|
2176
|
+
});
|
|
2177
|
+
await fs$1.chmod(baseDir, 448);
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2180
|
+
throw new Error(`Failed to prepare queue directory ${baseDir}: ${message}`, { cause: error });
|
|
2181
|
+
}
|
|
2182
|
+
const socketDir = queueSocketBaseDir();
|
|
2183
|
+
if (socketDir) try {
|
|
2184
|
+
await fs$1.mkdir(socketDir, {
|
|
2185
|
+
recursive: true,
|
|
2186
|
+
mode: 448
|
|
2187
|
+
});
|
|
2188
|
+
await fs$1.chmod(socketDir, 448);
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2191
|
+
throw new Error(`Failed to prepare queue socket directory ${socketDir}: ${message}`, { cause: error });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async function removeSocketFile(socketPath) {
|
|
2195
|
+
if (process.platform === "win32") return;
|
|
2196
|
+
try {
|
|
2197
|
+
await fs$1.unlink(socketPath);
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
if (error.code !== "ENOENT") throw error;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
2203
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
2204
|
+
while (Date.now() <= deadline) {
|
|
2205
|
+
if (!isProcessAlive(pid)) return true;
|
|
2206
|
+
await waitMs(PROCESS_POLL_MS);
|
|
2207
|
+
}
|
|
2208
|
+
return !isProcessAlive(pid);
|
|
2209
|
+
}
|
|
2210
|
+
async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
|
|
2211
|
+
if (!await isCurrent()) return;
|
|
2212
|
+
await removeSocketFile(socketPath).catch(() => {});
|
|
2213
|
+
if (!await isCurrent()) return;
|
|
2214
|
+
await fs$1.unlink(queueLockFilePath(sessionId)).catch((error) => {
|
|
2215
|
+
if (error.code !== "ENOENT") throw error;
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
async function ownsQueueLease(owner, requireStale = false) {
|
|
2219
|
+
const current = await readQueueOwnerRecord(owner.sessionId);
|
|
2220
|
+
return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && current.sessionId === owner.sessionId && (!requireStale || !ownerIsAlive(current) || isQueueOwnerHeartbeatStale(current));
|
|
2221
|
+
}
|
|
2222
|
+
function ownerIsAlive(owner) {
|
|
2223
|
+
return owner.pid === process.pid || isProcessAlive(owner.pid);
|
|
2224
|
+
}
|
|
2225
|
+
async function readQueueOwnerRecord(sessionId) {
|
|
2226
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
2227
|
+
try {
|
|
2228
|
+
const payload = await fs$1.readFile(lockPath, "utf8");
|
|
2229
|
+
const parsed = parseQueueOwnerRecord(JSON.parse(payload));
|
|
2230
|
+
return parsed?.sessionId === sessionId ? parsed : void 0;
|
|
2231
|
+
} catch {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
async function terminateProcess(pid, beforeSignal) {
|
|
2236
|
+
if (!isProcessAlive(pid)) return false;
|
|
2237
|
+
for (const [signal, graceMs] of [["SIGTERM", PROCESS_SIGTERM_GRACE_MS], ["SIGKILL", PROCESS_SIGKILL_GRACE_MS]]) {
|
|
2238
|
+
if (beforeSignal && !await beforeSignal(signal)) return false;
|
|
2239
|
+
try {
|
|
2240
|
+
process.kill(pid, signal);
|
|
2241
|
+
} catch {
|
|
2242
|
+
return false;
|
|
2243
|
+
}
|
|
2244
|
+
if (await waitForProcessExit(pid, graceMs)) return true;
|
|
2245
|
+
}
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
async function resolveUsableQueueOwner(sessionId, owner) {
|
|
2249
|
+
if (ownerIsAlive(owner) && !isQueueOwnerHeartbeatStale(owner)) return owner;
|
|
2250
|
+
await terminateQueueOwnerForSession(sessionId, owner, true);
|
|
2251
|
+
const current = await readQueueOwnerRecord(sessionId);
|
|
2252
|
+
return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && ownerIsAlive(current) && !isQueueOwnerHeartbeatStale(current) ? current : void 0;
|
|
2253
|
+
}
|
|
2254
|
+
async function readQueueOwnerStatus(sessionId) {
|
|
2255
|
+
const observed = await readQueueOwnerRecord(sessionId);
|
|
2256
|
+
if (!observed) return;
|
|
2257
|
+
const owner = await resolveUsableQueueOwner(sessionId, observed);
|
|
2258
|
+
if (!owner) return;
|
|
2259
|
+
return {
|
|
2260
|
+
pid: owner.pid,
|
|
2261
|
+
socketPath: owner.socketPath,
|
|
2262
|
+
heartbeatAt: owner.heartbeatAt,
|
|
2263
|
+
ownerGeneration: owner.ownerGeneration,
|
|
2264
|
+
queueDepth: owner.queueDepth,
|
|
2265
|
+
alive: true,
|
|
2266
|
+
stale: isQueueOwnerHeartbeatStale(owner)
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, nowIsoFactory = nowIso) {
|
|
2270
|
+
const { mcpConfigPath, clock } = resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory);
|
|
2271
|
+
const mcpConfigMetadata = createMcpConfigMetadata(mcpConfigPath, readMcpConfigFingerprint(mcpConfigOrNowIsoFactory));
|
|
2272
|
+
await ensureQueueDir();
|
|
2273
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
2274
|
+
const socketPath = queueSocketPath(sessionId);
|
|
2275
|
+
const createdAt = clock();
|
|
2276
|
+
const ownerGeneration = createOwnerGeneration();
|
|
2277
|
+
const lease = {
|
|
2278
|
+
pid: process.pid,
|
|
2279
|
+
sessionId,
|
|
2280
|
+
lockPath,
|
|
2281
|
+
socketPath,
|
|
2282
|
+
createdAt,
|
|
2283
|
+
ownerGeneration,
|
|
2284
|
+
...mcpConfigMetadata,
|
|
2285
|
+
updates: Promise.resolve(),
|
|
2286
|
+
released: false
|
|
2287
|
+
};
|
|
2288
|
+
try {
|
|
2289
|
+
await stageQueueOwnerRecord(lease, 0, () => createdAt, async (tempPath, payload) => {
|
|
2290
|
+
try {
|
|
2291
|
+
await fs$1.link(tempPath, lockPath);
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
if (!isHardlinkFallbackError(error)) throw error;
|
|
2294
|
+
await fs$1.writeFile(lockPath, payload, {
|
|
2295
|
+
encoding: "utf8",
|
|
2296
|
+
flag: "wx",
|
|
2297
|
+
mode: 384
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
});
|
|
2301
|
+
await removeSocketFile(socketPath).catch(() => {});
|
|
2302
|
+
return lease;
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
return await handleLeaseCollision(sessionId, error);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
function readMcpConfigFingerprint(mcpConfigOrNowIsoFactory) {
|
|
2308
|
+
return typeof mcpConfigOrNowIsoFactory === "object" ? mcpConfigOrNowIsoFactory?.fingerprint : void 0;
|
|
2309
|
+
}
|
|
2310
|
+
function createMcpConfigMetadata(mcpConfigPath, mcpConfigFingerprint) {
|
|
2311
|
+
return {
|
|
2312
|
+
...mcpConfigPath ? { mcpConfigPath } : {},
|
|
2313
|
+
...mcpConfigFingerprint ? { mcpConfigFingerprint } : {}
|
|
2314
|
+
};
|
|
2315
|
+
}
|
|
2316
|
+
async function handleLeaseCollision(sessionId, error) {
|
|
2317
|
+
if (error.code !== "EEXIST") throw error;
|
|
2318
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
2319
|
+
if (!owner) {
|
|
2320
|
+
await cleanupAbandonedReservation(sessionId);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
await resolveUsableQueueOwner(sessionId, owner);
|
|
2324
|
+
}
|
|
2325
|
+
async function cleanupAbandonedReservation(sessionId) {
|
|
2326
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
2327
|
+
try {
|
|
2328
|
+
const observed = await fs$1.lstat(lockPath, { bigint: true });
|
|
2329
|
+
if (!observed.isFile() || Date.now() - Number(observed.mtimeMs) <= QUEUE_OWNER_STALE_HEARTBEAT_MS) return;
|
|
2330
|
+
const raw = await fs$1.readFile(lockPath, "utf8");
|
|
2331
|
+
if (await readQueueOwnerRecord(sessionId)) return;
|
|
2332
|
+
await cleanupQueueOwnerFiles(sessionId, queueSocketPath(sessionId), async () => {
|
|
2333
|
+
const current = await fs$1.lstat(lockPath, { bigint: true });
|
|
2334
|
+
return current.isFile() && current.dev === observed.dev && current.ino === observed.ino && current.mtimeNs === observed.mtimeNs && await fs$1.readFile(lockPath, "utf8") === raw;
|
|
2335
|
+
});
|
|
2336
|
+
} catch (error) {
|
|
2337
|
+
if (error.code !== "ENOENT") throw error;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
|
|
2341
|
+
if (typeof mcpConfigOrNowIsoFactory === "string") return {
|
|
2342
|
+
mcpConfigPath: mcpConfigOrNowIsoFactory,
|
|
2343
|
+
clock: nowIsoFactory
|
|
2344
|
+
};
|
|
2345
|
+
if (typeof mcpConfigOrNowIsoFactory === "function") return {
|
|
2346
|
+
mcpConfigPath: void 0,
|
|
2347
|
+
clock: mcpConfigOrNowIsoFactory
|
|
2348
|
+
};
|
|
2349
|
+
if (mcpConfigOrNowIsoFactory) return {
|
|
2350
|
+
mcpConfigPath: mcpConfigOrNowIsoFactory.path,
|
|
2351
|
+
clock: nowIsoFactory
|
|
2352
|
+
};
|
|
2353
|
+
return {
|
|
2354
|
+
mcpConfigPath: void 0,
|
|
2355
|
+
clock: nowIsoFactory
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
|
|
2359
|
+
if (lease.released) return Promise.resolve();
|
|
2360
|
+
const update = lease.updates.then(async () => {
|
|
2361
|
+
await stageQueueOwnerRecord(lease, options.queueDepth, nowIsoFactory, async (tempPath) => {
|
|
2362
|
+
if (await ownsQueueLease(lease)) await fs$1.rename(tempPath, lease.lockPath);
|
|
2363
|
+
});
|
|
2364
|
+
});
|
|
2365
|
+
lease.updates = update.catch(() => {});
|
|
2366
|
+
return update;
|
|
2367
|
+
}
|
|
2368
|
+
async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
|
|
2369
|
+
const payload = JSON.stringify({
|
|
2370
|
+
pid: process.pid,
|
|
2371
|
+
sessionId: lease.sessionId,
|
|
2372
|
+
socketPath: lease.socketPath,
|
|
2373
|
+
createdAt: lease.createdAt,
|
|
2374
|
+
heartbeatAt: clock(),
|
|
2375
|
+
ownerGeneration: lease.ownerGeneration,
|
|
2376
|
+
queueDepth: Math.max(0, Math.round(queueDepth)),
|
|
2377
|
+
sharedRuntime: true,
|
|
2378
|
+
sessionWatch: true,
|
|
2379
|
+
...lease.mcpConfigPath ? { mcpConfigPath: lease.mcpConfigPath } : {},
|
|
2380
|
+
...lease.mcpConfigFingerprint ? { mcpConfigFingerprint: lease.mcpConfigFingerprint } : {}
|
|
2381
|
+
}, null, 2);
|
|
2382
|
+
await withTempFile({
|
|
2383
|
+
rootDir: path.dirname(lease.lockPath),
|
|
2384
|
+
prefix: "owner",
|
|
2385
|
+
fileName: "lease"
|
|
2386
|
+
}, async (tempPath) => {
|
|
2387
|
+
await fs$1.writeFile(tempPath, `${payload}\n`, {
|
|
2388
|
+
encoding: "utf8",
|
|
2389
|
+
flag: "wx",
|
|
2390
|
+
mode: 384
|
|
2391
|
+
});
|
|
2392
|
+
await publish(tempPath, `${payload}\n`);
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
async function releaseQueueOwnerLease(lease) {
|
|
2396
|
+
lease.released = true;
|
|
2397
|
+
await lease.updates;
|
|
2398
|
+
await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease));
|
|
2399
|
+
}
|
|
2400
|
+
async function terminateQueueOwnerForSession(sessionId, expectedOwner, requireStale = false) {
|
|
2401
|
+
const owner = expectedOwner ?? await readQueueOwnerRecord(sessionId);
|
|
2402
|
+
if (!owner || owner.sessionId !== sessionId) return;
|
|
2403
|
+
if (ownerIsAlive(owner)) await terminateProcess(owner.pid, (signal) => ownsQueueLease(owner, requireStale && signal === "SIGTERM"));
|
|
2404
|
+
if (ownerIsAlive(owner)) return;
|
|
2405
|
+
await cleanupQueueOwnerFiles(sessionId, owner.socketPath, () => ownsQueueLease(owner));
|
|
2406
|
+
}
|
|
2407
|
+
async function waitMs(ms) {
|
|
2408
|
+
await new Promise((resolve) => {
|
|
2409
|
+
setTimeout(resolve, ms);
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
//#endregion
|
|
2413
|
+
//#region src/session/queue/ipc-transport.ts
|
|
2414
|
+
const QUEUE_CONNECT_ATTEMPTS = 40;
|
|
2415
|
+
const SOCKET_CONNECTION_TIMEOUT_MS = 5e3;
|
|
2416
|
+
function shouldRetryQueueConnect(error) {
|
|
2417
|
+
const code = error.code;
|
|
2418
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
2419
|
+
}
|
|
2420
|
+
async function connectToSocket(socketPath, timeoutMs = SOCKET_CONNECTION_TIMEOUT_MS) {
|
|
2421
|
+
return await new Promise((resolve, reject) => {
|
|
2422
|
+
const socket = net.createConnection(socketPath);
|
|
2423
|
+
let settled = false;
|
|
2424
|
+
const timeout = setTimeout(() => {
|
|
2425
|
+
if (settled) return;
|
|
2426
|
+
settled = true;
|
|
2427
|
+
socket.destroy();
|
|
2428
|
+
reject(/* @__PURE__ */ new Error(`Connection to ${socketPath} timed out after ${timeoutMs}ms`));
|
|
2429
|
+
}, timeoutMs);
|
|
2430
|
+
const onConnect = () => {
|
|
2431
|
+
if (settled) return;
|
|
2432
|
+
settled = true;
|
|
2433
|
+
clearTimeout(timeout);
|
|
2434
|
+
socket.off("error", onError);
|
|
2435
|
+
resolve(socket);
|
|
2436
|
+
};
|
|
2437
|
+
const onError = (error) => {
|
|
2438
|
+
if (settled) return;
|
|
2439
|
+
settled = true;
|
|
2440
|
+
clearTimeout(timeout);
|
|
2441
|
+
socket.off("connect", onConnect);
|
|
2442
|
+
reject(error);
|
|
2443
|
+
};
|
|
2444
|
+
socket.once("connect", onConnect);
|
|
2445
|
+
socket.once("error", onError);
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
async function connectToQueueOwner(owner, maxAttempts = QUEUE_CONNECT_ATTEMPTS) {
|
|
2449
|
+
const attempts = Math.max(1, Math.trunc(maxAttempts));
|
|
2450
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
2451
|
+
return await measurePerf("queue.connect", async () => await connectToSocket(owner.socketPath));
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
if (!shouldRetryQueueConnect(error)) throw error;
|
|
2454
|
+
await waitMs(50);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
//#endregion
|
|
2458
|
+
//#region src/session/queue/ipc-health.ts
|
|
2459
|
+
async function probeQueueOwnerHealth(sessionId) {
|
|
2460
|
+
const ownerRecord = await readQueueOwnerRecord(sessionId);
|
|
2461
|
+
if (!ownerRecord) return {
|
|
2462
|
+
sessionId,
|
|
2463
|
+
hasLease: false,
|
|
2464
|
+
healthy: false,
|
|
2465
|
+
socketReachable: false,
|
|
2466
|
+
pidAlive: false
|
|
2467
|
+
};
|
|
2468
|
+
const owner = await readQueueOwnerStatus(sessionId);
|
|
2469
|
+
if (!owner) return {
|
|
2470
|
+
sessionId,
|
|
2471
|
+
hasLease: false,
|
|
2472
|
+
healthy: false,
|
|
2473
|
+
socketReachable: false,
|
|
2474
|
+
pidAlive: false
|
|
2475
|
+
};
|
|
2476
|
+
const pidAlive = owner.alive;
|
|
2477
|
+
let socketReachable = false;
|
|
2478
|
+
try {
|
|
2479
|
+
const socket = await connectToQueueOwner(ownerRecord, 2);
|
|
2480
|
+
if (socket) {
|
|
2481
|
+
socketReachable = true;
|
|
2482
|
+
if (!socket.destroyed) socket.end();
|
|
2483
|
+
}
|
|
2484
|
+
} catch {
|
|
2485
|
+
socketReachable = false;
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
sessionId,
|
|
2489
|
+
hasLease: true,
|
|
2490
|
+
healthy: socketReachable,
|
|
2491
|
+
socketReachable,
|
|
2492
|
+
pidAlive,
|
|
2493
|
+
pid: owner.pid,
|
|
2494
|
+
socketPath: owner.socketPath,
|
|
2495
|
+
ownerGeneration: owner.ownerGeneration,
|
|
2496
|
+
queueDepth: owner.queueDepth
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
//#endregion
|
|
2500
|
+
//#region src/session/queue/messages.ts
|
|
2501
|
+
function asRecord(value) {
|
|
2502
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2503
|
+
return value;
|
|
2504
|
+
}
|
|
2505
|
+
function isPermissionMode(value) {
|
|
2506
|
+
return value === "approve-all" || value === "approve-reads" || value === "deny-all";
|
|
2507
|
+
}
|
|
2508
|
+
function isSessionResumePolicy(value) {
|
|
2509
|
+
return value === "allow-new" || value === "same-session-only";
|
|
2510
|
+
}
|
|
2511
|
+
function isNonInteractivePermissionPolicy(value) {
|
|
2512
|
+
return value === "deny" || value === "fail";
|
|
2513
|
+
}
|
|
2514
|
+
function isPermissionPolicy(value) {
|
|
2515
|
+
const record = asRecord(value);
|
|
2516
|
+
if (!record) return false;
|
|
2517
|
+
return hasValidPermissionRuleLists(record) && hasValidPermissionDefaultAction(record);
|
|
2518
|
+
}
|
|
2519
|
+
function hasValidPermissionRuleLists(record) {
|
|
2520
|
+
for (const key of [
|
|
2521
|
+
"autoApprove",
|
|
2522
|
+
"autoDeny",
|
|
2523
|
+
"escalate"
|
|
2524
|
+
]) if (!isOptionalStringList(record[key])) return false;
|
|
2525
|
+
return true;
|
|
2526
|
+
}
|
|
2527
|
+
function isOptionalStringList(value) {
|
|
2528
|
+
return value == null || Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2529
|
+
}
|
|
2530
|
+
function hasValidPermissionDefaultAction(record) {
|
|
2531
|
+
return record.defaultAction == null || record.defaultAction === "approve" || record.defaultAction === "deny" || record.defaultAction === "escalate";
|
|
2532
|
+
}
|
|
2533
|
+
function isOutputErrorCode(value) {
|
|
2534
|
+
return typeof value === "string" && OUTPUT_ERROR_CODES.includes(value);
|
|
2535
|
+
}
|
|
2536
|
+
function isOutputErrorOrigin(value) {
|
|
2537
|
+
return typeof value === "string" && OUTPUT_ERROR_ORIGINS.includes(value);
|
|
2538
|
+
}
|
|
2539
|
+
function isPermissionEscalationEvent(value) {
|
|
2540
|
+
const event = asRecord(value);
|
|
2541
|
+
return !!event && hasRequiredPermissionEscalationFields(event) && hasOptionalStringFields(event, [
|
|
2542
|
+
"toolName",
|
|
2543
|
+
"toolKind",
|
|
2544
|
+
"matchedRule"
|
|
2545
|
+
]);
|
|
2546
|
+
}
|
|
2547
|
+
function hasRequiredPermissionEscalationFields(event) {
|
|
2548
|
+
return event.type === "permission_escalation" && typeof event.sessionId === "string" && typeof event.toolCallId === "string" && typeof event.toolTitle === "string" && event.action === "escalate" && typeof event.message === "string" && typeof event.timestamp === "string";
|
|
2549
|
+
}
|
|
2550
|
+
function hasOptionalStringFields(record, keys) {
|
|
2551
|
+
return keys.every((key) => record[key] == null || typeof record[key] === "string");
|
|
2552
|
+
}
|
|
2553
|
+
function parseSessionOptions(value) {
|
|
2554
|
+
if (value == null) return;
|
|
2555
|
+
const record = asRecord(value);
|
|
2556
|
+
if (!record) return null;
|
|
2557
|
+
const sessionOptions = {};
|
|
2558
|
+
if (!assignSessionModel(sessionOptions, record.model)) return null;
|
|
2559
|
+
if (!assignSessionAllowedTools(sessionOptions, record.allowedTools)) return null;
|
|
2560
|
+
if (!assignSessionMaxTurns(sessionOptions, record.maxTurns)) return null;
|
|
2561
|
+
if (!assignSessionSystemPrompt(sessionOptions, record.systemPrompt)) return null;
|
|
2562
|
+
if (!assignSessionEnv(sessionOptions, record.env)) return null;
|
|
2563
|
+
return sessionOptions;
|
|
2564
|
+
}
|
|
2565
|
+
function assignSessionModel(options, value) {
|
|
2566
|
+
if (value == null) return true;
|
|
2567
|
+
if (typeof value !== "string" || value.trim().length === 0) return false;
|
|
2568
|
+
options.model = value;
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
function assignSessionAllowedTools(options, value) {
|
|
2572
|
+
if (value == null) return true;
|
|
2573
|
+
if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string")) return false;
|
|
2574
|
+
options.allowedTools = value;
|
|
2575
|
+
return true;
|
|
2576
|
+
}
|
|
2577
|
+
function assignSessionMaxTurns(options, value) {
|
|
2578
|
+
if (value == null) return true;
|
|
2579
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return false;
|
|
2580
|
+
options.maxTurns = Math.max(1, Math.round(value));
|
|
2581
|
+
return true;
|
|
2582
|
+
}
|
|
2583
|
+
function assignSessionSystemPrompt(options, value) {
|
|
2584
|
+
if (value == null) return true;
|
|
2585
|
+
if (typeof value === "string") {
|
|
2586
|
+
options.systemPrompt = value;
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
const systemPrompt = asRecord(value);
|
|
2590
|
+
if (!systemPrompt || typeof systemPrompt.append !== "string") return false;
|
|
2591
|
+
options.systemPrompt = { append: systemPrompt.append };
|
|
2592
|
+
return true;
|
|
2593
|
+
}
|
|
2594
|
+
function assignSessionEnv(options, value) {
|
|
2595
|
+
if (value == null) return true;
|
|
2596
|
+
const env = asRecord(value);
|
|
2597
|
+
if (!env) return false;
|
|
2598
|
+
const entries = Object.entries(env);
|
|
2599
|
+
if (entries.some(([, entryValue]) => typeof entryValue !== "string")) return false;
|
|
2600
|
+
options.env = Object.fromEntries(entries);
|
|
2601
|
+
return true;
|
|
2602
|
+
}
|
|
2603
|
+
function parseOwnerGeneration(value) {
|
|
2604
|
+
if (value == null) return;
|
|
2605
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return null;
|
|
2606
|
+
return value;
|
|
2607
|
+
}
|
|
2608
|
+
function parseNonNegativeInteger(value) {
|
|
2609
|
+
if (value == null) return;
|
|
2610
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
2611
|
+
return Math.round(value);
|
|
2612
|
+
}
|
|
2613
|
+
function parseQueueRequest(raw) {
|
|
2614
|
+
const request = asRecord(raw);
|
|
2615
|
+
if (!request || typeof request.type !== "string" || typeof request.requestId !== "string") return null;
|
|
2616
|
+
const ownerGeneration = parseOwnerGeneration(request.ownerGeneration);
|
|
2617
|
+
if (ownerGeneration === null) return null;
|
|
2618
|
+
return parseTypedQueueRequest(request, parseQueueRequestContext(request.requestId, ownerGeneration, request.timeoutMs));
|
|
2619
|
+
}
|
|
2620
|
+
function parseQueueRequestContext(requestId, ownerGeneration, timeoutRaw) {
|
|
2621
|
+
return {
|
|
2622
|
+
requestId,
|
|
2623
|
+
ownerGeneration,
|
|
2624
|
+
timeoutMs: parsePositiveTimeout(timeoutRaw)
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
function parsePositiveTimeout(value) {
|
|
2628
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return;
|
|
2629
|
+
return Math.round(value);
|
|
2630
|
+
}
|
|
2631
|
+
function parseTypedQueueRequest(request, context) {
|
|
2632
|
+
switch (request.type) {
|
|
2633
|
+
case "submit_prompt": return parseSubmitRequest(request, context);
|
|
2634
|
+
case "cancel_prompt": return parseCancelRequest(request, context);
|
|
2635
|
+
case "close_session": return {
|
|
2636
|
+
type: "close_session",
|
|
2637
|
+
...context
|
|
2638
|
+
};
|
|
2639
|
+
case "set_mode": return parseStringFieldRequest(request, context, "set_mode", "modeId");
|
|
2640
|
+
case "set_model": return parseStringFieldRequest(request, context, "set_model", "modelId");
|
|
2641
|
+
case "set_config_option": return parseSetConfigOptionRequest(request, context);
|
|
2642
|
+
default: return null;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function parseCancelRequest(request, context) {
|
|
2646
|
+
if (request.targetRequestId != null && !parseNonEmptyString(request.targetRequestId)) return null;
|
|
2647
|
+
return {
|
|
2648
|
+
type: "cancel_prompt",
|
|
2649
|
+
requestId: context.requestId,
|
|
2650
|
+
ownerGeneration: context.ownerGeneration,
|
|
2651
|
+
...typeof request.targetRequestId === "string" ? { targetRequestId: request.targetRequestId } : {}
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function parseSubmitRequest(request, context) {
|
|
2655
|
+
const parsed = parseSubmitRequestFields(request);
|
|
2656
|
+
if (!parsed) return null;
|
|
2657
|
+
return {
|
|
2658
|
+
...parsed,
|
|
2659
|
+
type: "submit_prompt",
|
|
2660
|
+
requestId: context.requestId,
|
|
2661
|
+
ownerGeneration: context.ownerGeneration,
|
|
2662
|
+
prompt: parsed.prompt ?? textPrompt(parsed.message),
|
|
2663
|
+
timeoutMs: context.timeoutMs
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
function parseSubmitRequestFields(request) {
|
|
2667
|
+
const parsed = {
|
|
2668
|
+
message: typeof request.message === "string" ? request.message : null,
|
|
2669
|
+
prompt: parseOptionalValue(request.prompt, isPromptInput),
|
|
2670
|
+
permissionMode: parseRequiredValue(request.permissionMode, isPermissionMode),
|
|
2671
|
+
resumePolicy: parseOptionalValue(request.resumePolicy, isSessionResumePolicy),
|
|
2672
|
+
nonInteractivePermissions: parseOptionalValue(request.nonInteractivePermissions, isNonInteractivePermissionPolicy),
|
|
2673
|
+
permissionPolicy: parseOptionalValue(request.permissionPolicy, isPermissionPolicy),
|
|
2674
|
+
suppressSdkConsoleErrors: parseOptionalBoolean(request.suppressSdkConsoleErrors),
|
|
2675
|
+
promptRetries: parseNonNegativeInteger(request.promptRetries),
|
|
2676
|
+
waitForCompletion: typeof request.waitForCompletion === "boolean" ? request.waitForCompletion : null,
|
|
2677
|
+
reportPromptStarted: parseOptionalBoolean(request.reportPromptStarted),
|
|
2678
|
+
sessionOptions: parseSessionOptions(request.sessionOptions)
|
|
2679
|
+
};
|
|
2680
|
+
if (Object.values(parsed).some((value) => value === null)) return null;
|
|
2681
|
+
for (const key of [
|
|
2682
|
+
"resumePolicy",
|
|
2683
|
+
"permissionPolicy",
|
|
2684
|
+
"suppressSdkConsoleErrors",
|
|
2685
|
+
"promptRetries",
|
|
2686
|
+
"reportPromptStarted",
|
|
2687
|
+
"sessionOptions"
|
|
2688
|
+
]) if (parsed[key] === void 0) delete parsed[key];
|
|
2689
|
+
return parsed;
|
|
2690
|
+
}
|
|
2691
|
+
function parseOptionalValue(value, guard) {
|
|
2692
|
+
if (value == null) return;
|
|
2693
|
+
return guard(value) ? value : null;
|
|
2694
|
+
}
|
|
2695
|
+
function parseRequiredValue(value, guard) {
|
|
2696
|
+
return guard(value) ? value : null;
|
|
2697
|
+
}
|
|
2698
|
+
function parseOptionalBoolean(value) {
|
|
2699
|
+
if (value == null) return;
|
|
2700
|
+
return typeof value === "boolean" ? value : null;
|
|
2701
|
+
}
|
|
2702
|
+
function parseStringFieldRequest(request, context, type, field) {
|
|
2703
|
+
const value = parseNonEmptyString(request[field]);
|
|
2704
|
+
if (!value) return null;
|
|
2705
|
+
return {
|
|
2706
|
+
type,
|
|
2707
|
+
...context,
|
|
2708
|
+
[field]: value
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
function parseSetConfigOptionRequest(request, context) {
|
|
2712
|
+
const configId = parseNonEmptyString(request.configId);
|
|
2713
|
+
const value = parseNonEmptyString(request.value);
|
|
2714
|
+
if (!configId || !value) return null;
|
|
2715
|
+
return {
|
|
2716
|
+
type: "set_config_option",
|
|
2717
|
+
...context,
|
|
2718
|
+
configId,
|
|
2719
|
+
value
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
function parseNonEmptyString(value) {
|
|
2723
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
2724
|
+
return value;
|
|
2725
|
+
}
|
|
2726
|
+
function parseSessionSendResult(raw) {
|
|
2727
|
+
const result = asRecord(raw);
|
|
2728
|
+
if (!result || !hasValidSessionSendResultCore(result)) return null;
|
|
2729
|
+
const permissionStats = asRecord(result.permissionStats);
|
|
2730
|
+
const record = asRecord(result.record);
|
|
2731
|
+
if (!permissionStats || !record) return null;
|
|
2732
|
+
if (!hasValidPermissionStats(permissionStats)) return null;
|
|
2733
|
+
if (!hasValidSessionRecordShape(record)) return null;
|
|
2734
|
+
return result;
|
|
2735
|
+
}
|
|
2736
|
+
function hasValidSessionSendResultCore(result) {
|
|
2737
|
+
return typeof result.stopReason === "string" && typeof result.sessionId === "string" && typeof result.resumed === "boolean" && (result._meta === void 0 || result._meta === null || asRecord(result._meta) !== void 0);
|
|
2738
|
+
}
|
|
2739
|
+
function hasValidPermissionStats(permissionStats) {
|
|
2740
|
+
return [
|
|
2741
|
+
"requested",
|
|
2742
|
+
"approved",
|
|
2743
|
+
"denied",
|
|
2744
|
+
"cancelled"
|
|
2745
|
+
].every((key) => typeof permissionStats[key] === "number");
|
|
2746
|
+
}
|
|
2747
|
+
function hasValidSessionRecordShape(record) {
|
|
2748
|
+
return hasStringFields(record, [
|
|
2749
|
+
"acpxRecordId",
|
|
2750
|
+
"acpSessionId",
|
|
2751
|
+
"agentCommand",
|
|
2752
|
+
"cwd",
|
|
2753
|
+
"createdAt",
|
|
2754
|
+
"lastUsedAt",
|
|
2755
|
+
"updated_at"
|
|
2756
|
+
]) && Array.isArray(record.messages) && typeof record.lastSeq === "number" && Number.isInteger(record.lastSeq) && !!record.eventLog && typeof record.eventLog === "object";
|
|
2757
|
+
}
|
|
2758
|
+
function hasStringFields(record, keys) {
|
|
2759
|
+
return keys.every((key) => typeof record[key] === "string");
|
|
2760
|
+
}
|
|
2761
|
+
function parseQueueOwnerMessage(raw) {
|
|
2762
|
+
const message = asRecord(raw);
|
|
2763
|
+
if (!message || typeof message.type !== "string" || typeof message.requestId !== "string") return null;
|
|
2764
|
+
const ownerGeneration = parseOwnerGeneration(message.ownerGeneration);
|
|
2765
|
+
if (ownerGeneration === null) return null;
|
|
2766
|
+
return parseTypedQueueOwnerMessage(message, {
|
|
2767
|
+
requestId: message.requestId,
|
|
2768
|
+
ownerGeneration
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
function parseTypedQueueOwnerMessage(message, context) {
|
|
2772
|
+
const parser = queueOwnerMessageParser(message.type);
|
|
2773
|
+
return parser ? parser(message, context) : null;
|
|
2774
|
+
}
|
|
2775
|
+
const QUEUE_OWNER_MESSAGE_PARSERS = {
|
|
2776
|
+
accepted: (_message, context) => ({
|
|
2777
|
+
type: "accepted",
|
|
2778
|
+
...context
|
|
2779
|
+
}),
|
|
2780
|
+
prompt_started: (_message, context) => ({
|
|
2781
|
+
type: "prompt_started",
|
|
2782
|
+
...context
|
|
2783
|
+
}),
|
|
2784
|
+
event: parseEventOwnerMessage,
|
|
2785
|
+
permission_escalation: parsePermissionEscalationOwnerMessage,
|
|
2786
|
+
result: parseResultOwnerMessage,
|
|
2787
|
+
cancel_result: (message, context) => parseBooleanResultOwnerMessage(message, context, "cancel_result", "cancelled"),
|
|
2788
|
+
close_session_result: (message, context) => parseBooleanResultOwnerMessage(message, context, "close_session_result", "closed"),
|
|
2789
|
+
set_mode_result: (message, context) => parseStringResultOwnerMessage(message, context, "set_mode_result", "modeId"),
|
|
2790
|
+
set_model_result: parseSetModelOwnerMessage,
|
|
2791
|
+
set_config_option_result: parseSetConfigOptionOwnerMessage,
|
|
2792
|
+
error: parseErrorOwnerMessage
|
|
2793
|
+
};
|
|
2794
|
+
function queueOwnerMessageParser(type) {
|
|
2795
|
+
return Object.hasOwn(QUEUE_OWNER_MESSAGE_PARSERS, type) ? QUEUE_OWNER_MESSAGE_PARSERS[type] : void 0;
|
|
2796
|
+
}
|
|
2797
|
+
function parseEventOwnerMessage(message, context) {
|
|
2798
|
+
if (!isAcpJsonRpcMessage(message.message)) return null;
|
|
2799
|
+
return {
|
|
2800
|
+
type: "event",
|
|
2801
|
+
...context,
|
|
2802
|
+
message: message.message
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
function parsePermissionEscalationOwnerMessage(message, context) {
|
|
2806
|
+
if (!isPermissionEscalationEvent(message.event)) return null;
|
|
2807
|
+
return {
|
|
2808
|
+
type: "permission_escalation",
|
|
2809
|
+
...context,
|
|
2810
|
+
event: message.event
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function parseResultOwnerMessage(message, context) {
|
|
2814
|
+
const result = parseSessionSendResult(message.result);
|
|
2815
|
+
if (!result) return null;
|
|
2816
|
+
return {
|
|
2817
|
+
type: "result",
|
|
2818
|
+
...context,
|
|
2819
|
+
result
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
function parseBooleanResultOwnerMessage(message, context, type, field) {
|
|
2823
|
+
if (typeof message[field] !== "boolean") return null;
|
|
2824
|
+
return {
|
|
2825
|
+
type,
|
|
2826
|
+
...context,
|
|
2827
|
+
[field]: message[field]
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function parseStringResultOwnerMessage(message, context, type, field) {
|
|
2831
|
+
if (typeof message[field] !== "string") return null;
|
|
2832
|
+
return {
|
|
2833
|
+
type,
|
|
2834
|
+
...context,
|
|
2835
|
+
[field]: message[field]
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function parseSetModelOwnerMessage(message, context) {
|
|
2839
|
+
if (typeof message.modelId !== "string") return null;
|
|
2840
|
+
const response = asRecord(message.response);
|
|
2841
|
+
if (message.response !== void 0 && (!response || !Array.isArray(response.configOptions))) return null;
|
|
2842
|
+
return {
|
|
2843
|
+
type: "set_model_result",
|
|
2844
|
+
...context,
|
|
2845
|
+
modelId: message.modelId,
|
|
2846
|
+
...response ? { response } : {}
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
function parseSetConfigOptionOwnerMessage(message, context) {
|
|
2850
|
+
const response = asRecord(message.response);
|
|
2851
|
+
if (!response || !Array.isArray(response.configOptions)) return null;
|
|
2852
|
+
return {
|
|
2853
|
+
type: "set_config_option_result",
|
|
2854
|
+
...context,
|
|
2855
|
+
response
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
function parseErrorOwnerMessage(message, context) {
|
|
2859
|
+
if (!isValidOwnerErrorCore(message)) return null;
|
|
2860
|
+
const outputAlreadyEmitted = typeof message.outputAlreadyEmitted === "boolean" ? message.outputAlreadyEmitted : void 0;
|
|
2861
|
+
return {
|
|
2862
|
+
type: "error",
|
|
2863
|
+
...context,
|
|
2864
|
+
code: message.code,
|
|
2865
|
+
detailCode: parseNonEmptyString(message.detailCode) ?? void 0,
|
|
2866
|
+
origin: message.origin,
|
|
2867
|
+
message: message.message,
|
|
2868
|
+
retryable: typeof message.retryable === "boolean" ? message.retryable : void 0,
|
|
2869
|
+
acp: toAcpErrorPayload(message.acp),
|
|
2870
|
+
...outputAlreadyEmitted === void 0 ? {} : { outputAlreadyEmitted }
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
function isValidOwnerErrorCore(message) {
|
|
2874
|
+
return typeof message.message === "string" && isOutputErrorCode(message.code) && isOutputErrorOrigin(message.origin);
|
|
2875
|
+
}
|
|
2876
|
+
//#endregion
|
|
2877
|
+
//#region src/session/queue/request-limit.ts
|
|
2878
|
+
function queueRequestByteLimit(raw = process.env.ACPX_QUEUE_MAX_REQUEST_BYTES) {
|
|
2879
|
+
if (raw === void 0 || raw.trim() === "") return;
|
|
2880
|
+
const limit = Number(raw);
|
|
2881
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || !/^\d+$/.test(raw.trim())) throw new Error("ACPX_QUEUE_MAX_REQUEST_BYTES must be a non-negative safe integer; zero is unlimited");
|
|
2882
|
+
return limit || void 0;
|
|
2883
|
+
}
|
|
2884
|
+
function queueRequestExceedsLimit(line, limit) {
|
|
2885
|
+
return limit !== void 0 && Buffer.byteLength(line, "utf8") > limit;
|
|
2886
|
+
}
|
|
2887
|
+
function assertQueueRequestSize(line) {
|
|
2888
|
+
const limit = queueRequestByteLimit();
|
|
2889
|
+
if (queueRequestExceedsLimit(line, limit)) throw new QueueProtocolError(`Queue request exceeded ACPX_QUEUE_MAX_REQUEST_BYTES (${limit} bytes); raise or unset the limit`, {
|
|
2890
|
+
detailCode: "QUEUE_REQUEST_TOO_LARGE",
|
|
2891
|
+
origin: "queue",
|
|
2892
|
+
retryable: false
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
//#endregion
|
|
2896
|
+
//#region src/session/queue/ipc.ts
|
|
2897
|
+
const MAX_MESSAGE_BUFFER_SIZE = 10485760;
|
|
2898
|
+
function assertOwnerGeneration(owner, message) {
|
|
2899
|
+
if (owner.ownerGeneration !== void 0 && message.ownerGeneration !== void 0 && message.ownerGeneration !== owner.ownerGeneration) throw new QueueProtocolError("Queue owner returned mismatched generation", {
|
|
2900
|
+
detailCode: "QUEUE_OWNER_GENERATION_MISMATCH",
|
|
2901
|
+
origin: "queue",
|
|
2902
|
+
retryable: true
|
|
2903
|
+
});
|
|
2904
|
+
return message;
|
|
2905
|
+
}
|
|
2906
|
+
function queueConnectionErrorFromOwner(message, outputAlreadyEmitted) {
|
|
2907
|
+
return new QueueConnectionError(message.message, {
|
|
2908
|
+
outputCode: message.code,
|
|
2909
|
+
detailCode: message.detailCode,
|
|
2910
|
+
origin: message.origin ?? "queue",
|
|
2911
|
+
retryable: message.retryable,
|
|
2912
|
+
acp: message.acp,
|
|
2913
|
+
...outputAlreadyEmitted ? { outputAlreadyEmitted: true } : {}
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
function makeMalformedQueueMessageError() {
|
|
2917
|
+
return new QueueProtocolError("Queue owner sent malformed message", {
|
|
2918
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
2919
|
+
origin: "queue",
|
|
2920
|
+
retryable: true
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
function notifyObserver(notify) {
|
|
2924
|
+
try {
|
|
2925
|
+
notify?.();
|
|
2926
|
+
} catch {}
|
|
2927
|
+
}
|
|
2928
|
+
function uncertainQueueOutcome(error) {
|
|
2929
|
+
const message = error instanceof Error ? error.message : "Queue request failed";
|
|
2930
|
+
const options = {
|
|
2931
|
+
detailCode: error instanceof QueueProtocolError ? error.detailCode : "QUEUE_SUBMISSION_OUTCOME_UNKNOWN",
|
|
2932
|
+
origin: "queue",
|
|
2933
|
+
retryable: false
|
|
2934
|
+
};
|
|
2935
|
+
return new (error instanceof QueueProtocolError ? QueueProtocolError : QueueConnectionError)(`${message}; request outcome is unknown. Do not automatically resubmit.`, options);
|
|
2936
|
+
}
|
|
2937
|
+
function emitQueueOwnerError(formatter, policy, sessionId, message) {
|
|
2938
|
+
notifyObserver(() => formatter.setContext({ sessionId }));
|
|
2939
|
+
const queueErrorAlreadyEmitted = policy?.queueErrorAlreadyEmitted ?? true;
|
|
2940
|
+
const shouldEmitInFormatter = message.outputAlreadyEmitted !== true || !queueErrorAlreadyEmitted;
|
|
2941
|
+
if (shouldEmitInFormatter) {
|
|
2942
|
+
notifyObserver(() => formatter.onError({
|
|
2943
|
+
code: message.code ?? "RUNTIME",
|
|
2944
|
+
detailCode: message.detailCode,
|
|
2945
|
+
origin: message.origin ?? "queue",
|
|
2946
|
+
message: message.message,
|
|
2947
|
+
retryable: message.retryable,
|
|
2948
|
+
acp: message.acp
|
|
2949
|
+
}));
|
|
2950
|
+
notifyObserver(() => formatter.flush());
|
|
2951
|
+
}
|
|
2952
|
+
return queueConnectionErrorFromOwner(message, queueErrorAlreadyEmitted || shouldEmitInFormatter);
|
|
2953
|
+
}
|
|
2954
|
+
function parseQueueOwnerResponseLine(owner, requestId, line) {
|
|
2955
|
+
let parsed;
|
|
2956
|
+
try {
|
|
2957
|
+
parsed = JSON.parse(line);
|
|
2958
|
+
} catch {
|
|
2959
|
+
throw new QueueProtocolError("Queue owner sent invalid JSON payload", {
|
|
2960
|
+
detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
|
|
2961
|
+
origin: "queue",
|
|
2962
|
+
retryable: true
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
const parsedMessage = parseQueueOwnerMessage(parsed);
|
|
2966
|
+
if (!parsedMessage) throw makeMalformedQueueMessageError();
|
|
2967
|
+
const message = assertOwnerGeneration(owner, parsedMessage);
|
|
2968
|
+
if (message.requestId !== requestId) throw makeMalformedQueueMessageError();
|
|
2969
|
+
return message;
|
|
2970
|
+
}
|
|
2971
|
+
async function runQueueOwnerRequest(options) {
|
|
2972
|
+
options.signal?.throwIfAborted();
|
|
2973
|
+
const requestLine = JSON.stringify(options.request);
|
|
2974
|
+
assertQueueRequestSize(requestLine);
|
|
2975
|
+
const socket = await connectToQueueOwner(options.owner);
|
|
2976
|
+
if (!socket) {
|
|
2977
|
+
options.signal?.throwIfAborted();
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
socket.setEncoding("utf8");
|
|
2981
|
+
return await new Promise((resolve, reject) => {
|
|
2982
|
+
let settled = false;
|
|
2983
|
+
let buffer = "";
|
|
2984
|
+
const state = { acknowledged: false };
|
|
2985
|
+
const finishResolve = (result) => {
|
|
2986
|
+
if (settled) return;
|
|
2987
|
+
settled = true;
|
|
2988
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
2989
|
+
socket.removeAllListeners();
|
|
2990
|
+
if (!socket.destroyed) socket.end();
|
|
2991
|
+
resolve(result);
|
|
2992
|
+
};
|
|
2993
|
+
const finishReject = (error) => {
|
|
2994
|
+
if (settled) return;
|
|
2995
|
+
settled = true;
|
|
2996
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
2997
|
+
socket.removeAllListeners();
|
|
2998
|
+
if (!socket.destroyed) socket.destroy();
|
|
2999
|
+
reject(error);
|
|
3000
|
+
};
|
|
3001
|
+
const onAbort = () => finishReject(options.signal?.reason);
|
|
3002
|
+
const controls = {
|
|
3003
|
+
state,
|
|
3004
|
+
resolve: finishResolve,
|
|
3005
|
+
reject: finishReject
|
|
3006
|
+
};
|
|
3007
|
+
const processLine = (line) => {
|
|
3008
|
+
let message;
|
|
3009
|
+
try {
|
|
3010
|
+
message = parseQueueOwnerResponseLine(options.owner, options.request.requestId, line);
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
finishReject(uncertainQueueOutcome(error));
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
if (message.type === "accepted") {
|
|
3016
|
+
state.acknowledged = true;
|
|
3017
|
+
notifyObserver(() => options.onAccepted?.(controls));
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
try {
|
|
3021
|
+
options.onMessage(message, controls);
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
finishReject(uncertainQueueOutcome(error));
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
socket.on("data", (chunk) => {
|
|
3027
|
+
buffer += chunk;
|
|
3028
|
+
if (buffer.length > 10485760) {
|
|
3029
|
+
socket.destroy();
|
|
3030
|
+
finishReject(uncertainQueueOutcome(/* @__PURE__ */ new Error(`Message buffer exceeded ${MAX_MESSAGE_BUFFER_SIZE} bytes`)));
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
let index = buffer.indexOf("\n");
|
|
3034
|
+
while (index >= 0) {
|
|
3035
|
+
if (settled) return;
|
|
3036
|
+
const line = buffer.slice(0, index).trim();
|
|
3037
|
+
buffer = buffer.slice(index + 1);
|
|
3038
|
+
if (line.length > 0) processLine(line);
|
|
3039
|
+
index = buffer.indexOf("\n");
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
socket.once("error", (error) => {
|
|
3043
|
+
finishReject(uncertainQueueOutcome(error));
|
|
3044
|
+
});
|
|
3045
|
+
socket.once("close", () => {
|
|
3046
|
+
if (settled) return;
|
|
3047
|
+
options.onClose(controls);
|
|
3048
|
+
});
|
|
3049
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3050
|
+
if (options.signal?.aborted) {
|
|
3051
|
+
onAbort();
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
try {
|
|
3055
|
+
socket.write(`${requestLine}\n`);
|
|
3056
|
+
} catch (error) {
|
|
3057
|
+
finishReject(uncertainQueueOutcome(error));
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
function missingQueueAckError() {
|
|
3062
|
+
return new QueueConnectionError("Queue owner did not acknowledge request; outcome unknown", {
|
|
3063
|
+
detailCode: "QUEUE_ACK_MISSING",
|
|
3064
|
+
origin: "queue",
|
|
3065
|
+
retryable: false
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
function unexpectedQueueResponseError() {
|
|
3069
|
+
return new QueueProtocolError("Queue owner returned unexpected response; outcome unknown", {
|
|
3070
|
+
detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
|
|
3071
|
+
origin: "queue",
|
|
3072
|
+
retryable: false
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
function handleAcknowledgedSubmitMessage(message, controls, formatter) {
|
|
3076
|
+
if (message.type === "event") {
|
|
3077
|
+
notifyObserver(() => formatter.onAcpMessage(message.message));
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
if (message.type === "permission_escalation") {
|
|
3081
|
+
notifyObserver(() => formatter.onPermissionEscalation(message.event));
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
if (message.type === "result") {
|
|
3085
|
+
notifyObserver(() => formatter.flush());
|
|
3086
|
+
controls.resolve(message.result);
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
controls.reject(unexpectedQueueResponseError());
|
|
3090
|
+
}
|
|
3091
|
+
function handleSubmitQueueOwnerMessage(message, controls, options) {
|
|
3092
|
+
if (message.type === "error") {
|
|
3093
|
+
controls.reject(emitQueueOwnerError(options.outputFormatter, options.errorEmissionPolicy, options.sessionId, message));
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
if (!controls.state.acknowledged) {
|
|
3097
|
+
controls.reject(missingQueueAckError());
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
if (message.type === "prompt_started") {
|
|
3101
|
+
notifyObserver(options.onPromptStarted);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
handleAcknowledgedSubmitMessage(message, controls, options.outputFormatter);
|
|
3105
|
+
}
|
|
3106
|
+
async function submitToQueueOwner(owner, options) {
|
|
3107
|
+
const requestId = options.requestId ?? randomUUID();
|
|
3108
|
+
const request = {
|
|
3109
|
+
type: "submit_prompt",
|
|
3110
|
+
requestId,
|
|
3111
|
+
ownerGeneration: owner.ownerGeneration,
|
|
3112
|
+
message: options.message,
|
|
3113
|
+
prompt: options.prompt,
|
|
3114
|
+
permissionMode: options.permissionMode,
|
|
3115
|
+
resumePolicy: options.resumePolicy,
|
|
3116
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
3117
|
+
permissionPolicy: options.permissionPolicy,
|
|
3118
|
+
timeoutMs: options.timeoutMs,
|
|
3119
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
3120
|
+
promptRetries: options.promptRetries ?? 0,
|
|
3121
|
+
waitForCompletion: options.waitForCompletion,
|
|
3122
|
+
...options.onPromptStarted ? { reportPromptStarted: true } : {},
|
|
3123
|
+
sessionOptions: options.sessionOptions
|
|
3124
|
+
};
|
|
3125
|
+
notifyObserver(() => options.outputFormatter.setContext({ sessionId: options.sessionId }));
|
|
3126
|
+
return await runQueueOwnerRequest({
|
|
3127
|
+
owner,
|
|
3128
|
+
request,
|
|
3129
|
+
signal: options.signal,
|
|
3130
|
+
onAccepted: ({ resolve }) => {
|
|
3131
|
+
notifyObserver(options.onQueueAccepted);
|
|
3132
|
+
notifyObserver(() => options.outputFormatter.setContext({ sessionId: options.sessionId }));
|
|
3133
|
+
if (!options.waitForCompletion) resolve({
|
|
3134
|
+
queued: true,
|
|
3135
|
+
sessionId: options.sessionId,
|
|
3136
|
+
requestId
|
|
3137
|
+
});
|
|
3138
|
+
},
|
|
3139
|
+
onMessage: (message, controls) => {
|
|
3140
|
+
handleSubmitQueueOwnerMessage(message, controls, options);
|
|
3141
|
+
},
|
|
3142
|
+
onClose: ({ state, resolve, reject }) => {
|
|
3143
|
+
if (!state.acknowledged) {
|
|
3144
|
+
reject(new QueueConnectionError("Queue owner disconnected before acknowledging request; outcome unknown", {
|
|
3145
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
|
|
3146
|
+
origin: "queue",
|
|
3147
|
+
retryable: false
|
|
3148
|
+
}));
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
if (!options.waitForCompletion) {
|
|
3152
|
+
resolve({
|
|
3153
|
+
queued: true,
|
|
3154
|
+
sessionId: options.sessionId,
|
|
3155
|
+
requestId
|
|
3156
|
+
});
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
reject(new QueueConnectionError("Queue owner disconnected before prompt completion; outcome unknown", {
|
|
3160
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
|
|
3161
|
+
origin: "queue",
|
|
3162
|
+
retryable: false
|
|
3163
|
+
}));
|
|
3164
|
+
}
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
|
|
3168
|
+
return await runQueueOwnerRequest({
|
|
3169
|
+
owner,
|
|
3170
|
+
request,
|
|
3171
|
+
onMessage: (message, { state, resolve, reject }) => {
|
|
3172
|
+
if (message.type === "error") {
|
|
3173
|
+
reject(queueConnectionErrorFromOwner(message, false));
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
if (!state.acknowledged) {
|
|
3177
|
+
reject(missingQueueAckError());
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
if (!isExpectedResponse(message)) {
|
|
3181
|
+
reject(unexpectedQueueResponseError());
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
resolve(message);
|
|
3185
|
+
},
|
|
3186
|
+
onClose: ({ state, reject }) => {
|
|
3187
|
+
if (!state.acknowledged) {
|
|
3188
|
+
reject(new QueueConnectionError("Queue owner disconnected before acknowledging request; outcome unknown", {
|
|
3189
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
|
|
3190
|
+
origin: "queue",
|
|
3191
|
+
retryable: false
|
|
3192
|
+
}));
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
reject(new QueueConnectionError("Queue owner disconnected before responding; outcome unknown", {
|
|
3196
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
|
|
3197
|
+
origin: "queue",
|
|
3198
|
+
retryable: false
|
|
3199
|
+
}));
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
async function submitCancelToQueueOwner(owner, targetRequestId) {
|
|
3204
|
+
if (targetRequestId !== void 0) assertSharedRuntimeOwner(owner);
|
|
3205
|
+
return (await submitControlToQueueOwner(owner, {
|
|
3206
|
+
type: "cancel_prompt",
|
|
3207
|
+
requestId: randomUUID(),
|
|
3208
|
+
ownerGeneration: owner.ownerGeneration,
|
|
3209
|
+
...targetRequestId !== void 0 ? { targetRequestId } : {}
|
|
3210
|
+
}, (message) => message.type === "cancel_result"))?.cancelled;
|
|
3211
|
+
}
|
|
3212
|
+
async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
|
|
3213
|
+
return await submitControlToQueueOwner(owner, {
|
|
3214
|
+
type: "set_mode",
|
|
3215
|
+
requestId: randomUUID(),
|
|
3216
|
+
ownerGeneration: owner.ownerGeneration,
|
|
3217
|
+
modeId,
|
|
3218
|
+
timeoutMs
|
|
3219
|
+
}, (message) => message.type === "set_mode_result") ? true : void 0;
|
|
3220
|
+
}
|
|
3221
|
+
async function submitSetModelToQueueOwner(owner, modelId, timeoutMs) {
|
|
3222
|
+
return await submitControlToQueueOwner(owner, {
|
|
3223
|
+
type: "set_model",
|
|
3224
|
+
requestId: randomUUID(),
|
|
3225
|
+
ownerGeneration: owner.ownerGeneration,
|
|
3226
|
+
modelId,
|
|
3227
|
+
timeoutMs
|
|
3228
|
+
}, (message) => message.type === "set_model_result");
|
|
3229
|
+
}
|
|
3230
|
+
async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
|
|
3231
|
+
return (await submitControlToQueueOwner(owner, {
|
|
3232
|
+
type: "set_config_option",
|
|
3233
|
+
requestId: randomUUID(),
|
|
3234
|
+
ownerGeneration: owner.ownerGeneration,
|
|
3235
|
+
configId,
|
|
3236
|
+
value,
|
|
3237
|
+
timeoutMs
|
|
3238
|
+
}, (message) => message.type === "set_config_option_result"))?.response;
|
|
3239
|
+
}
|
|
3240
|
+
async function submitCloseSessionToQueueOwner(owner, timeoutMs) {
|
|
3241
|
+
return (await submitControlToQueueOwner(owner, {
|
|
3242
|
+
type: "close_session",
|
|
3243
|
+
requestId: randomUUID(),
|
|
3244
|
+
ownerGeneration: owner.ownerGeneration,
|
|
3245
|
+
timeoutMs
|
|
3246
|
+
}, (message) => message.type === "close_session_result"))?.closed;
|
|
3247
|
+
}
|
|
3248
|
+
function queueOwnerMcpConfigMatches(owner, options) {
|
|
3249
|
+
return owner.mcpConfigPath === options.mcpConfigPath && owner.mcpConfigFingerprint === options.mcpConfigFingerprint;
|
|
3250
|
+
}
|
|
3251
|
+
function assertQueueOwnerMcpConfigMatches(owner, options) {
|
|
3252
|
+
if (queueOwnerMcpConfigMatches(owner, options)) return;
|
|
3253
|
+
throw new QueueConnectionError("Session queue owner uses a different MCP config; close the session before retrying", {
|
|
3254
|
+
detailCode: "QUEUE_MCP_CONFIG_CONFLICT",
|
|
3255
|
+
origin: "queue",
|
|
3256
|
+
retryable: false
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
function assertSharedRuntimeOwner(owner) {
|
|
3260
|
+
if (owner.sharedRuntime === true) return;
|
|
3261
|
+
throw new QueueConnectionError("The running queue owner predates shared runtime support. Wait for its idle expiry or close the session before retrying.", {
|
|
3262
|
+
detailCode: "QUEUE_SHARED_RUNTIME_UNSUPPORTED",
|
|
3263
|
+
origin: "queue",
|
|
3264
|
+
retryable: false
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
async function trySubmitToRunningOwner(options) {
|
|
3268
|
+
options.signal?.throwIfAborted();
|
|
3269
|
+
const observed = await readQueueOwnerRecord(options.sessionId);
|
|
3270
|
+
if (!observed) return;
|
|
3271
|
+
const owner = await resolveUsableQueueOwner(options.sessionId, observed);
|
|
3272
|
+
if (!owner) return;
|
|
3273
|
+
if (options.requireSharedRuntime) assertSharedRuntimeOwner(owner);
|
|
3274
|
+
assertQueueOwnerMcpConfigMatches(owner, options);
|
|
3275
|
+
const submitted = await submitToQueueOwner(owner, options);
|
|
3276
|
+
if (submitted) {
|
|
3277
|
+
if (options.verbose) process.stderr.write(`[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
|
|
3278
|
+
return submitted;
|
|
3279
|
+
}
|
|
3280
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
3281
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting queue requests", {
|
|
3282
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
3283
|
+
origin: "queue",
|
|
3284
|
+
retryable: true
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
async function tryControlOnRunningOwner(options) {
|
|
3288
|
+
const owner = await readQueueOwnerRecord(options.sessionId);
|
|
3289
|
+
if (!owner) return;
|
|
3290
|
+
const response = await options.submit(owner);
|
|
3291
|
+
if (response !== void 0) {
|
|
3292
|
+
if (options.verbose) process.stderr.write(`${options.logPrefix} ${owner.pid} for session ${options.sessionId}\n`);
|
|
3293
|
+
return response;
|
|
3294
|
+
}
|
|
3295
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
3296
|
+
throw new QueueConnectionError(`Session queue owner is running but not accepting ${options.requestName} requests`, {
|
|
3297
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
3298
|
+
origin: "queue",
|
|
3299
|
+
retryable: true
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
async function tryCloseSessionOnRunningOwner(options) {
|
|
3303
|
+
return await tryControlOnRunningOwner({
|
|
3304
|
+
sessionId: options.sessionId,
|
|
3305
|
+
verbose: options.verbose,
|
|
3306
|
+
requestName: "close_session",
|
|
3307
|
+
logPrefix: "[acpx] requested session/close on active owner pid",
|
|
3308
|
+
submit: (owner) => submitCloseSessionToQueueOwner(owner, options.timeoutMs)
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
async function tryCancelOnRunningOwner(options) {
|
|
3312
|
+
return await tryControlOnRunningOwner({
|
|
3313
|
+
sessionId: options.sessionId,
|
|
3314
|
+
verbose: options.verbose,
|
|
3315
|
+
requestName: "cancel",
|
|
3316
|
+
logPrefix: "[acpx] requested cancel on active owner pid",
|
|
3317
|
+
submit: (owner) => submitCancelToQueueOwner(owner, options.targetRequestId)
|
|
3318
|
+
});
|
|
3319
|
+
}
|
|
3320
|
+
async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
|
|
3321
|
+
return await tryControlOnRunningOwner({
|
|
3322
|
+
sessionId,
|
|
3323
|
+
verbose,
|
|
3324
|
+
requestName: "set_mode",
|
|
3325
|
+
logPrefix: "[acpx] requested session/set_mode on owner pid",
|
|
3326
|
+
submit: (owner) => submitSetModeToQueueOwner(owner, modeId, timeoutMs)
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose) {
|
|
3330
|
+
return await tryControlOnRunningOwner({
|
|
3331
|
+
sessionId,
|
|
3332
|
+
verbose,
|
|
3333
|
+
requestName: "set_model",
|
|
3334
|
+
logPrefix: "[acpx] requested a model config update on owner pid",
|
|
3335
|
+
submit: (owner) => submitSetModelToQueueOwner(owner, modelId, timeoutMs)
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
|
|
3339
|
+
return await tryControlOnRunningOwner({
|
|
3340
|
+
sessionId,
|
|
3341
|
+
verbose,
|
|
3342
|
+
requestName: "set_config_option",
|
|
3343
|
+
logPrefix: "[acpx] requested session/set_config_option on owner pid",
|
|
3344
|
+
submit: (owner) => submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs)
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
//#endregion
|
|
3348
|
+
export { startPerfTimer as $, listSessions as A, AgentSpawnError as At, extractAgentSessionId as B, SessionModeReplayError as Bt, textPrompt as C, OUTPUT_ERROR_ORIGINS as Ct, findSession as D, SESSION_RECORD_SCHEMA as Dt, findGitRepositoryRoot as E, PERMISSION_POLICY_ACTIONS as Et, resolveSessionRecord as F, GeminiAcpStartupTimeoutError as Ft, watchSession as G, SESSION_JOURNAL_SCHEMA as H, SessionNotFoundError as Ht, writeSessionRecord as I, PermissionDeniedError as It, incrementPerfCounter as J, formatPerfMetric as K, assertPersistedKeyPolicy as L, PermissionPromptUnavailableError as Lt, normalizeName as M, AuthPolicyError as Mt, pruneSessions as N, ClaudeAcpSessionCreateTimeoutError as Nt, findSessionByDirectoryWalk as O, AcpxOperationalError as Ot, readSessionRecord as P, CopilotAcpUnsupportedError as Pt, setPerfGauge as Q, parseSessionRecord as R, QueueConnectionError as Rt, promptToDisplayText as S, OUTPUT_ERROR_CODES as St, absolutePath as T, PERMISSION_MODES as Tt, SessionJournalReader as U, SessionResumeRequiredError as Ut, normalizeAgentSessionId as V, SessionModelReplayError as Vt, SessionWatchError as W, UnsupportedPromptContentError as Wt, recordPerfDuration as X, measurePerf as Y, resetPerfMetrics as Z, waitMs as _, formatUnknownErrorMessage as _t, trySetModelOnRunningOwner as a, parsePermissionNotice as at, mergePromptSourceWithText as b, EXIT_CODES as bt, queueRequestExceedsLimit as c, defaultSessionEventLog as ct, readQueueOwnerRecord as d, sessionEventLockPath as dt, extractSessionUpdateNotification as et, refreshQueueOwnerLease as f, sessionEventSegmentPath as ft, tryAcquireQueueOwnerLease as g, extractAcpError as gt, terminateQueueOwnerForSession as h, isProcessAlive as ht, trySetModeOnRunningOwner as i, parseJsonRpcErrorMessage as it, listSessionsForAgent as j, AgentStartupError as jt, isoNow as k, AgentDisconnectedError as kt, parseQueueRequest as l, sessionBaseDir as lt, terminateProcess as m, writePrivateJsonFile as mt, tryCloseSessionOnRunningOwner as n, isAcpMessageObject as nt, trySubmitToRunningOwner as o, parsePromptStopReason as ot, releaseQueueOwnerLease as p, writePrivateFile as pt, getPerfMetricsSnapshot as q, trySetConfigOptionOnRunningOwner as r, isSessionUpdateNotification as rt, queueRequestByteLimit as s, DEFAULT_EVENT_SEGMENT_MAX_BYTES as st, tryCancelOnRunningOwner as t, isAcpJsonRpcMessage as tt, probeQueueOwnerHealth as u, sessionEventActivePath as ut, PromptInputValidationError as v, isAcpResourceNotFoundError as vt, DEFAULT_HISTORY_LIMIT as w, OUTPUT_FORMATS as wt, parsePromptSource as x, NON_INTERACTIVE_PERMISSION_POLICIES as xt, getUnsupportedPromptContentMessage as y, AUTH_POLICIES as yt, serializeSessionRecordForDisk as z, SessionConfigOptionReplayError as zt };
|
|
3349
|
+
|
|
3350
|
+
//# sourceMappingURL=ipc-D3akHMEq.js.map
|