acpx 0.6.1 → 0.8.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 +8 -2
- package/dist/{cli-Ddxpnz9X.js → cli-BGYGVo3b.js} +35 -10
- package/dist/cli-BGYGVo3b.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -19
- package/dist/cli.js.map +1 -1
- package/dist/{client-2fTFutRH.d.ts → client-FzXPdgP7.d.ts} +10 -4
- package/dist/client-FzXPdgP7.d.ts.map +1 -0
- package/dist/{flags-yXzUm7Aq.js → flags-D706STfk.js} +46 -6
- package/dist/flags-D706STfk.js.map +1 -0
- package/dist/{flows-CDsfbaA2.js → flows-hcjHmU7P.js} +117 -11
- package/dist/flows-hcjHmU7P.js.map +1 -0
- package/dist/flows.d.ts +19 -3
- package/dist/flows.d.ts.map +1 -1
- package/dist/flows.js +2 -2
- package/dist/{prompt-turn-BY5SwU1F.js → live-checkpoint-B9ctAuqV.js} +1335 -82
- package/dist/live-checkpoint-B9ctAuqV.js.map +1 -0
- package/dist/output-BL9XRWzS.js +3712 -0
- package/dist/output-BL9XRWzS.js.map +1 -0
- package/dist/runtime.d.ts +32 -6
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +169 -32
- package/dist/runtime.js.map +1 -1
- package/dist/{types-CVBeQyi3.d.ts → session-options-BJyG6zEH.d.ts} +56 -3
- package/dist/session-options-BJyG6zEH.d.ts.map +1 -0
- package/package.json +27 -25
- package/skills/acpx/SKILL.md +200 -9
- package/dist/cli-Ddxpnz9X.js.map +0 -1
- package/dist/client-2fTFutRH.d.ts.map +0 -1
- package/dist/flags-yXzUm7Aq.js.map +0 -1
- package/dist/flows-CDsfbaA2.js.map +0 -1
- package/dist/ipc-BruTG5Fb.js +0 -1241
- package/dist/ipc-BruTG5Fb.js.map +0 -1
- package/dist/jsonrpc-DSxh2w5R.js +0 -68
- package/dist/jsonrpc-DSxh2w5R.js.map +0 -1
- package/dist/output-DmHvT8vm.js +0 -807
- package/dist/output-DmHvT8vm.js.map +0 -1
- package/dist/perf-metrics-C2pXfxvR.js +0 -598
- package/dist/perf-metrics-C2pXfxvR.js.map +0 -1
- package/dist/prompt-turn-BY5SwU1F.js.map +0 -1
- package/dist/render-yqwtaOX4.js +0 -172
- package/dist/render-yqwtaOX4.js.map +0 -1
- package/dist/rolldown-runtime-CiIaOW0V.js +0 -13
- package/dist/session-BwgaPK8-.js +0 -1526
- package/dist/session-BwgaPK8-.js.map +0 -1
- package/dist/session-options-pCbHn_n7.d.ts +0 -13
- package/dist/session-options-pCbHn_n7.d.ts.map +0 -1
- package/dist/types-CVBeQyi3.d.ts.map +0 -1
|
@@ -0,0 +1,3712 @@
|
|
|
1
|
+
import { $ as getPerfMetricsSnapshot, A as parsePromptStopReason, B as normalizeName, C as mergeSessionOptions, Ct as formatErrorMessage, D as extractSessionUpdateNotification, E as AcpClient, F as findSession, H as resolveSessionRecord, I as findSessionByDirectoryWalk, J as sessionEventActivePath, K as defaultSessionEventLog, L as isoNow, Mt as OUTPUT_ERROR_CODES, N as absolutePath, Nt as OUTPUT_ERROR_ORIGINS, O as isAcpJsonRpcMessage, Ot as toAcpErrorPayload, P as findGitRepositoryRoot, Q as formatPerfMetric, R as listSessions, Rt as QueueConnectionError, S as trimConversationForRuntime, T as sessionOptionsFromRecord, Tt as normalizeOutputError, U as writeSessionRecord, V as pruneSessions, X as sessionEventSegmentPath, Y as sessionEventLockPath, _ as cloneSessionConversation, _t as withTimeout, a as applyConversation, at as startPerfTimer, b as recordPromptSubmission, c as applyRequestedModelIfAdvertised, d as setDesiredConfigOption, et as incrementPerfCounter, f as setDesiredModeId, ft as promptToDisplayText, g as cloneSessionAcpxState, gt as withInterrupt, h as applyConfigOptionsToRecord, ht as TimeoutError, i as connectAndLoadSession, it as setPerfGauge, k as parseJsonRpcErrorMessage, l as assertRequestedModelSupported, lt as isPromptInput, m as syncAdvertisedModelState, mt as InterruptedError, n as runPromptTurn, nt as recordPerfDuration, o as applyLifecycleSnapshotToRecord, p as setDesiredModelId, pt as textPrompt, q as sessionBaseDir, r as withConnectedSession, rt as resetPerfMetrics, st as normalizeRuntimeSessionId, t as LiveSessionCheckpoint, tt as measurePerf, u as setCurrentModelId, v as createSessionConversation, w as persistSessionOptions, wt as isRetryablePromptError, x as recordSessionUpdate, y as recordClientOperation, z as listSessionsForAgent, zt as QueueProtocolError } from "./live-checkpoint-B9ctAuqV.js";
|
|
2
|
+
import fs, { realpathSync } 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 { spawn } from "node:child_process";
|
|
7
|
+
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
8
|
+
import net from "node:net";
|
|
9
|
+
//#region \0rolldown/runtime.js
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __exportAll = (all, no_symbols) => {
|
|
12
|
+
let target = {};
|
|
13
|
+
for (var name in all) __defProp(target, name, {
|
|
14
|
+
get: all[name],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
18
|
+
return target;
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/cli/session/contracts.ts
|
|
22
|
+
const DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
|
|
23
|
+
function normalizeQueueOwnerTtlMs(ttlMs) {
|
|
24
|
+
if (ttlMs == null) return DEFAULT_QUEUE_OWNER_TTL_MS;
|
|
25
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) return DEFAULT_QUEUE_OWNER_TTL_MS;
|
|
26
|
+
return Math.round(ttlMs);
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/cli/queue/paths.ts
|
|
30
|
+
function shortHash(value, length) {
|
|
31
|
+
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
32
|
+
}
|
|
33
|
+
function queueKeyForSession(sessionId) {
|
|
34
|
+
return shortHash(sessionId, 24);
|
|
35
|
+
}
|
|
36
|
+
function queueBaseDir(homeDir = os.homedir()) {
|
|
37
|
+
return path.join(homeDir, ".acpx", "queues");
|
|
38
|
+
}
|
|
39
|
+
function queueSocketBaseDir(homeDir = os.homedir()) {
|
|
40
|
+
if (process.platform === "win32") return;
|
|
41
|
+
return path.join("/tmp", `acpx-${shortHash(homeDir, 10)}`);
|
|
42
|
+
}
|
|
43
|
+
function queueLockFilePath(sessionId, homeDir = os.homedir()) {
|
|
44
|
+
return path.join(queueBaseDir(homeDir), `${queueKeyForSession(sessionId)}.lock`);
|
|
45
|
+
}
|
|
46
|
+
function queueSocketPath(sessionId, homeDir = os.homedir()) {
|
|
47
|
+
const key = queueKeyForSession(sessionId);
|
|
48
|
+
if (process.platform === "win32") return `\\\\.\\pipe\\acpx-${key}`;
|
|
49
|
+
return path.join(queueSocketBaseDir(homeDir) ?? "/tmp", `${key}.sock`);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/cli/queue/lease-store.ts
|
|
53
|
+
const PROCESS_EXIT_GRACE_MS = 1500;
|
|
54
|
+
const PROCESS_POLL_MS = 50;
|
|
55
|
+
const QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
|
|
56
|
+
function parseQueueOwnerRecord(raw) {
|
|
57
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
58
|
+
const record = raw;
|
|
59
|
+
if (!Number.isInteger(record.pid) || record.pid <= 0 || typeof record.sessionId !== "string" || typeof record.socketPath !== "string" || typeof record.createdAt !== "string" || typeof record.heartbeatAt !== "string" || !Number.isInteger(record.ownerGeneration) || record.ownerGeneration <= 0 || !Number.isInteger(record.queueDepth) || record.queueDepth < 0) return null;
|
|
60
|
+
return {
|
|
61
|
+
pid: record.pid,
|
|
62
|
+
sessionId: record.sessionId,
|
|
63
|
+
socketPath: record.socketPath,
|
|
64
|
+
createdAt: record.createdAt,
|
|
65
|
+
heartbeatAt: record.heartbeatAt,
|
|
66
|
+
ownerGeneration: record.ownerGeneration,
|
|
67
|
+
queueDepth: record.queueDepth
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function createOwnerGeneration() {
|
|
71
|
+
return randomInt(1, 2 ** 48);
|
|
72
|
+
}
|
|
73
|
+
function nowIso() {
|
|
74
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
75
|
+
}
|
|
76
|
+
function isQueueOwnerHeartbeatStale(owner) {
|
|
77
|
+
const heartbeatMs = Date.parse(owner.heartbeatAt);
|
|
78
|
+
if (!Number.isFinite(heartbeatMs)) return true;
|
|
79
|
+
return Date.now() - heartbeatMs > QUEUE_OWNER_STALE_HEARTBEAT_MS;
|
|
80
|
+
}
|
|
81
|
+
async function ensureQueueDir() {
|
|
82
|
+
const baseDir = queueBaseDir();
|
|
83
|
+
await fs$1.mkdir(baseDir, {
|
|
84
|
+
recursive: true,
|
|
85
|
+
mode: 448
|
|
86
|
+
});
|
|
87
|
+
await fs$1.chmod(baseDir, 448);
|
|
88
|
+
const socketDir = queueSocketBaseDir();
|
|
89
|
+
if (socketDir) {
|
|
90
|
+
await fs$1.mkdir(socketDir, {
|
|
91
|
+
recursive: true,
|
|
92
|
+
mode: 448
|
|
93
|
+
});
|
|
94
|
+
await fs$1.chmod(socketDir, 448);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function removeSocketFile(socketPath) {
|
|
98
|
+
if (process.platform === "win32") return;
|
|
99
|
+
try {
|
|
100
|
+
await fs$1.unlink(socketPath);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error.code !== "ENOENT") throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
106
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
107
|
+
while (Date.now() <= deadline) {
|
|
108
|
+
if (!isProcessAlive(pid)) return true;
|
|
109
|
+
await waitMs(PROCESS_POLL_MS);
|
|
110
|
+
}
|
|
111
|
+
return !isProcessAlive(pid);
|
|
112
|
+
}
|
|
113
|
+
async function cleanupStaleQueueOwner(sessionId, owner) {
|
|
114
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
115
|
+
await removeSocketFile(owner?.socketPath ?? queueSocketPath(sessionId)).catch(() => {});
|
|
116
|
+
await fs$1.unlink(lockPath).catch((error) => {
|
|
117
|
+
if (error.code !== "ENOENT") throw error;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function readQueueOwnerRecord(sessionId) {
|
|
121
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
122
|
+
try {
|
|
123
|
+
const payload = await fs$1.readFile(lockPath, "utf8");
|
|
124
|
+
return parseQueueOwnerRecord(JSON.parse(payload)) ?? void 0;
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isProcessAlive(pid) {
|
|
130
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
|
|
131
|
+
try {
|
|
132
|
+
process.kill(pid, 0);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function terminateProcess(pid) {
|
|
139
|
+
if (!isProcessAlive(pid)) return false;
|
|
140
|
+
try {
|
|
141
|
+
process.kill(pid, "SIGTERM");
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) return true;
|
|
146
|
+
try {
|
|
147
|
+
process.kill(pid, "SIGKILL");
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
async function ensureOwnerIsUsable(sessionId, owner) {
|
|
155
|
+
const alive = isProcessAlive(owner.pid);
|
|
156
|
+
const stale = isQueueOwnerHeartbeatStale(owner);
|
|
157
|
+
if (alive && !stale) return true;
|
|
158
|
+
if (alive) await terminateProcess(owner.pid).catch(() => {});
|
|
159
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
async function readQueueOwnerStatus(sessionId) {
|
|
163
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
164
|
+
if (!owner) return;
|
|
165
|
+
const alive = await ensureOwnerIsUsable(sessionId, owner);
|
|
166
|
+
if (!alive) return;
|
|
167
|
+
return {
|
|
168
|
+
pid: owner.pid,
|
|
169
|
+
socketPath: owner.socketPath,
|
|
170
|
+
heartbeatAt: owner.heartbeatAt,
|
|
171
|
+
ownerGeneration: owner.ownerGeneration,
|
|
172
|
+
queueDepth: owner.queueDepth,
|
|
173
|
+
alive,
|
|
174
|
+
stale: isQueueOwnerHeartbeatStale(owner)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function tryAcquireQueueOwnerLease(sessionId, nowIsoFactory = nowIso) {
|
|
178
|
+
await ensureQueueDir();
|
|
179
|
+
const lockPath = queueLockFilePath(sessionId);
|
|
180
|
+
const socketPath = queueSocketPath(sessionId);
|
|
181
|
+
const createdAt = nowIsoFactory();
|
|
182
|
+
const ownerGeneration = createOwnerGeneration();
|
|
183
|
+
const payload = JSON.stringify({
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
sessionId,
|
|
186
|
+
socketPath,
|
|
187
|
+
createdAt,
|
|
188
|
+
heartbeatAt: createdAt,
|
|
189
|
+
ownerGeneration,
|
|
190
|
+
queueDepth: 0
|
|
191
|
+
}, null, 2);
|
|
192
|
+
try {
|
|
193
|
+
await fs$1.writeFile(lockPath, `${payload}\n`, {
|
|
194
|
+
encoding: "utf8",
|
|
195
|
+
flag: "wx"
|
|
196
|
+
});
|
|
197
|
+
await removeSocketFile(socketPath).catch(() => {});
|
|
198
|
+
return {
|
|
199
|
+
sessionId,
|
|
200
|
+
lockPath,
|
|
201
|
+
socketPath,
|
|
202
|
+
createdAt,
|
|
203
|
+
ownerGeneration
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error.code !== "EEXIST") throw error;
|
|
207
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
208
|
+
if (!owner) {
|
|
209
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!isProcessAlive(owner.pid) || isQueueOwnerHeartbeatStale(owner)) {
|
|
213
|
+
if (isProcessAlive(owner.pid)) await terminateProcess(owner.pid).catch(() => {});
|
|
214
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
|
|
220
|
+
const payload = JSON.stringify({
|
|
221
|
+
pid: process.pid,
|
|
222
|
+
sessionId: lease.sessionId,
|
|
223
|
+
socketPath: lease.socketPath,
|
|
224
|
+
createdAt: lease.createdAt,
|
|
225
|
+
heartbeatAt: nowIsoFactory(),
|
|
226
|
+
ownerGeneration: lease.ownerGeneration,
|
|
227
|
+
queueDepth: Math.max(0, Math.round(options.queueDepth))
|
|
228
|
+
}, null, 2);
|
|
229
|
+
await fs$1.writeFile(lease.lockPath, `${payload}\n`, { encoding: "utf8" });
|
|
230
|
+
}
|
|
231
|
+
async function releaseQueueOwnerLease(lease) {
|
|
232
|
+
await removeSocketFile(lease.socketPath).catch(() => {});
|
|
233
|
+
await fs$1.unlink(lease.lockPath).catch((error) => {
|
|
234
|
+
if (error.code !== "ENOENT") throw error;
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function terminateQueueOwnerForSession(sessionId) {
|
|
238
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
239
|
+
if (!owner) return;
|
|
240
|
+
if (isProcessAlive(owner.pid)) await terminateProcess(owner.pid);
|
|
241
|
+
await cleanupStaleQueueOwner(sessionId, owner);
|
|
242
|
+
}
|
|
243
|
+
async function waitMs(ms) {
|
|
244
|
+
await new Promise((resolve) => {
|
|
245
|
+
setTimeout(resolve, ms);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/cli/queue/ipc-transport.ts
|
|
250
|
+
const QUEUE_CONNECT_ATTEMPTS = 40;
|
|
251
|
+
const SOCKET_CONNECTION_TIMEOUT_MS = 5e3;
|
|
252
|
+
function shouldRetryQueueConnect(error) {
|
|
253
|
+
const code = error.code;
|
|
254
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
255
|
+
}
|
|
256
|
+
async function connectToSocket(socketPath, timeoutMs = SOCKET_CONNECTION_TIMEOUT_MS) {
|
|
257
|
+
return await new Promise((resolve, reject) => {
|
|
258
|
+
const socket = net.createConnection(socketPath);
|
|
259
|
+
let settled = false;
|
|
260
|
+
const timeout = setTimeout(() => {
|
|
261
|
+
if (settled) return;
|
|
262
|
+
settled = true;
|
|
263
|
+
socket.destroy();
|
|
264
|
+
reject(/* @__PURE__ */ new Error(`Connection to ${socketPath} timed out after ${timeoutMs}ms`));
|
|
265
|
+
}, timeoutMs);
|
|
266
|
+
const onConnect = () => {
|
|
267
|
+
if (settled) return;
|
|
268
|
+
settled = true;
|
|
269
|
+
clearTimeout(timeout);
|
|
270
|
+
socket.off("error", onError);
|
|
271
|
+
resolve(socket);
|
|
272
|
+
};
|
|
273
|
+
const onError = (error) => {
|
|
274
|
+
if (settled) return;
|
|
275
|
+
settled = true;
|
|
276
|
+
clearTimeout(timeout);
|
|
277
|
+
socket.off("connect", onConnect);
|
|
278
|
+
reject(error);
|
|
279
|
+
};
|
|
280
|
+
socket.once("connect", onConnect);
|
|
281
|
+
socket.once("error", onError);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function connectToQueueOwner(owner, maxAttempts = QUEUE_CONNECT_ATTEMPTS) {
|
|
285
|
+
let lastError;
|
|
286
|
+
const attempts = Math.max(1, Math.trunc(maxAttempts));
|
|
287
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
288
|
+
return await measurePerf("queue.connect", async () => await connectToSocket(owner.socketPath));
|
|
289
|
+
} catch (error) {
|
|
290
|
+
lastError = error;
|
|
291
|
+
if (!shouldRetryQueueConnect(error)) throw error;
|
|
292
|
+
await waitMs(50);
|
|
293
|
+
}
|
|
294
|
+
if (lastError && !shouldRetryQueueConnect(lastError)) throw lastError;
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/cli/queue/ipc-health.ts
|
|
298
|
+
async function probeQueueOwnerHealth(sessionId) {
|
|
299
|
+
const ownerRecord = await readQueueOwnerRecord(sessionId);
|
|
300
|
+
if (!ownerRecord) return {
|
|
301
|
+
sessionId,
|
|
302
|
+
hasLease: false,
|
|
303
|
+
healthy: false,
|
|
304
|
+
socketReachable: false,
|
|
305
|
+
pidAlive: false
|
|
306
|
+
};
|
|
307
|
+
const owner = await readQueueOwnerStatus(sessionId);
|
|
308
|
+
if (!owner) return {
|
|
309
|
+
sessionId,
|
|
310
|
+
hasLease: false,
|
|
311
|
+
healthy: false,
|
|
312
|
+
socketReachable: false,
|
|
313
|
+
pidAlive: false
|
|
314
|
+
};
|
|
315
|
+
const pidAlive = owner.alive;
|
|
316
|
+
let socketReachable = false;
|
|
317
|
+
try {
|
|
318
|
+
const socket = await connectToQueueOwner(ownerRecord, 2);
|
|
319
|
+
if (socket) {
|
|
320
|
+
socketReachable = true;
|
|
321
|
+
if (!socket.destroyed) socket.end();
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
socketReachable = false;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
sessionId,
|
|
328
|
+
hasLease: true,
|
|
329
|
+
healthy: socketReachable,
|
|
330
|
+
socketReachable,
|
|
331
|
+
pidAlive,
|
|
332
|
+
pid: owner.pid,
|
|
333
|
+
socketPath: owner.socketPath,
|
|
334
|
+
ownerGeneration: owner.ownerGeneration,
|
|
335
|
+
queueDepth: owner.queueDepth
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/cli/queue/messages.ts
|
|
340
|
+
function asRecord$2(value) {
|
|
341
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
342
|
+
return value;
|
|
343
|
+
}
|
|
344
|
+
function isPermissionMode(value) {
|
|
345
|
+
return value === "approve-all" || value === "approve-reads" || value === "deny-all";
|
|
346
|
+
}
|
|
347
|
+
function isSessionResumePolicy(value) {
|
|
348
|
+
return value === "allow-new" || value === "same-session-only";
|
|
349
|
+
}
|
|
350
|
+
function isNonInteractivePermissionPolicy(value) {
|
|
351
|
+
return value === "deny" || value === "fail";
|
|
352
|
+
}
|
|
353
|
+
function isPermissionPolicy(value) {
|
|
354
|
+
if (value == null) return false;
|
|
355
|
+
if (typeof value !== "object" || Array.isArray(value)) return false;
|
|
356
|
+
const record = value;
|
|
357
|
+
for (const key of [
|
|
358
|
+
"autoApprove",
|
|
359
|
+
"autoDeny",
|
|
360
|
+
"escalate"
|
|
361
|
+
]) {
|
|
362
|
+
const entry = record[key];
|
|
363
|
+
if (entry == null) continue;
|
|
364
|
+
if (!Array.isArray(entry) || entry.some((item) => typeof item !== "string")) return false;
|
|
365
|
+
}
|
|
366
|
+
return record.defaultAction == null || record.defaultAction === "approve" || record.defaultAction === "deny" || record.defaultAction === "escalate";
|
|
367
|
+
}
|
|
368
|
+
function isOutputErrorCode(value) {
|
|
369
|
+
return typeof value === "string" && OUTPUT_ERROR_CODES.includes(value);
|
|
370
|
+
}
|
|
371
|
+
function isOutputErrorOrigin(value) {
|
|
372
|
+
return typeof value === "string" && OUTPUT_ERROR_ORIGINS.includes(value);
|
|
373
|
+
}
|
|
374
|
+
function isPermissionEscalationEvent(value) {
|
|
375
|
+
const event = asRecord$2(value);
|
|
376
|
+
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" && (event.toolName == null || typeof event.toolName === "string") && (event.toolKind == null || typeof event.toolKind === "string") && (event.matchedRule == null || typeof event.matchedRule === "string");
|
|
377
|
+
}
|
|
378
|
+
function parseSessionOptions(value) {
|
|
379
|
+
if (value == null) return;
|
|
380
|
+
const record = asRecord$2(value);
|
|
381
|
+
if (!record) return null;
|
|
382
|
+
const sessionOptions = {};
|
|
383
|
+
if (record.model != null) {
|
|
384
|
+
if (typeof record.model !== "string" || record.model.trim().length === 0) return null;
|
|
385
|
+
sessionOptions.model = record.model;
|
|
386
|
+
}
|
|
387
|
+
if (record.allowedTools != null) {
|
|
388
|
+
if (!Array.isArray(record.allowedTools)) return null;
|
|
389
|
+
const allowedTools = record.allowedTools.filter((tool) => typeof tool === "string");
|
|
390
|
+
if (allowedTools.length !== record.allowedTools.length) return null;
|
|
391
|
+
sessionOptions.allowedTools = allowedTools;
|
|
392
|
+
}
|
|
393
|
+
if (record.maxTurns != null) {
|
|
394
|
+
if (typeof record.maxTurns !== "number" || !Number.isFinite(record.maxTurns)) return null;
|
|
395
|
+
sessionOptions.maxTurns = Math.max(1, Math.round(record.maxTurns));
|
|
396
|
+
}
|
|
397
|
+
if (record.systemPrompt != null) if (typeof record.systemPrompt === "string") sessionOptions.systemPrompt = record.systemPrompt;
|
|
398
|
+
else {
|
|
399
|
+
const systemPrompt = asRecord$2(record.systemPrompt);
|
|
400
|
+
if (!systemPrompt || typeof systemPrompt.append !== "string") return null;
|
|
401
|
+
sessionOptions.systemPrompt = { append: systemPrompt.append };
|
|
402
|
+
}
|
|
403
|
+
return sessionOptions;
|
|
404
|
+
}
|
|
405
|
+
function parseOwnerGeneration(value) {
|
|
406
|
+
if (value == null) return;
|
|
407
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return null;
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
function parseNonNegativeInteger(value) {
|
|
411
|
+
if (value == null) return;
|
|
412
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
|
|
413
|
+
return Math.round(value);
|
|
414
|
+
}
|
|
415
|
+
function parseQueueRequest(raw) {
|
|
416
|
+
const request = asRecord$2(raw);
|
|
417
|
+
if (!request) return null;
|
|
418
|
+
if (typeof request.type !== "string" || typeof request.requestId !== "string") return null;
|
|
419
|
+
const ownerGeneration = parseOwnerGeneration(request.ownerGeneration);
|
|
420
|
+
if (ownerGeneration === null) return null;
|
|
421
|
+
const timeoutRaw = request.timeoutMs;
|
|
422
|
+
const timeoutMs = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? Math.round(timeoutRaw) : void 0;
|
|
423
|
+
if (request.type === "submit_prompt") {
|
|
424
|
+
const resumePolicy = request.resumePolicy == null ? void 0 : isSessionResumePolicy(request.resumePolicy) ? request.resumePolicy : null;
|
|
425
|
+
const nonInteractivePermissions = request.nonInteractivePermissions == null ? void 0 : isNonInteractivePermissionPolicy(request.nonInteractivePermissions) ? request.nonInteractivePermissions : null;
|
|
426
|
+
const permissionPolicy = request.permissionPolicy == null ? void 0 : isPermissionPolicy(request.permissionPolicy) ? request.permissionPolicy : null;
|
|
427
|
+
const suppressSdkConsoleErrors = request.suppressSdkConsoleErrors == null ? void 0 : typeof request.suppressSdkConsoleErrors === "boolean" ? request.suppressSdkConsoleErrors : null;
|
|
428
|
+
const promptRetries = parseNonNegativeInteger(request.promptRetries);
|
|
429
|
+
const sessionOptions = parseSessionOptions(request.sessionOptions);
|
|
430
|
+
const prompt = request.prompt == null ? void 0 : isPromptInput(request.prompt) ? request.prompt : null;
|
|
431
|
+
if (typeof request.message !== "string" || !isPermissionMode(request.permissionMode) || resumePolicy === null || prompt === null || nonInteractivePermissions === null || permissionPolicy === null || suppressSdkConsoleErrors === null || promptRetries === null || sessionOptions === null || typeof request.waitForCompletion !== "boolean") return null;
|
|
432
|
+
return {
|
|
433
|
+
type: "submit_prompt",
|
|
434
|
+
requestId: request.requestId,
|
|
435
|
+
ownerGeneration,
|
|
436
|
+
message: request.message,
|
|
437
|
+
prompt: prompt ?? textPrompt(request.message),
|
|
438
|
+
permissionMode: request.permissionMode,
|
|
439
|
+
...resumePolicy !== void 0 ? { resumePolicy } : {},
|
|
440
|
+
nonInteractivePermissions,
|
|
441
|
+
...permissionPolicy !== void 0 ? { permissionPolicy } : {},
|
|
442
|
+
timeoutMs,
|
|
443
|
+
...suppressSdkConsoleErrors !== void 0 ? { suppressSdkConsoleErrors } : {},
|
|
444
|
+
...promptRetries !== void 0 ? { promptRetries } : {},
|
|
445
|
+
waitForCompletion: request.waitForCompletion,
|
|
446
|
+
...sessionOptions !== void 0 ? { sessionOptions } : {}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (request.type === "cancel_prompt") return {
|
|
450
|
+
type: "cancel_prompt",
|
|
451
|
+
requestId: request.requestId,
|
|
452
|
+
ownerGeneration
|
|
453
|
+
};
|
|
454
|
+
if (request.type === "close_session") return {
|
|
455
|
+
type: "close_session",
|
|
456
|
+
requestId: request.requestId,
|
|
457
|
+
ownerGeneration,
|
|
458
|
+
timeoutMs
|
|
459
|
+
};
|
|
460
|
+
if (request.type === "set_mode") {
|
|
461
|
+
if (typeof request.modeId !== "string" || request.modeId.trim().length === 0) return null;
|
|
462
|
+
return {
|
|
463
|
+
type: "set_mode",
|
|
464
|
+
requestId: request.requestId,
|
|
465
|
+
ownerGeneration,
|
|
466
|
+
modeId: request.modeId,
|
|
467
|
+
timeoutMs
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (request.type === "set_model") {
|
|
471
|
+
if (typeof request.modelId !== "string" || request.modelId.trim().length === 0) return null;
|
|
472
|
+
return {
|
|
473
|
+
type: "set_model",
|
|
474
|
+
requestId: request.requestId,
|
|
475
|
+
ownerGeneration,
|
|
476
|
+
modelId: request.modelId,
|
|
477
|
+
timeoutMs
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (request.type === "set_config_option") {
|
|
481
|
+
if (typeof request.configId !== "string" || request.configId.trim().length === 0 || typeof request.value !== "string" || request.value.trim().length === 0) return null;
|
|
482
|
+
return {
|
|
483
|
+
type: "set_config_option",
|
|
484
|
+
requestId: request.requestId,
|
|
485
|
+
ownerGeneration,
|
|
486
|
+
configId: request.configId,
|
|
487
|
+
value: request.value,
|
|
488
|
+
timeoutMs
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
function parseSessionSendResult(raw) {
|
|
494
|
+
const result = asRecord$2(raw);
|
|
495
|
+
if (!result) return null;
|
|
496
|
+
if (typeof result.stopReason !== "string" || typeof result.sessionId !== "string" || typeof result.resumed !== "boolean") return null;
|
|
497
|
+
const permissionStats = asRecord$2(result.permissionStats);
|
|
498
|
+
const record = asRecord$2(result.record);
|
|
499
|
+
if (!permissionStats || !record) return null;
|
|
500
|
+
if (!(typeof permissionStats.requested === "number" && typeof permissionStats.approved === "number" && typeof permissionStats.denied === "number" && typeof permissionStats.cancelled === "number")) return null;
|
|
501
|
+
if (!(typeof record.acpxRecordId === "string" && typeof record.acpSessionId === "string" && typeof record.agentCommand === "string" && typeof record.cwd === "string" && typeof record.createdAt === "string" && typeof record.lastUsedAt === "string" && Array.isArray(record.messages) && typeof record.updated_at === "string" && typeof record.lastSeq === "number" && Number.isInteger(record.lastSeq) && !!record.eventLog && typeof record.eventLog === "object")) return null;
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
function parseQueueOwnerMessage(raw) {
|
|
505
|
+
const message = asRecord$2(raw);
|
|
506
|
+
if (!message || typeof message.type !== "string") return null;
|
|
507
|
+
if (typeof message.requestId !== "string") return null;
|
|
508
|
+
const ownerGeneration = parseOwnerGeneration(message.ownerGeneration);
|
|
509
|
+
if (ownerGeneration === null) return null;
|
|
510
|
+
if (message.type === "accepted") return {
|
|
511
|
+
type: "accepted",
|
|
512
|
+
requestId: message.requestId,
|
|
513
|
+
ownerGeneration
|
|
514
|
+
};
|
|
515
|
+
if (message.type === "event") {
|
|
516
|
+
if (!isAcpJsonRpcMessage(message.message)) return null;
|
|
517
|
+
return {
|
|
518
|
+
type: "event",
|
|
519
|
+
requestId: message.requestId,
|
|
520
|
+
ownerGeneration,
|
|
521
|
+
message: message.message
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
if (message.type === "permission_escalation") {
|
|
525
|
+
if (!isPermissionEscalationEvent(message.event)) return null;
|
|
526
|
+
return {
|
|
527
|
+
type: "permission_escalation",
|
|
528
|
+
requestId: message.requestId,
|
|
529
|
+
ownerGeneration,
|
|
530
|
+
event: message.event
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (message.type === "result") {
|
|
534
|
+
const parsedResult = parseSessionSendResult(message.result);
|
|
535
|
+
if (!parsedResult) return null;
|
|
536
|
+
return {
|
|
537
|
+
type: "result",
|
|
538
|
+
requestId: message.requestId,
|
|
539
|
+
ownerGeneration,
|
|
540
|
+
result: parsedResult
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (message.type === "cancel_result") {
|
|
544
|
+
if (typeof message.cancelled !== "boolean") return null;
|
|
545
|
+
return {
|
|
546
|
+
type: "cancel_result",
|
|
547
|
+
requestId: message.requestId,
|
|
548
|
+
ownerGeneration,
|
|
549
|
+
cancelled: message.cancelled
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
if (message.type === "set_mode_result") {
|
|
553
|
+
if (typeof message.modeId !== "string") return null;
|
|
554
|
+
return {
|
|
555
|
+
type: "set_mode_result",
|
|
556
|
+
requestId: message.requestId,
|
|
557
|
+
ownerGeneration,
|
|
558
|
+
modeId: message.modeId
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (message.type === "set_model_result") {
|
|
562
|
+
if (typeof message.modelId !== "string") return null;
|
|
563
|
+
return {
|
|
564
|
+
type: "set_model_result",
|
|
565
|
+
requestId: message.requestId,
|
|
566
|
+
ownerGeneration,
|
|
567
|
+
modelId: message.modelId
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (message.type === "set_config_option_result") {
|
|
571
|
+
const response = asRecord$2(message.response);
|
|
572
|
+
if (!response || !Array.isArray(response.configOptions)) return null;
|
|
573
|
+
return {
|
|
574
|
+
type: "set_config_option_result",
|
|
575
|
+
requestId: message.requestId,
|
|
576
|
+
ownerGeneration,
|
|
577
|
+
response
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (message.type === "error") {
|
|
581
|
+
if (typeof message.message !== "string" || !isOutputErrorCode(message.code) || !isOutputErrorOrigin(message.origin)) return null;
|
|
582
|
+
const detailCode = typeof message.detailCode === "string" && message.detailCode.trim().length > 0 ? message.detailCode : void 0;
|
|
583
|
+
const retryable = typeof message.retryable === "boolean" ? message.retryable : void 0;
|
|
584
|
+
const acp = toAcpErrorPayload(message.acp);
|
|
585
|
+
const outputAlreadyEmitted = typeof message.outputAlreadyEmitted === "boolean" ? message.outputAlreadyEmitted : void 0;
|
|
586
|
+
return {
|
|
587
|
+
type: "error",
|
|
588
|
+
requestId: message.requestId,
|
|
589
|
+
ownerGeneration,
|
|
590
|
+
code: message.code,
|
|
591
|
+
detailCode,
|
|
592
|
+
origin: message.origin,
|
|
593
|
+
message: message.message,
|
|
594
|
+
retryable,
|
|
595
|
+
acp,
|
|
596
|
+
...outputAlreadyEmitted === void 0 ? {} : { outputAlreadyEmitted }
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/cli/queue/ipc-server.ts
|
|
603
|
+
function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
|
|
604
|
+
return {
|
|
605
|
+
type: "error",
|
|
606
|
+
requestId,
|
|
607
|
+
ownerGeneration: void 0,
|
|
608
|
+
code: "RUNTIME",
|
|
609
|
+
detailCode,
|
|
610
|
+
origin: "queue",
|
|
611
|
+
retryable: options.retryable,
|
|
612
|
+
message
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options = {}) {
|
|
616
|
+
const normalized = normalizeOutputError(error, {
|
|
617
|
+
defaultCode: "RUNTIME",
|
|
618
|
+
origin: "queue",
|
|
619
|
+
detailCode,
|
|
620
|
+
retryable: options.retryable
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
type: "error",
|
|
624
|
+
requestId,
|
|
625
|
+
code: normalized.code,
|
|
626
|
+
detailCode: normalized.detailCode,
|
|
627
|
+
origin: normalized.origin,
|
|
628
|
+
message: normalized.message,
|
|
629
|
+
retryable: normalized.retryable,
|
|
630
|
+
acp: normalized.acp
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function writeQueueMessage(socket, message) {
|
|
634
|
+
if (socket.destroyed || !socket.writable) return;
|
|
635
|
+
socket.write(`${JSON.stringify(message)}\n`);
|
|
636
|
+
}
|
|
637
|
+
var SessionQueueOwner = class SessionQueueOwner {
|
|
638
|
+
server;
|
|
639
|
+
controlHandlers;
|
|
640
|
+
ownerGeneration;
|
|
641
|
+
maxQueueDepth;
|
|
642
|
+
onQueueDepthChanged;
|
|
643
|
+
pending = [];
|
|
644
|
+
waiters = [];
|
|
645
|
+
closed = false;
|
|
646
|
+
constructor(server, controlHandlers, lease, options) {
|
|
647
|
+
this.server = server;
|
|
648
|
+
this.controlHandlers = controlHandlers;
|
|
649
|
+
this.ownerGeneration = lease.ownerGeneration;
|
|
650
|
+
this.maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth));
|
|
651
|
+
this.onQueueDepthChanged = options.onQueueDepthChanged;
|
|
652
|
+
}
|
|
653
|
+
static async start(lease, controlHandlers, options = { maxQueueDepth: 16 }) {
|
|
654
|
+
const ownerRef = { current: void 0 };
|
|
655
|
+
const server = net.createServer((socket) => {
|
|
656
|
+
ownerRef.current?.handleConnection(socket);
|
|
657
|
+
});
|
|
658
|
+
ownerRef.current = new SessionQueueOwner(server, controlHandlers, lease, options);
|
|
659
|
+
await new Promise((resolve, reject) => {
|
|
660
|
+
const onListening = () => {
|
|
661
|
+
server.off("error", onError);
|
|
662
|
+
resolve();
|
|
663
|
+
};
|
|
664
|
+
const onError = (error) => {
|
|
665
|
+
server.off("listening", onListening);
|
|
666
|
+
reject(error);
|
|
667
|
+
};
|
|
668
|
+
server.once("listening", onListening);
|
|
669
|
+
server.once("error", onError);
|
|
670
|
+
server.listen(lease.socketPath);
|
|
671
|
+
});
|
|
672
|
+
return ownerRef.current;
|
|
673
|
+
}
|
|
674
|
+
async close() {
|
|
675
|
+
if (this.closed) return;
|
|
676
|
+
this.closed = true;
|
|
677
|
+
for (const waiter of this.waiters.splice(0)) waiter(void 0);
|
|
678
|
+
for (const task of this.pending.splice(0)) {
|
|
679
|
+
if (task.waitForCompletion) task.send(makeQueueOwnerError(task.requestId, "Queue owner shutting down before prompt execution", "QUEUE_OWNER_SHUTTING_DOWN", { retryable: true }));
|
|
680
|
+
task.close();
|
|
681
|
+
}
|
|
682
|
+
this.emitQueueDepth();
|
|
683
|
+
await new Promise((resolve) => {
|
|
684
|
+
this.server.close(() => resolve());
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
async nextTask(timeoutMs) {
|
|
688
|
+
if (this.pending.length > 0) {
|
|
689
|
+
const task = this.pending.shift();
|
|
690
|
+
this.emitQueueDepth();
|
|
691
|
+
if (task) recordPerfDuration("queue.owner.wait_ms", Date.now() - task.enqueuedAt);
|
|
692
|
+
return task;
|
|
693
|
+
}
|
|
694
|
+
if (this.closed) return;
|
|
695
|
+
return await new Promise((resolve) => {
|
|
696
|
+
const timer = timeoutMs != null && setTimeout(() => {
|
|
697
|
+
const index = this.waiters.indexOf(waiter);
|
|
698
|
+
if (index >= 0) this.waiters.splice(index, 1);
|
|
699
|
+
resolve(void 0);
|
|
700
|
+
}, Math.max(0, timeoutMs));
|
|
701
|
+
const waiter = (task) => {
|
|
702
|
+
if (timer) clearTimeout(timer);
|
|
703
|
+
resolve(task);
|
|
704
|
+
};
|
|
705
|
+
this.waiters.push(waiter);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
queueDepth() {
|
|
709
|
+
return this.pending.length;
|
|
710
|
+
}
|
|
711
|
+
emitQueueDepth() {
|
|
712
|
+
this.onQueueDepthChanged?.(this.pending.length);
|
|
713
|
+
}
|
|
714
|
+
enqueue(task) {
|
|
715
|
+
if (this.closed) {
|
|
716
|
+
if (task.waitForCompletion) task.send(makeQueueOwnerError(task.requestId, "Queue owner is shutting down", "QUEUE_OWNER_SHUTTING_DOWN", { retryable: true }));
|
|
717
|
+
task.close();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const waiter = this.waiters.shift();
|
|
721
|
+
if (waiter) {
|
|
722
|
+
waiter(task);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (this.pending.length >= this.maxQueueDepth) {
|
|
726
|
+
if (task.waitForCompletion) task.send({
|
|
727
|
+
...makeQueueOwnerError(task.requestId, `Queue owner is overloaded (${this.pending.length}/${this.maxQueueDepth} queued)`, "QUEUE_OWNER_OVERLOADED", { retryable: true }),
|
|
728
|
+
ownerGeneration: this.ownerGeneration
|
|
729
|
+
});
|
|
730
|
+
task.close();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
this.pending.push(task);
|
|
734
|
+
this.emitQueueDepth();
|
|
735
|
+
}
|
|
736
|
+
handleControlRequest(options) {
|
|
737
|
+
writeQueueMessage(options.socket, {
|
|
738
|
+
type: "accepted",
|
|
739
|
+
requestId: options.requestId,
|
|
740
|
+
ownerGeneration: this.ownerGeneration
|
|
741
|
+
});
|
|
742
|
+
options.run().then((message) => {
|
|
743
|
+
writeQueueMessage(options.socket, {
|
|
744
|
+
...message,
|
|
745
|
+
ownerGeneration: this.ownerGeneration
|
|
746
|
+
});
|
|
747
|
+
}).catch((error) => {
|
|
748
|
+
writeQueueMessage(options.socket, {
|
|
749
|
+
...makeQueueOwnerErrorFromUnknown(options.requestId, error, "QUEUE_CONTROL_REQUEST_FAILED"),
|
|
750
|
+
ownerGeneration: this.ownerGeneration
|
|
751
|
+
});
|
|
752
|
+
}).finally(() => {
|
|
753
|
+
if (!options.socket.destroyed) options.socket.end();
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
handleConnection(socket) {
|
|
757
|
+
socket.setEncoding("utf8");
|
|
758
|
+
if (this.closed) {
|
|
759
|
+
writeQueueMessage(socket, makeQueueOwnerError("unknown", "Queue owner is closed", "QUEUE_OWNER_CLOSED", { retryable: true }));
|
|
760
|
+
socket.end();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
let buffer = "";
|
|
764
|
+
let handled = false;
|
|
765
|
+
const fail = (requestId, message, detailCode) => {
|
|
766
|
+
writeQueueMessage(socket, {
|
|
767
|
+
...makeQueueOwnerError(requestId, message, detailCode, { retryable: false }),
|
|
768
|
+
ownerGeneration: this.ownerGeneration
|
|
769
|
+
});
|
|
770
|
+
socket.end();
|
|
771
|
+
};
|
|
772
|
+
const processLine = (line) => {
|
|
773
|
+
if (handled) return;
|
|
774
|
+
handled = true;
|
|
775
|
+
let parsed;
|
|
776
|
+
try {
|
|
777
|
+
parsed = JSON.parse(line);
|
|
778
|
+
} catch {
|
|
779
|
+
fail("unknown", "Invalid queue request payload", "QUEUE_REQUEST_PAYLOAD_INVALID_JSON");
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const request = parseQueueRequest(parsed);
|
|
783
|
+
if (!request) {
|
|
784
|
+
fail("unknown", "Invalid queue request", "QUEUE_REQUEST_INVALID");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (request.ownerGeneration !== void 0 && this.ownerGeneration !== void 0 && request.ownerGeneration !== this.ownerGeneration) {
|
|
788
|
+
fail(request.requestId, "Queue request targeted a stale queue owner generation", "QUEUE_OWNER_GENERATION_MISMATCH");
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (request.type === "cancel_prompt") {
|
|
792
|
+
this.handleControlRequest({
|
|
793
|
+
socket,
|
|
794
|
+
requestId: request.requestId,
|
|
795
|
+
run: async () => ({
|
|
796
|
+
type: "cancel_result",
|
|
797
|
+
requestId: request.requestId,
|
|
798
|
+
cancelled: await this.controlHandlers.cancelPrompt()
|
|
799
|
+
})
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (request.type === "close_session") {
|
|
804
|
+
this.handleControlRequest({
|
|
805
|
+
socket,
|
|
806
|
+
requestId: request.requestId,
|
|
807
|
+
run: async () => ({
|
|
808
|
+
type: "close_session_result",
|
|
809
|
+
requestId: request.requestId,
|
|
810
|
+
closed: await this.controlHandlers.closeSession(request.timeoutMs)
|
|
811
|
+
})
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (request.type === "set_mode") {
|
|
816
|
+
this.handleControlRequest({
|
|
817
|
+
socket,
|
|
818
|
+
requestId: request.requestId,
|
|
819
|
+
run: async () => {
|
|
820
|
+
await this.controlHandlers.setSessionMode(request.modeId, request.timeoutMs);
|
|
821
|
+
return {
|
|
822
|
+
type: "set_mode_result",
|
|
823
|
+
requestId: request.requestId,
|
|
824
|
+
modeId: request.modeId
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (request.type === "set_model") {
|
|
831
|
+
this.handleControlRequest({
|
|
832
|
+
socket,
|
|
833
|
+
requestId: request.requestId,
|
|
834
|
+
run: async () => {
|
|
835
|
+
await this.controlHandlers.setSessionModel(request.modelId, request.timeoutMs);
|
|
836
|
+
return {
|
|
837
|
+
type: "set_model_result",
|
|
838
|
+
requestId: request.requestId,
|
|
839
|
+
modelId: request.modelId
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (request.type === "set_config_option") {
|
|
846
|
+
this.handleControlRequest({
|
|
847
|
+
socket,
|
|
848
|
+
requestId: request.requestId,
|
|
849
|
+
run: async () => ({
|
|
850
|
+
type: "set_config_option_result",
|
|
851
|
+
requestId: request.requestId,
|
|
852
|
+
response: await this.controlHandlers.setSessionConfigOption(request.configId, request.value, request.timeoutMs)
|
|
853
|
+
})
|
|
854
|
+
});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const task = {
|
|
858
|
+
requestId: request.requestId,
|
|
859
|
+
message: request.message,
|
|
860
|
+
prompt: request.prompt ?? textPrompt(request.message),
|
|
861
|
+
permissionMode: request.permissionMode,
|
|
862
|
+
resumePolicy: request.resumePolicy,
|
|
863
|
+
nonInteractivePermissions: request.nonInteractivePermissions,
|
|
864
|
+
permissionPolicy: request.permissionPolicy,
|
|
865
|
+
timeoutMs: request.timeoutMs,
|
|
866
|
+
suppressSdkConsoleErrors: request.suppressSdkConsoleErrors,
|
|
867
|
+
promptRetries: request.promptRetries,
|
|
868
|
+
sessionOptions: request.sessionOptions,
|
|
869
|
+
waitForCompletion: request.waitForCompletion,
|
|
870
|
+
enqueuedAt: Date.now(),
|
|
871
|
+
send: (message) => {
|
|
872
|
+
writeQueueMessage(socket, {
|
|
873
|
+
...message,
|
|
874
|
+
ownerGeneration: this.ownerGeneration
|
|
875
|
+
});
|
|
876
|
+
},
|
|
877
|
+
close: () => {
|
|
878
|
+
if (!socket.destroyed) socket.end();
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
writeQueueMessage(socket, {
|
|
882
|
+
type: "accepted",
|
|
883
|
+
requestId: request.requestId,
|
|
884
|
+
ownerGeneration: this.ownerGeneration
|
|
885
|
+
});
|
|
886
|
+
if (!request.waitForCompletion) task.close();
|
|
887
|
+
this.enqueue(task);
|
|
888
|
+
};
|
|
889
|
+
socket.on("data", (chunk) => {
|
|
890
|
+
buffer += chunk;
|
|
891
|
+
let index = buffer.indexOf("\n");
|
|
892
|
+
while (index >= 0) {
|
|
893
|
+
const line = buffer.slice(0, index).trim();
|
|
894
|
+
buffer = buffer.slice(index + 1);
|
|
895
|
+
if (line.length > 0) processLine(line);
|
|
896
|
+
index = buffer.indexOf("\n");
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
socket.on("error", () => {});
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
//#endregion
|
|
903
|
+
//#region src/cli/queue/ipc.ts
|
|
904
|
+
const MAX_MESSAGE_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
905
|
+
const STALE_OWNER_PROTOCOL_DETAIL_CODES = new Set(["QUEUE_PROTOCOL_MALFORMED_MESSAGE", "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE"]);
|
|
906
|
+
async function maybeRecoverStaleOwnerAfterProtocolMismatch(params) {
|
|
907
|
+
if (!(params.error instanceof QueueProtocolError)) return false;
|
|
908
|
+
const detailCode = params.error.detailCode;
|
|
909
|
+
if (!detailCode || !STALE_OWNER_PROTOCOL_DETAIL_CODES.has(detailCode)) return false;
|
|
910
|
+
await terminateQueueOwnerForSession(params.sessionId).catch(() => {});
|
|
911
|
+
incrementPerfCounter("queue.owner.stale_recovered");
|
|
912
|
+
if (params.verbose) process.stderr.write(`[acpx] dropped stale queue owner metadata after protocol mismatch for session ${params.sessionId} (${detailCode})\n`);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
function assertOwnerGeneration(owner, message) {
|
|
916
|
+
if (owner.ownerGeneration !== void 0 && message.ownerGeneration !== void 0 && message.ownerGeneration !== owner.ownerGeneration) throw new QueueProtocolError("Queue owner returned mismatched generation", {
|
|
917
|
+
detailCode: "QUEUE_OWNER_GENERATION_MISMATCH",
|
|
918
|
+
origin: "queue",
|
|
919
|
+
retryable: true
|
|
920
|
+
});
|
|
921
|
+
return message;
|
|
922
|
+
}
|
|
923
|
+
function makeMalformedQueueMessageError() {
|
|
924
|
+
return new QueueProtocolError("Queue owner sent malformed message", {
|
|
925
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
926
|
+
origin: "queue",
|
|
927
|
+
retryable: true
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
function parseQueueOwnerResponseLine(owner, requestId, line) {
|
|
931
|
+
let parsed;
|
|
932
|
+
try {
|
|
933
|
+
parsed = JSON.parse(line);
|
|
934
|
+
} catch {
|
|
935
|
+
throw new QueueProtocolError("Queue owner sent invalid JSON payload", {
|
|
936
|
+
detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
|
|
937
|
+
origin: "queue",
|
|
938
|
+
retryable: true
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
const parsedMessage = parseQueueOwnerMessage(parsed);
|
|
942
|
+
if (!parsedMessage) throw makeMalformedQueueMessageError();
|
|
943
|
+
const message = assertOwnerGeneration(owner, parsedMessage);
|
|
944
|
+
if (message.requestId !== requestId) throw makeMalformedQueueMessageError();
|
|
945
|
+
return message;
|
|
946
|
+
}
|
|
947
|
+
async function runQueueOwnerRequest(options) {
|
|
948
|
+
const socket = await connectToQueueOwner(options.owner);
|
|
949
|
+
if (!socket) return;
|
|
950
|
+
socket.setEncoding("utf8");
|
|
951
|
+
return await new Promise((resolve, reject) => {
|
|
952
|
+
let settled = false;
|
|
953
|
+
let buffer = "";
|
|
954
|
+
const state = { acknowledged: false };
|
|
955
|
+
const finishResolve = (result) => {
|
|
956
|
+
if (settled) return;
|
|
957
|
+
settled = true;
|
|
958
|
+
socket.removeAllListeners();
|
|
959
|
+
if (!socket.destroyed) socket.end();
|
|
960
|
+
resolve(result);
|
|
961
|
+
};
|
|
962
|
+
const finishReject = (error) => {
|
|
963
|
+
if (settled) return;
|
|
964
|
+
settled = true;
|
|
965
|
+
socket.removeAllListeners();
|
|
966
|
+
if (!socket.destroyed) socket.destroy();
|
|
967
|
+
reject(error);
|
|
968
|
+
};
|
|
969
|
+
const controls = {
|
|
970
|
+
state,
|
|
971
|
+
resolve: finishResolve,
|
|
972
|
+
reject: finishReject
|
|
973
|
+
};
|
|
974
|
+
const processLine = (line) => {
|
|
975
|
+
let message;
|
|
976
|
+
try {
|
|
977
|
+
message = parseQueueOwnerResponseLine(options.owner, options.request.requestId, line);
|
|
978
|
+
} catch (error) {
|
|
979
|
+
finishReject(error);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (message.type === "accepted") {
|
|
983
|
+
state.acknowledged = true;
|
|
984
|
+
options.onAccepted?.(controls);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
options.onMessage(message, controls);
|
|
988
|
+
};
|
|
989
|
+
socket.on("data", (chunk) => {
|
|
990
|
+
buffer += chunk;
|
|
991
|
+
if (buffer.length > 10485760) {
|
|
992
|
+
socket.destroy();
|
|
993
|
+
finishReject(/* @__PURE__ */ new Error(`Message buffer exceeded ${MAX_MESSAGE_BUFFER_SIZE} bytes`));
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
let index = buffer.indexOf("\n");
|
|
997
|
+
while (index >= 0) {
|
|
998
|
+
const line = buffer.slice(0, index).trim();
|
|
999
|
+
buffer = buffer.slice(index + 1);
|
|
1000
|
+
if (line.length > 0) processLine(line);
|
|
1001
|
+
index = buffer.indexOf("\n");
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
socket.once("error", (error) => {
|
|
1005
|
+
finishReject(error);
|
|
1006
|
+
});
|
|
1007
|
+
socket.once("close", () => {
|
|
1008
|
+
if (settled) return;
|
|
1009
|
+
options.onClose(controls);
|
|
1010
|
+
});
|
|
1011
|
+
socket.write(`${JSON.stringify(options.request)}\n`);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
async function submitToQueueOwner(owner, options) {
|
|
1015
|
+
const requestId = randomUUID();
|
|
1016
|
+
const request = {
|
|
1017
|
+
type: "submit_prompt",
|
|
1018
|
+
requestId,
|
|
1019
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1020
|
+
message: options.message,
|
|
1021
|
+
prompt: options.prompt,
|
|
1022
|
+
permissionMode: options.permissionMode,
|
|
1023
|
+
resumePolicy: options.resumePolicy,
|
|
1024
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1025
|
+
permissionPolicy: options.permissionPolicy,
|
|
1026
|
+
timeoutMs: options.timeoutMs,
|
|
1027
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1028
|
+
promptRetries: options.promptRetries ?? 0,
|
|
1029
|
+
waitForCompletion: options.waitForCompletion,
|
|
1030
|
+
sessionOptions: options.sessionOptions
|
|
1031
|
+
};
|
|
1032
|
+
options.outputFormatter.setContext({ sessionId: options.sessionId });
|
|
1033
|
+
return await runQueueOwnerRequest({
|
|
1034
|
+
owner,
|
|
1035
|
+
request,
|
|
1036
|
+
onAccepted: ({ resolve }) => {
|
|
1037
|
+
options.outputFormatter.setContext({ sessionId: options.sessionId });
|
|
1038
|
+
if (!options.waitForCompletion) resolve({
|
|
1039
|
+
queued: true,
|
|
1040
|
+
sessionId: options.sessionId,
|
|
1041
|
+
requestId
|
|
1042
|
+
});
|
|
1043
|
+
},
|
|
1044
|
+
onMessage: (message, { state, resolve, reject }) => {
|
|
1045
|
+
if (message.type === "error") {
|
|
1046
|
+
options.outputFormatter.setContext({ sessionId: options.sessionId });
|
|
1047
|
+
const queueErrorAlreadyEmitted = options.errorEmissionPolicy?.queueErrorAlreadyEmitted ?? true;
|
|
1048
|
+
if (!(message.outputAlreadyEmitted === true) || !queueErrorAlreadyEmitted) {
|
|
1049
|
+
options.outputFormatter.onError({
|
|
1050
|
+
code: message.code ?? "RUNTIME",
|
|
1051
|
+
detailCode: message.detailCode,
|
|
1052
|
+
origin: message.origin ?? "queue",
|
|
1053
|
+
message: message.message,
|
|
1054
|
+
retryable: message.retryable,
|
|
1055
|
+
acp: message.acp
|
|
1056
|
+
});
|
|
1057
|
+
options.outputFormatter.flush();
|
|
1058
|
+
}
|
|
1059
|
+
reject(new QueueConnectionError(message.message, {
|
|
1060
|
+
outputCode: message.code,
|
|
1061
|
+
detailCode: message.detailCode,
|
|
1062
|
+
origin: message.origin ?? "queue",
|
|
1063
|
+
retryable: message.retryable,
|
|
1064
|
+
acp: message.acp,
|
|
1065
|
+
...queueErrorAlreadyEmitted ? { outputAlreadyEmitted: true } : {}
|
|
1066
|
+
}));
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (!state.acknowledged) {
|
|
1070
|
+
reject(new QueueConnectionError("Queue owner did not acknowledge request", {
|
|
1071
|
+
detailCode: "QUEUE_ACK_MISSING",
|
|
1072
|
+
origin: "queue",
|
|
1073
|
+
retryable: true
|
|
1074
|
+
}));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (message.type === "event") {
|
|
1078
|
+
options.outputFormatter.onAcpMessage(message.message);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (message.type === "permission_escalation") {
|
|
1082
|
+
options.outputFormatter.onPermissionEscalation(message.event);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (message.type === "result") {
|
|
1086
|
+
options.outputFormatter.flush();
|
|
1087
|
+
resolve(message.result);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
reject(new QueueProtocolError("Queue owner returned unexpected response", {
|
|
1091
|
+
detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
|
|
1092
|
+
origin: "queue",
|
|
1093
|
+
retryable: true
|
|
1094
|
+
}));
|
|
1095
|
+
},
|
|
1096
|
+
onClose: ({ state, resolve, reject }) => {
|
|
1097
|
+
if (!state.acknowledged) {
|
|
1098
|
+
reject(new QueueConnectionError("Queue owner disconnected before acknowledging request", {
|
|
1099
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
|
|
1100
|
+
origin: "queue",
|
|
1101
|
+
retryable: true
|
|
1102
|
+
}));
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (!options.waitForCompletion) {
|
|
1106
|
+
resolve({
|
|
1107
|
+
queued: true,
|
|
1108
|
+
sessionId: options.sessionId,
|
|
1109
|
+
requestId
|
|
1110
|
+
});
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
reject(new QueueConnectionError("Queue owner disconnected before prompt completion", {
|
|
1114
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
|
|
1115
|
+
origin: "queue",
|
|
1116
|
+
retryable: true
|
|
1117
|
+
}));
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
|
|
1122
|
+
return await runQueueOwnerRequest({
|
|
1123
|
+
owner,
|
|
1124
|
+
request,
|
|
1125
|
+
onMessage: (message, { state, resolve, reject }) => {
|
|
1126
|
+
if (message.type === "error") {
|
|
1127
|
+
reject(new QueueConnectionError(message.message, {
|
|
1128
|
+
outputCode: message.code,
|
|
1129
|
+
detailCode: message.detailCode,
|
|
1130
|
+
origin: message.origin ?? "queue",
|
|
1131
|
+
retryable: message.retryable,
|
|
1132
|
+
acp: message.acp
|
|
1133
|
+
}));
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (!state.acknowledged) {
|
|
1137
|
+
reject(new QueueConnectionError("Queue owner did not acknowledge request", {
|
|
1138
|
+
detailCode: "QUEUE_ACK_MISSING",
|
|
1139
|
+
origin: "queue",
|
|
1140
|
+
retryable: true
|
|
1141
|
+
}));
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (!isExpectedResponse(message)) {
|
|
1145
|
+
reject(new QueueProtocolError("Queue owner returned unexpected response", {
|
|
1146
|
+
detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
|
|
1147
|
+
origin: "queue",
|
|
1148
|
+
retryable: true
|
|
1149
|
+
}));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
resolve(message);
|
|
1153
|
+
},
|
|
1154
|
+
onClose: ({ state, reject }) => {
|
|
1155
|
+
if (!state.acknowledged) {
|
|
1156
|
+
reject(new QueueConnectionError("Queue owner disconnected before acknowledging request", {
|
|
1157
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
|
|
1158
|
+
origin: "queue",
|
|
1159
|
+
retryable: true
|
|
1160
|
+
}));
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
reject(new QueueConnectionError("Queue owner disconnected before responding", {
|
|
1164
|
+
detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
|
|
1165
|
+
origin: "queue",
|
|
1166
|
+
retryable: true
|
|
1167
|
+
}));
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
async function submitCancelToQueueOwner(owner) {
|
|
1172
|
+
const request = {
|
|
1173
|
+
type: "cancel_prompt",
|
|
1174
|
+
requestId: randomUUID(),
|
|
1175
|
+
ownerGeneration: owner.ownerGeneration
|
|
1176
|
+
};
|
|
1177
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "cancel_result");
|
|
1178
|
+
if (!response) return;
|
|
1179
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched cancel response", {
|
|
1180
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1181
|
+
origin: "queue",
|
|
1182
|
+
retryable: true
|
|
1183
|
+
});
|
|
1184
|
+
return response.cancelled;
|
|
1185
|
+
}
|
|
1186
|
+
async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
|
|
1187
|
+
const request = {
|
|
1188
|
+
type: "set_mode",
|
|
1189
|
+
requestId: randomUUID(),
|
|
1190
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1191
|
+
modeId,
|
|
1192
|
+
timeoutMs
|
|
1193
|
+
};
|
|
1194
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_mode_result");
|
|
1195
|
+
if (!response) return;
|
|
1196
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_mode response", {
|
|
1197
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1198
|
+
origin: "queue",
|
|
1199
|
+
retryable: true
|
|
1200
|
+
});
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
async function submitSetModelToQueueOwner(owner, modelId, timeoutMs) {
|
|
1204
|
+
const request = {
|
|
1205
|
+
type: "set_model",
|
|
1206
|
+
requestId: randomUUID(),
|
|
1207
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1208
|
+
modelId,
|
|
1209
|
+
timeoutMs
|
|
1210
|
+
};
|
|
1211
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_model_result");
|
|
1212
|
+
if (!response) return;
|
|
1213
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_model response", {
|
|
1214
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1215
|
+
origin: "queue",
|
|
1216
|
+
retryable: true
|
|
1217
|
+
});
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
|
|
1221
|
+
const request = {
|
|
1222
|
+
type: "set_config_option",
|
|
1223
|
+
requestId: randomUUID(),
|
|
1224
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1225
|
+
configId,
|
|
1226
|
+
value,
|
|
1227
|
+
timeoutMs
|
|
1228
|
+
};
|
|
1229
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_config_option_result");
|
|
1230
|
+
if (!response) return;
|
|
1231
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_config_option response", {
|
|
1232
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1233
|
+
origin: "queue",
|
|
1234
|
+
retryable: true
|
|
1235
|
+
});
|
|
1236
|
+
return response.response;
|
|
1237
|
+
}
|
|
1238
|
+
async function submitCloseSessionToQueueOwner(owner, timeoutMs) {
|
|
1239
|
+
const request = {
|
|
1240
|
+
type: "close_session",
|
|
1241
|
+
requestId: randomUUID(),
|
|
1242
|
+
ownerGeneration: owner.ownerGeneration,
|
|
1243
|
+
timeoutMs
|
|
1244
|
+
};
|
|
1245
|
+
const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "close_session_result");
|
|
1246
|
+
if (!response) return;
|
|
1247
|
+
if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched close_session response", {
|
|
1248
|
+
detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
|
|
1249
|
+
origin: "queue",
|
|
1250
|
+
retryable: true
|
|
1251
|
+
});
|
|
1252
|
+
return response.closed;
|
|
1253
|
+
}
|
|
1254
|
+
async function trySubmitToRunningOwner(options) {
|
|
1255
|
+
const owner = await readQueueOwnerRecord(options.sessionId);
|
|
1256
|
+
if (!owner) return;
|
|
1257
|
+
let submitted;
|
|
1258
|
+
try {
|
|
1259
|
+
submitted = await submitToQueueOwner(owner, options);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (await maybeRecoverStaleOwnerAfterProtocolMismatch({
|
|
1262
|
+
sessionId: options.sessionId,
|
|
1263
|
+
owner,
|
|
1264
|
+
error,
|
|
1265
|
+
verbose: options.verbose
|
|
1266
|
+
})) return;
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
if (submitted) {
|
|
1270
|
+
if (options.verbose) process.stderr.write(`[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
|
|
1271
|
+
return submitted;
|
|
1272
|
+
}
|
|
1273
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
1274
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting queue requests", {
|
|
1275
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1276
|
+
origin: "queue",
|
|
1277
|
+
retryable: true
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
async function tryCloseSessionOnRunningOwner(options) {
|
|
1281
|
+
const owner = await readQueueOwnerRecord(options.sessionId);
|
|
1282
|
+
if (!owner) return;
|
|
1283
|
+
const closed = await submitCloseSessionToQueueOwner(owner, options.timeoutMs);
|
|
1284
|
+
if (closed !== void 0) {
|
|
1285
|
+
if (options.verbose) process.stderr.write(`[acpx] requested session/close on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
|
|
1286
|
+
return closed;
|
|
1287
|
+
}
|
|
1288
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
1289
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting close_session requests", {
|
|
1290
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1291
|
+
origin: "queue",
|
|
1292
|
+
retryable: true
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
async function tryCancelOnRunningOwner(options) {
|
|
1296
|
+
const owner = await readQueueOwnerRecord(options.sessionId);
|
|
1297
|
+
if (!owner) return;
|
|
1298
|
+
const cancelled = await submitCancelToQueueOwner(owner);
|
|
1299
|
+
if (cancelled !== void 0) {
|
|
1300
|
+
if (options.verbose) process.stderr.write(`[acpx] requested cancel on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
|
|
1301
|
+
return cancelled;
|
|
1302
|
+
}
|
|
1303
|
+
if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
|
|
1304
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting cancel requests", {
|
|
1305
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1306
|
+
origin: "queue",
|
|
1307
|
+
retryable: true
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
|
|
1311
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
1312
|
+
if (!owner) return;
|
|
1313
|
+
if (await submitSetModeToQueueOwner(owner, modeId, timeoutMs)) {
|
|
1314
|
+
if (verbose) process.stderr.write(`[acpx] requested session/set_mode on owner pid ${owner.pid} for session ${sessionId}\n`);
|
|
1315
|
+
return true;
|
|
1316
|
+
}
|
|
1317
|
+
if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
|
|
1318
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting set_mode requests", {
|
|
1319
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1320
|
+
origin: "queue",
|
|
1321
|
+
retryable: true
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose) {
|
|
1325
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
1326
|
+
if (!owner) return;
|
|
1327
|
+
if (await submitSetModelToQueueOwner(owner, modelId, timeoutMs)) {
|
|
1328
|
+
if (verbose) process.stderr.write(`[acpx] requested session/set_model on owner pid ${owner.pid} for session ${sessionId}\n`);
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
|
|
1332
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting set_model requests", {
|
|
1333
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1334
|
+
origin: "queue",
|
|
1335
|
+
retryable: true
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
|
|
1339
|
+
const owner = await readQueueOwnerRecord(sessionId);
|
|
1340
|
+
if (!owner) return;
|
|
1341
|
+
const response = await submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs);
|
|
1342
|
+
if (response) {
|
|
1343
|
+
if (verbose) process.stderr.write(`[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}\n`);
|
|
1344
|
+
return response;
|
|
1345
|
+
}
|
|
1346
|
+
if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
|
|
1347
|
+
throw new QueueConnectionError("Session queue owner is running but not accepting set_config_option requests", {
|
|
1348
|
+
detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
|
|
1349
|
+
origin: "queue",
|
|
1350
|
+
retryable: true
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
//#endregion
|
|
1354
|
+
//#region src/cli/session/prompt-runner.ts
|
|
1355
|
+
function buildDirectConnectedSessionOptions(options, run) {
|
|
1356
|
+
return {
|
|
1357
|
+
sessionRecordId: options.sessionRecordId,
|
|
1358
|
+
loadRecord: resolveSessionRecord,
|
|
1359
|
+
saveRecord: writeSessionRecord,
|
|
1360
|
+
mcpServers: options.mcpServers,
|
|
1361
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1362
|
+
authCredentials: options.authCredentials,
|
|
1363
|
+
authPolicy: options.authPolicy,
|
|
1364
|
+
terminal: options.terminal,
|
|
1365
|
+
timeoutMs: options.timeoutMs,
|
|
1366
|
+
verbose: options.verbose,
|
|
1367
|
+
onClientAvailable: (controller) => {
|
|
1368
|
+
options.onClientAvailable?.(controller);
|
|
1369
|
+
},
|
|
1370
|
+
onClientClosed: options.onClientClosed,
|
|
1371
|
+
run
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function toSessionMutationResult(result) {
|
|
1375
|
+
return {
|
|
1376
|
+
record: result.record,
|
|
1377
|
+
resumed: result.resumed,
|
|
1378
|
+
loadError: result.loadError
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
async function runSessionSetModeDirect(options) {
|
|
1382
|
+
return toSessionMutationResult(await withConnectedSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
|
|
1383
|
+
await withTimeout(client.setSessionMode(sessionId, options.modeId), options.timeoutMs);
|
|
1384
|
+
setDesiredModeId(record, options.modeId);
|
|
1385
|
+
})));
|
|
1386
|
+
}
|
|
1387
|
+
async function runSessionSetModelDirect(options) {
|
|
1388
|
+
return toSessionMutationResult(await withConnectedSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
|
|
1389
|
+
await withTimeout(client.setSessionModel(sessionId, options.modelId), options.timeoutMs);
|
|
1390
|
+
setDesiredModelId(record, options.modelId);
|
|
1391
|
+
setCurrentModelId(record, options.modelId);
|
|
1392
|
+
})));
|
|
1393
|
+
}
|
|
1394
|
+
async function runSessionSetConfigOptionDirect(options) {
|
|
1395
|
+
const result = await withConnectedSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
|
|
1396
|
+
const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value), options.timeoutMs);
|
|
1397
|
+
if (options.configId === "mode") setDesiredModeId(record, options.value);
|
|
1398
|
+
else setDesiredConfigOption(record, options.configId, options.value);
|
|
1399
|
+
return response;
|
|
1400
|
+
}));
|
|
1401
|
+
return {
|
|
1402
|
+
record: result.record,
|
|
1403
|
+
response: result.value,
|
|
1404
|
+
resumed: result.resumed,
|
|
1405
|
+
loadError: result.loadError
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
//#endregion
|
|
1409
|
+
//#region src/cli/session/session-control.ts
|
|
1410
|
+
async function cancelSessionPrompt(options) {
|
|
1411
|
+
const cancelled = await tryCancelOnRunningOwner(options);
|
|
1412
|
+
return {
|
|
1413
|
+
sessionId: options.sessionId,
|
|
1414
|
+
cancelled: cancelled === true
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
async function setSessionMode(options) {
|
|
1418
|
+
if (await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose)) {
|
|
1419
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
1420
|
+
setDesiredModeId(record, options.modeId);
|
|
1421
|
+
await writeSessionRecord(record);
|
|
1422
|
+
return {
|
|
1423
|
+
record,
|
|
1424
|
+
resumed: false
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
return await runSessionSetModeDirect({
|
|
1428
|
+
sessionRecordId: options.sessionId,
|
|
1429
|
+
modeId: options.modeId,
|
|
1430
|
+
mcpServers: options.mcpServers,
|
|
1431
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1432
|
+
authCredentials: options.authCredentials,
|
|
1433
|
+
authPolicy: options.authPolicy,
|
|
1434
|
+
terminal: options.terminal,
|
|
1435
|
+
timeoutMs: options.timeoutMs,
|
|
1436
|
+
verbose: options.verbose
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
async function setSessionModel(options) {
|
|
1440
|
+
if (await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose)) {
|
|
1441
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
1442
|
+
setDesiredModelId(record, options.modelId);
|
|
1443
|
+
setCurrentModelId(record, options.modelId);
|
|
1444
|
+
await writeSessionRecord(record);
|
|
1445
|
+
return {
|
|
1446
|
+
record,
|
|
1447
|
+
resumed: false
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
return await runSessionSetModelDirect({
|
|
1451
|
+
sessionRecordId: options.sessionId,
|
|
1452
|
+
modelId: options.modelId,
|
|
1453
|
+
mcpServers: options.mcpServers,
|
|
1454
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1455
|
+
authCredentials: options.authCredentials,
|
|
1456
|
+
authPolicy: options.authPolicy,
|
|
1457
|
+
terminal: options.terminal,
|
|
1458
|
+
timeoutMs: options.timeoutMs,
|
|
1459
|
+
verbose: options.verbose
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
async function setSessionConfigOption(options) {
|
|
1463
|
+
const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose);
|
|
1464
|
+
if (ownerResponse) {
|
|
1465
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
1466
|
+
if (options.configId === "mode") setDesiredModeId(record, options.value);
|
|
1467
|
+
else setDesiredConfigOption(record, options.configId, options.value);
|
|
1468
|
+
await writeSessionRecord(record);
|
|
1469
|
+
return {
|
|
1470
|
+
record,
|
|
1471
|
+
response: ownerResponse,
|
|
1472
|
+
resumed: false
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
return await runSessionSetConfigOptionDirect({
|
|
1476
|
+
sessionRecordId: options.sessionId,
|
|
1477
|
+
configId: options.configId,
|
|
1478
|
+
value: options.value,
|
|
1479
|
+
mcpServers: options.mcpServers,
|
|
1480
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1481
|
+
authCredentials: options.authCredentials,
|
|
1482
|
+
authPolicy: options.authPolicy,
|
|
1483
|
+
terminal: options.terminal,
|
|
1484
|
+
timeoutMs: options.timeoutMs,
|
|
1485
|
+
verbose: options.verbose
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
function firstAgentCommandToken(command) {
|
|
1489
|
+
const trimmed = command.trim();
|
|
1490
|
+
if (!trimmed) return;
|
|
1491
|
+
const token = trimmed.split(/\s+/, 1)[0];
|
|
1492
|
+
return token.length > 0 ? token : void 0;
|
|
1493
|
+
}
|
|
1494
|
+
async function isLikelyMatchingProcess(pid, agentCommand) {
|
|
1495
|
+
const expectedToken = firstAgentCommandToken(agentCommand);
|
|
1496
|
+
if (!expectedToken) return false;
|
|
1497
|
+
const procCmdline = `/proc/${pid}/cmdline`;
|
|
1498
|
+
try {
|
|
1499
|
+
const argv = (await fs$1.readFile(procCmdline, "utf8")).split("\0").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
1500
|
+
if (argv.length === 0) return false;
|
|
1501
|
+
const executableBase = path.basename(argv[0]);
|
|
1502
|
+
const expectedBase = path.basename(expectedToken);
|
|
1503
|
+
return executableBase === expectedBase || argv.some((entry) => path.basename(entry) === expectedBase);
|
|
1504
|
+
} catch {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
async function closeSession(sessionId) {
|
|
1509
|
+
const record = await resolveSessionRecord(sessionId);
|
|
1510
|
+
await tryCloseSessionOnRunningOwner({ sessionId: record.acpxRecordId }).catch(() => {});
|
|
1511
|
+
await terminateQueueOwnerForSession(record.acpxRecordId);
|
|
1512
|
+
if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) await terminateProcess(record.pid);
|
|
1513
|
+
record.pid = void 0;
|
|
1514
|
+
record.closed = true;
|
|
1515
|
+
record.closedAt = isoNow();
|
|
1516
|
+
await writeSessionRecord(record);
|
|
1517
|
+
return record;
|
|
1518
|
+
}
|
|
1519
|
+
//#endregion
|
|
1520
|
+
//#region src/cli/session/session-management.ts
|
|
1521
|
+
async function createSessionRecordWithClient(client, options) {
|
|
1522
|
+
const cwd = absolutePath(options.cwd);
|
|
1523
|
+
await withTimeout(client.start(), options.timeoutMs);
|
|
1524
|
+
let sessionId;
|
|
1525
|
+
let agentSessionId;
|
|
1526
|
+
let sessionResult;
|
|
1527
|
+
let sessionModels;
|
|
1528
|
+
let requestedModelApplied = false;
|
|
1529
|
+
if (options.resumeSessionId) {
|
|
1530
|
+
if (!client.supportsLoadSession()) throw new Error(`Agent command "${options.agentCommand}" does not support session/load; cannot resume session ${options.resumeSessionId}`);
|
|
1531
|
+
try {
|
|
1532
|
+
const loadedSession = await withTimeout(client.loadSession(options.resumeSessionId, cwd), options.timeoutMs);
|
|
1533
|
+
sessionId = options.resumeSessionId;
|
|
1534
|
+
agentSessionId = normalizeRuntimeSessionId(loadedSession.agentSessionId);
|
|
1535
|
+
sessionResult = loadedSession;
|
|
1536
|
+
sessionModels = loadedSession.models;
|
|
1537
|
+
requestedModelApplied = await applyRequestedModelIfAdvertised({
|
|
1538
|
+
client,
|
|
1539
|
+
sessionId,
|
|
1540
|
+
requestedModel: options.sessionOptions?.model,
|
|
1541
|
+
models: sessionModels,
|
|
1542
|
+
agentCommand: options.agentCommand,
|
|
1543
|
+
timeoutMs: options.timeoutMs
|
|
1544
|
+
});
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
throw new Error(`Failed to resume ACP session ${options.resumeSessionId}: ${formatErrorMessage(error)}`, { cause: error });
|
|
1547
|
+
}
|
|
1548
|
+
} else {
|
|
1549
|
+
const createdSession = await withTimeout(client.createSession(cwd), options.timeoutMs);
|
|
1550
|
+
sessionId = createdSession.sessionId;
|
|
1551
|
+
agentSessionId = normalizeRuntimeSessionId(createdSession.agentSessionId);
|
|
1552
|
+
sessionResult = createdSession;
|
|
1553
|
+
sessionModels = createdSession.models;
|
|
1554
|
+
requestedModelApplied = await applyRequestedModelIfAdvertised({
|
|
1555
|
+
client,
|
|
1556
|
+
sessionId,
|
|
1557
|
+
requestedModel: options.sessionOptions?.model,
|
|
1558
|
+
models: sessionModels,
|
|
1559
|
+
agentCommand: options.agentCommand,
|
|
1560
|
+
timeoutMs: options.timeoutMs
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
const lifecycle = client.getAgentLifecycleSnapshot();
|
|
1564
|
+
const now = isoNow();
|
|
1565
|
+
const record = {
|
|
1566
|
+
schema: "acpx.session.v1",
|
|
1567
|
+
acpxRecordId: sessionId,
|
|
1568
|
+
acpSessionId: sessionId,
|
|
1569
|
+
agentSessionId,
|
|
1570
|
+
agentCommand: options.agentCommand,
|
|
1571
|
+
cwd,
|
|
1572
|
+
name: normalizeName(options.name),
|
|
1573
|
+
createdAt: now,
|
|
1574
|
+
lastUsedAt: now,
|
|
1575
|
+
lastSeq: 0,
|
|
1576
|
+
lastRequestId: void 0,
|
|
1577
|
+
eventLog: defaultSessionEventLog(sessionId),
|
|
1578
|
+
closed: false,
|
|
1579
|
+
closedAt: void 0,
|
|
1580
|
+
pid: lifecycle.pid,
|
|
1581
|
+
agentStartedAt: lifecycle.startedAt,
|
|
1582
|
+
protocolVersion: client.initializeResult?.protocolVersion,
|
|
1583
|
+
agentCapabilities: client.initializeResult?.agentCapabilities,
|
|
1584
|
+
...createSessionConversation(now),
|
|
1585
|
+
acpx: {}
|
|
1586
|
+
};
|
|
1587
|
+
persistSessionOptions(record, options.sessionOptions);
|
|
1588
|
+
applyConfigOptionsToRecord(record, sessionResult);
|
|
1589
|
+
syncAdvertisedModelState(record, sessionModels);
|
|
1590
|
+
if (requestedModelApplied) setCurrentModelId(record, options.sessionOptions?.model);
|
|
1591
|
+
await writeSessionRecord(record);
|
|
1592
|
+
return record;
|
|
1593
|
+
}
|
|
1594
|
+
async function createSessionWithClient(options) {
|
|
1595
|
+
const client = new AcpClient({
|
|
1596
|
+
agentCommand: options.agentCommand,
|
|
1597
|
+
cwd: absolutePath(options.cwd),
|
|
1598
|
+
mcpServers: options.mcpServers,
|
|
1599
|
+
permissionMode: options.permissionMode,
|
|
1600
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1601
|
+
permissionPolicy: options.permissionPolicy,
|
|
1602
|
+
authCredentials: options.authCredentials,
|
|
1603
|
+
authPolicy: options.authPolicy,
|
|
1604
|
+
terminal: options.terminal,
|
|
1605
|
+
verbose: options.verbose,
|
|
1606
|
+
sessionOptions: options.sessionOptions
|
|
1607
|
+
});
|
|
1608
|
+
try {
|
|
1609
|
+
return {
|
|
1610
|
+
record: await withInterrupt(async () => await createSessionRecordWithClient(client, options), async () => {
|
|
1611
|
+
await client.close();
|
|
1612
|
+
}),
|
|
1613
|
+
client
|
|
1614
|
+
};
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
await client.close();
|
|
1617
|
+
throw error;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
async function createSession(options) {
|
|
1621
|
+
const { record, client } = await createSessionWithClient(options);
|
|
1622
|
+
try {
|
|
1623
|
+
return record;
|
|
1624
|
+
} finally {
|
|
1625
|
+
await client.close();
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async function ensureSession(options) {
|
|
1629
|
+
const cwd = absolutePath(options.cwd);
|
|
1630
|
+
const gitRoot = findGitRepositoryRoot(cwd);
|
|
1631
|
+
const walkBoundary = options.walkBoundary ?? gitRoot ?? cwd;
|
|
1632
|
+
const existing = await findSessionByDirectoryWalk({
|
|
1633
|
+
agentCommand: options.agentCommand,
|
|
1634
|
+
cwd,
|
|
1635
|
+
name: options.name,
|
|
1636
|
+
boundary: walkBoundary
|
|
1637
|
+
});
|
|
1638
|
+
if (existing) {
|
|
1639
|
+
const requestedModel = options.sessionOptions?.model;
|
|
1640
|
+
if (requestedModel) return {
|
|
1641
|
+
record: (await setSessionModel({
|
|
1642
|
+
sessionId: existing.acpxRecordId,
|
|
1643
|
+
modelId: requestedModel,
|
|
1644
|
+
mcpServers: options.mcpServers,
|
|
1645
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1646
|
+
authCredentials: options.authCredentials,
|
|
1647
|
+
authPolicy: options.authPolicy,
|
|
1648
|
+
terminal: options.terminal,
|
|
1649
|
+
timeoutMs: options.timeoutMs,
|
|
1650
|
+
verbose: options.verbose
|
|
1651
|
+
})).record,
|
|
1652
|
+
created: false
|
|
1653
|
+
};
|
|
1654
|
+
return {
|
|
1655
|
+
record: existing,
|
|
1656
|
+
created: false
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
record: await createSession({
|
|
1661
|
+
agentCommand: options.agentCommand,
|
|
1662
|
+
cwd,
|
|
1663
|
+
name: options.name,
|
|
1664
|
+
resumeSessionId: options.resumeSessionId,
|
|
1665
|
+
mcpServers: options.mcpServers,
|
|
1666
|
+
permissionMode: options.permissionMode,
|
|
1667
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1668
|
+
permissionPolicy: options.permissionPolicy,
|
|
1669
|
+
authCredentials: options.authCredentials,
|
|
1670
|
+
authPolicy: options.authPolicy,
|
|
1671
|
+
terminal: options.terminal,
|
|
1672
|
+
timeoutMs: options.timeoutMs,
|
|
1673
|
+
verbose: options.verbose,
|
|
1674
|
+
sessionOptions: options.sessionOptions
|
|
1675
|
+
}),
|
|
1676
|
+
created: true
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/perf-metrics-capture.ts
|
|
1681
|
+
const PERF_METRICS_FILE_ENV = "ACPX_PERF_METRICS_FILE";
|
|
1682
|
+
let installed = false;
|
|
1683
|
+
let flushed = false;
|
|
1684
|
+
let captureFilePath;
|
|
1685
|
+
let captureRole = "cli";
|
|
1686
|
+
let captureArgv = [];
|
|
1687
|
+
let captureSequence = 0;
|
|
1688
|
+
function shouldCapture() {
|
|
1689
|
+
return typeof captureFilePath === "string" && captureFilePath.trim().length > 0;
|
|
1690
|
+
}
|
|
1691
|
+
function buildPayload(reason) {
|
|
1692
|
+
return {
|
|
1693
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1694
|
+
pid: process.pid,
|
|
1695
|
+
ppid: process.ppid,
|
|
1696
|
+
role: captureRole,
|
|
1697
|
+
argv: captureArgv,
|
|
1698
|
+
cwd: process.cwd(),
|
|
1699
|
+
sequence: captureSequence,
|
|
1700
|
+
reason,
|
|
1701
|
+
metrics: getPerfMetricsSnapshot()
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function writePerfMetricsCapture(reason, resetAfterWrite) {
|
|
1705
|
+
if (!shouldCapture()) return false;
|
|
1706
|
+
const payload = buildPayload(reason);
|
|
1707
|
+
const metrics = payload.metrics;
|
|
1708
|
+
if (!(Object.keys(metrics.counters ?? {}).length > 0 || Object.keys(metrics.gauges ?? {}).length > 0 || Object.keys(metrics.timings ?? {}).length > 0)) return false;
|
|
1709
|
+
try {
|
|
1710
|
+
fs.mkdirSync(path.dirname(captureFilePath), { recursive: true });
|
|
1711
|
+
fs.appendFileSync(captureFilePath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
1712
|
+
captureSequence += 1;
|
|
1713
|
+
if (resetAfterWrite) resetPerfMetrics();
|
|
1714
|
+
return true;
|
|
1715
|
+
} catch {
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function checkpointPerfMetricsCapture() {
|
|
1720
|
+
flushed = false;
|
|
1721
|
+
writePerfMetricsCapture("checkpoint", true);
|
|
1722
|
+
}
|
|
1723
|
+
function flushPerfMetricsCapture(reason = "exit") {
|
|
1724
|
+
if (flushed || !shouldCapture()) return;
|
|
1725
|
+
flushed = true;
|
|
1726
|
+
writePerfMetricsCapture(reason, false);
|
|
1727
|
+
}
|
|
1728
|
+
function installPerfMetricsCapture(options = {}) {
|
|
1729
|
+
captureFilePath = options.filePath ?? process.env[PERF_METRICS_FILE_ENV];
|
|
1730
|
+
if (!shouldCapture()) return;
|
|
1731
|
+
resetPerfMetrics();
|
|
1732
|
+
captureRole = options.role ?? captureRole;
|
|
1733
|
+
captureArgv = options.argv ?? [];
|
|
1734
|
+
captureSequence = 0;
|
|
1735
|
+
flushed = false;
|
|
1736
|
+
if (installed) return;
|
|
1737
|
+
installed = true;
|
|
1738
|
+
process.once("exit", () => {
|
|
1739
|
+
flushPerfMetricsCapture("exit");
|
|
1740
|
+
});
|
|
1741
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
1742
|
+
const handler = () => {
|
|
1743
|
+
flushPerfMetricsCapture("signal");
|
|
1744
|
+
process.removeListener(signal, handler);
|
|
1745
|
+
process.kill(process.pid, signal);
|
|
1746
|
+
};
|
|
1747
|
+
process.once(signal, handler);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
//#endregion
|
|
1751
|
+
//#region src/cli/queue/owner-turn-controller.ts
|
|
1752
|
+
var QueueOwnerTurnController = class {
|
|
1753
|
+
options;
|
|
1754
|
+
state = "idle";
|
|
1755
|
+
pendingCancel = false;
|
|
1756
|
+
activeController;
|
|
1757
|
+
constructor(options) {
|
|
1758
|
+
this.options = options;
|
|
1759
|
+
}
|
|
1760
|
+
get lifecycleState() {
|
|
1761
|
+
return this.state;
|
|
1762
|
+
}
|
|
1763
|
+
get hasPendingCancel() {
|
|
1764
|
+
return this.pendingCancel;
|
|
1765
|
+
}
|
|
1766
|
+
beginTurn() {
|
|
1767
|
+
this.state = "starting";
|
|
1768
|
+
this.pendingCancel = false;
|
|
1769
|
+
}
|
|
1770
|
+
markPromptActive() {
|
|
1771
|
+
if (this.state === "starting" || this.state === "active") this.state = "active";
|
|
1772
|
+
}
|
|
1773
|
+
endTurn() {
|
|
1774
|
+
this.state = "idle";
|
|
1775
|
+
this.pendingCancel = false;
|
|
1776
|
+
}
|
|
1777
|
+
beginClosing() {
|
|
1778
|
+
this.state = "closing";
|
|
1779
|
+
this.pendingCancel = false;
|
|
1780
|
+
this.activeController = void 0;
|
|
1781
|
+
}
|
|
1782
|
+
setActiveController(controller) {
|
|
1783
|
+
this.activeController = controller;
|
|
1784
|
+
}
|
|
1785
|
+
clearActiveController() {
|
|
1786
|
+
this.activeController = void 0;
|
|
1787
|
+
}
|
|
1788
|
+
assertCanHandleControlRequest() {
|
|
1789
|
+
if (this.state === "closing") throw new QueueConnectionError("Queue owner is closing", {
|
|
1790
|
+
detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
|
|
1791
|
+
origin: "queue",
|
|
1792
|
+
retryable: true
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
async requestCancel() {
|
|
1796
|
+
const activeController = this.activeController;
|
|
1797
|
+
if (activeController?.hasActivePrompt()) {
|
|
1798
|
+
const cancelled = await activeController.requestCancelActivePrompt();
|
|
1799
|
+
if (cancelled) this.pendingCancel = false;
|
|
1800
|
+
return cancelled;
|
|
1801
|
+
}
|
|
1802
|
+
if (this.state === "starting" || this.state === "active") {
|
|
1803
|
+
this.pendingCancel = true;
|
|
1804
|
+
return true;
|
|
1805
|
+
}
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
async applyPendingCancel() {
|
|
1809
|
+
const activeController = this.activeController;
|
|
1810
|
+
if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) return false;
|
|
1811
|
+
const cancelled = await activeController.requestCancelActivePrompt();
|
|
1812
|
+
if (cancelled) this.pendingCancel = false;
|
|
1813
|
+
return cancelled;
|
|
1814
|
+
}
|
|
1815
|
+
async setSessionMode(modeId, timeoutMs) {
|
|
1816
|
+
this.assertCanHandleControlRequest();
|
|
1817
|
+
const activeController = this.activeController;
|
|
1818
|
+
if (activeController) {
|
|
1819
|
+
await this.options.withTimeout(async () => await activeController.setSessionMode(modeId), timeoutMs);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
await this.options.setSessionModeFallback(modeId, timeoutMs);
|
|
1823
|
+
}
|
|
1824
|
+
async setSessionModel(modelId, timeoutMs) {
|
|
1825
|
+
this.assertCanHandleControlRequest();
|
|
1826
|
+
const activeController = this.activeController;
|
|
1827
|
+
if (activeController) {
|
|
1828
|
+
await this.options.withTimeout(async () => await activeController.setSessionModel(modelId), timeoutMs);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
await this.options.setSessionModelFallback(modelId, timeoutMs);
|
|
1832
|
+
}
|
|
1833
|
+
async setSessionConfigOption(configId, value, timeoutMs) {
|
|
1834
|
+
this.assertCanHandleControlRequest();
|
|
1835
|
+
const activeController = this.activeController;
|
|
1836
|
+
if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionConfigOption(configId, value), timeoutMs);
|
|
1837
|
+
return await this.options.setSessionConfigOptionFallback(configId, value, timeoutMs);
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
//#endregion
|
|
1841
|
+
//#region src/cli/session/queue-owner-process.ts
|
|
1842
|
+
function sanitizeQueueOwnerExecArgv(execArgv = process.execArgv) {
|
|
1843
|
+
const sanitized = [];
|
|
1844
|
+
for (let index = 0; index < execArgv.length; index += 1) {
|
|
1845
|
+
const value = execArgv[index];
|
|
1846
|
+
if (value === "--experimental-test-coverage" || value === "--test") continue;
|
|
1847
|
+
if (value === "--test-name-pattern" || value === "--test-reporter" || value === "--test-reporter-destination") {
|
|
1848
|
+
index += 1;
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
if (value.startsWith("--test-")) continue;
|
|
1852
|
+
if (value === "--inspect" || value === "--inspect-brk" || value === "--inspect-port" || value === "--inspect-publish-uid" || value.startsWith("--inspect=") || value.startsWith("--inspect-brk=") || value.startsWith("--inspect-port=") || value.startsWith("--inspect-publish-uid=") || value === "--debug-port" || value.startsWith("--debug-port=")) {
|
|
1853
|
+
if (value === "--inspect" || value === "--inspect-brk" || value === "--inspect-port" || value === "--inspect-publish-uid" || value === "--debug-port") index += 1;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
sanitized.push(value);
|
|
1857
|
+
}
|
|
1858
|
+
return sanitized;
|
|
1859
|
+
}
|
|
1860
|
+
function buildQueueOwnerArgOverride(entryPath, execArgv = process.execArgv) {
|
|
1861
|
+
const sanitized = sanitizeQueueOwnerExecArgv(execArgv);
|
|
1862
|
+
if (sanitized.length === 0) return null;
|
|
1863
|
+
return JSON.stringify([
|
|
1864
|
+
...sanitized,
|
|
1865
|
+
entryPath,
|
|
1866
|
+
"__queue-owner"
|
|
1867
|
+
]);
|
|
1868
|
+
}
|
|
1869
|
+
function resolveQueueOwnerSpawnArgs(argv = process.argv) {
|
|
1870
|
+
const override = process.env.ACPX_QUEUE_OWNER_ARGS;
|
|
1871
|
+
if (override) {
|
|
1872
|
+
const parsed = JSON.parse(override);
|
|
1873
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0)) return [...parsed];
|
|
1874
|
+
throw new Error("acpx self-spawn failed: invalid ACPX_QUEUE_OWNER_ARGS");
|
|
1875
|
+
}
|
|
1876
|
+
const entry = argv[1];
|
|
1877
|
+
if (!entry || entry.trim().length === 0) throw new Error("acpx self-spawn failed: missing CLI entry path");
|
|
1878
|
+
return [realpathSync(entry), "__queue-owner"];
|
|
1879
|
+
}
|
|
1880
|
+
function queueOwnerRuntimeOptionsFromSend(options) {
|
|
1881
|
+
return {
|
|
1882
|
+
sessionId: options.sessionId,
|
|
1883
|
+
mcpServers: options.mcpServers,
|
|
1884
|
+
permissionMode: options.permissionMode,
|
|
1885
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1886
|
+
authCredentials: options.authCredentials,
|
|
1887
|
+
authPolicy: options.authPolicy,
|
|
1888
|
+
terminal: options.terminal,
|
|
1889
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1890
|
+
verbose: options.verbose,
|
|
1891
|
+
ttlMs: options.ttlMs,
|
|
1892
|
+
maxQueueDepth: options.maxQueueDepth,
|
|
1893
|
+
promptRetries: options.promptRetries,
|
|
1894
|
+
sessionOptions: options.sessionOptions
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function buildQueueOwnerSpawnOptions(payload) {
|
|
1898
|
+
return {
|
|
1899
|
+
detached: true,
|
|
1900
|
+
stdio: "ignore",
|
|
1901
|
+
env: {
|
|
1902
|
+
...process.env,
|
|
1903
|
+
ACPX_QUEUE_OWNER_PAYLOAD: payload
|
|
1904
|
+
},
|
|
1905
|
+
windowsHide: true
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function spawnQueueOwnerProcess(options) {
|
|
1909
|
+
const payload = JSON.stringify(options);
|
|
1910
|
+
spawn(process.execPath, resolveQueueOwnerSpawnArgs(), buildQueueOwnerSpawnOptions(payload)).unref();
|
|
1911
|
+
}
|
|
1912
|
+
//#endregion
|
|
1913
|
+
//#region src/session/events.ts
|
|
1914
|
+
const LOCK_RETRY_MS = 15;
|
|
1915
|
+
const EVENT_LOCK_STALE_MS = 15e3;
|
|
1916
|
+
async function ensureSessionDir() {
|
|
1917
|
+
await fs$1.mkdir(sessionBaseDir(), { recursive: true });
|
|
1918
|
+
}
|
|
1919
|
+
async function pathExists(filePath) {
|
|
1920
|
+
try {
|
|
1921
|
+
await fs$1.access(filePath);
|
|
1922
|
+
return true;
|
|
1923
|
+
} catch {
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
async function statSize(filePath) {
|
|
1928
|
+
try {
|
|
1929
|
+
return (await fs$1.stat(filePath)).size;
|
|
1930
|
+
} catch {
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
async function countExistingSegments(sessionId, maxSegments) {
|
|
1935
|
+
let count = 0;
|
|
1936
|
+
for (let segment = 1; segment <= maxSegments; segment += 1) if (await pathExists(sessionEventSegmentPath(sessionId, segment))) count += 1;
|
|
1937
|
+
if (await pathExists(sessionEventActivePath(sessionId))) count += 1;
|
|
1938
|
+
return count;
|
|
1939
|
+
}
|
|
1940
|
+
async function rotateSegments(sessionId, maxSegments) {
|
|
1941
|
+
const active = sessionEventActivePath(sessionId);
|
|
1942
|
+
const overflow = sessionEventSegmentPath(sessionId, maxSegments);
|
|
1943
|
+
await fs$1.unlink(overflow).catch((error) => {
|
|
1944
|
+
if (error.code !== "ENOENT") throw error;
|
|
1945
|
+
});
|
|
1946
|
+
for (let segment = maxSegments - 1; segment >= 1; segment -= 1) {
|
|
1947
|
+
const from = sessionEventSegmentPath(sessionId, segment);
|
|
1948
|
+
const to = sessionEventSegmentPath(sessionId, segment + 1);
|
|
1949
|
+
if (!await pathExists(from)) continue;
|
|
1950
|
+
await fs$1.rename(from, to);
|
|
1951
|
+
}
|
|
1952
|
+
if (await pathExists(active)) await fs$1.rename(active, sessionEventSegmentPath(sessionId, 1));
|
|
1953
|
+
}
|
|
1954
|
+
function parseEventLockPayload(raw) {
|
|
1955
|
+
try {
|
|
1956
|
+
const parsed = JSON.parse(raw);
|
|
1957
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
1958
|
+
const record = parsed;
|
|
1959
|
+
return {
|
|
1960
|
+
pid: typeof record.pid === "number" ? record.pid : void 0,
|
|
1961
|
+
created_at: typeof record.created_at === "string" ? record.created_at : void 0
|
|
1962
|
+
};
|
|
1963
|
+
} catch {
|
|
1964
|
+
return {};
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
async function removeStaleEventLock(lockPath) {
|
|
1968
|
+
try {
|
|
1969
|
+
const parsed = parseEventLockPayload(await fs$1.readFile(lockPath, "utf8"));
|
|
1970
|
+
const createdAtMs = parsed.created_at ? Date.parse(parsed.created_at) : NaN;
|
|
1971
|
+
const lockAgeMs = Number.isFinite(createdAtMs) ? Date.now() - createdAtMs : Number.POSITIVE_INFINITY;
|
|
1972
|
+
if (isProcessAlive(parsed.pid) && lockAgeMs <= EVENT_LOCK_STALE_MS) return false;
|
|
1973
|
+
await fs$1.unlink(lockPath);
|
|
1974
|
+
incrementPerfCounter("session.events.stale_lock_recovered");
|
|
1975
|
+
return true;
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
if (error.code === "ENOENT") return true;
|
|
1978
|
+
return false;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
async function acquireEventsLock(sessionId) {
|
|
1982
|
+
await ensureSessionDir();
|
|
1983
|
+
const lockPath = sessionEventLockPath(sessionId);
|
|
1984
|
+
const payload = JSON.stringify({
|
|
1985
|
+
pid: process.pid,
|
|
1986
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1987
|
+
}, null, 2);
|
|
1988
|
+
for (;;) try {
|
|
1989
|
+
await fs$1.writeFile(lockPath, `${payload}\n`, {
|
|
1990
|
+
encoding: "utf8",
|
|
1991
|
+
flag: "wx"
|
|
1992
|
+
});
|
|
1993
|
+
return { filePath: lockPath };
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
if (error.code !== "EEXIST") throw error;
|
|
1996
|
+
if (await removeStaleEventLock(lockPath)) continue;
|
|
1997
|
+
await new Promise((resolve) => {
|
|
1998
|
+
setTimeout(resolve, LOCK_RETRY_MS);
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
async function releaseEventsLock(lock) {
|
|
2003
|
+
await fs$1.unlink(lock.filePath).catch((error) => {
|
|
2004
|
+
if (error.code !== "ENOENT") throw error;
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
var SessionEventWriter = class SessionEventWriter {
|
|
2008
|
+
record;
|
|
2009
|
+
lock;
|
|
2010
|
+
maxSegmentBytes;
|
|
2011
|
+
maxSegments;
|
|
2012
|
+
activePath;
|
|
2013
|
+
activeSizeBytes;
|
|
2014
|
+
segmentCount;
|
|
2015
|
+
closed = false;
|
|
2016
|
+
constructor(record, lock, options, state) {
|
|
2017
|
+
this.record = record;
|
|
2018
|
+
this.lock = lock;
|
|
2019
|
+
this.maxSegmentBytes = options.maxSegmentBytes;
|
|
2020
|
+
this.maxSegments = options.maxSegments;
|
|
2021
|
+
this.activePath = state.activePath;
|
|
2022
|
+
this.activeSizeBytes = state.activeSizeBytes;
|
|
2023
|
+
this.segmentCount = state.segmentCount;
|
|
2024
|
+
}
|
|
2025
|
+
static async open(record, options = {}) {
|
|
2026
|
+
const lock = await acquireEventsLock(record.acpxRecordId);
|
|
2027
|
+
const maxSegmentBytes = options.maxSegmentBytes ?? record.eventLog.max_segment_bytes ?? 67108864;
|
|
2028
|
+
const maxSegments = options.maxSegments ?? record.eventLog.max_segments ?? 5;
|
|
2029
|
+
const activePath = sessionEventActivePath(record.acpxRecordId);
|
|
2030
|
+
const activeSizeBytes = await statSize(activePath);
|
|
2031
|
+
const segmentCount = Number.isInteger(record.eventLog.segment_count) && record.eventLog.segment_count > 0 ? record.eventLog.segment_count : await countExistingSegments(record.acpxRecordId, maxSegments) || 1;
|
|
2032
|
+
return new SessionEventWriter(record, lock, {
|
|
2033
|
+
maxSegmentBytes,
|
|
2034
|
+
maxSegments
|
|
2035
|
+
}, {
|
|
2036
|
+
activePath,
|
|
2037
|
+
activeSizeBytes,
|
|
2038
|
+
segmentCount
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
getRecord() {
|
|
2042
|
+
return this.record;
|
|
2043
|
+
}
|
|
2044
|
+
async appendMessage(message, options = {}) {
|
|
2045
|
+
await this.appendMessages([message], options);
|
|
2046
|
+
}
|
|
2047
|
+
async appendMessages(messages, options = {}) {
|
|
2048
|
+
if (this.closed) throw new Error("SessionEventWriter is closed");
|
|
2049
|
+
if (messages.length === 0) return;
|
|
2050
|
+
await ensureSessionDir();
|
|
2051
|
+
await measurePerf("session.events.append_batch", async () => {
|
|
2052
|
+
for (const message of messages) {
|
|
2053
|
+
if (!isAcpJsonRpcMessage(message)) throw new Error("Attempted to persist invalid ACP JSON-RPC payload");
|
|
2054
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
2055
|
+
const lineBytes = Buffer.byteLength(line);
|
|
2056
|
+
if (this.activeSizeBytes > 0 && this.activeSizeBytes + lineBytes > this.maxSegmentBytes) {
|
|
2057
|
+
await rotateSegments(this.record.acpxRecordId, this.maxSegments);
|
|
2058
|
+
this.activePath = sessionEventActivePath(this.record.acpxRecordId);
|
|
2059
|
+
this.activeSizeBytes = 0;
|
|
2060
|
+
this.segmentCount = Math.min(this.segmentCount + 1, this.maxSegments);
|
|
2061
|
+
incrementPerfCounter("session.events.rotate");
|
|
2062
|
+
}
|
|
2063
|
+
await fs$1.appendFile(this.activePath, line, "utf8");
|
|
2064
|
+
this.activeSizeBytes += lineBytes;
|
|
2065
|
+
this.record.lastSeq += 1;
|
|
2066
|
+
if (Object.hasOwn(message, "id")) {
|
|
2067
|
+
const id = message.id;
|
|
2068
|
+
if (typeof id === "string" || typeof id === "number") this.record.lastRequestId = String(id);
|
|
2069
|
+
}
|
|
2070
|
+
const writeTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
2071
|
+
this.record.lastUsedAt = writeTs;
|
|
2072
|
+
this.record.eventLog = {
|
|
2073
|
+
active_path: this.activePath,
|
|
2074
|
+
segment_count: this.segmentCount,
|
|
2075
|
+
max_segment_bytes: this.maxSegmentBytes,
|
|
2076
|
+
max_segments: this.maxSegments,
|
|
2077
|
+
last_write_at: writeTs,
|
|
2078
|
+
last_write_error: null
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
if (options.checkpoint === true) await writeSessionRecord(this.record);
|
|
2083
|
+
}
|
|
2084
|
+
async checkpoint() {
|
|
2085
|
+
if (this.closed) throw new Error("SessionEventWriter is closed");
|
|
2086
|
+
await writeSessionRecord(this.record);
|
|
2087
|
+
}
|
|
2088
|
+
async close(options = {}) {
|
|
2089
|
+
if (this.closed) return;
|
|
2090
|
+
try {
|
|
2091
|
+
if (options.checkpoint !== false) await writeSessionRecord(this.record);
|
|
2092
|
+
} finally {
|
|
2093
|
+
this.closed = true;
|
|
2094
|
+
await releaseEventsLock(this.lock);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
//#endregion
|
|
2099
|
+
//#region src/cli/session/runtime.ts
|
|
2100
|
+
const INTERRUPT_CANCEL_WAIT_MS = 2500;
|
|
2101
|
+
var QueueTaskOutputFormatter = class {
|
|
2102
|
+
requestId;
|
|
2103
|
+
send;
|
|
2104
|
+
constructor(task) {
|
|
2105
|
+
this.requestId = task.requestId;
|
|
2106
|
+
this.send = task.send;
|
|
2107
|
+
}
|
|
2108
|
+
setContext(_context) {}
|
|
2109
|
+
onAcpMessage(message) {
|
|
2110
|
+
this.send({
|
|
2111
|
+
type: "event",
|
|
2112
|
+
requestId: this.requestId,
|
|
2113
|
+
message
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
onError(params) {
|
|
2117
|
+
this.send({
|
|
2118
|
+
type: "error",
|
|
2119
|
+
requestId: this.requestId,
|
|
2120
|
+
code: params.code,
|
|
2121
|
+
detailCode: params.detailCode,
|
|
2122
|
+
origin: params.origin,
|
|
2123
|
+
message: params.message,
|
|
2124
|
+
retryable: params.retryable,
|
|
2125
|
+
acp: params.acp
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
onPermissionEscalation(event) {
|
|
2129
|
+
this.send({
|
|
2130
|
+
type: "permission_escalation",
|
|
2131
|
+
requestId: this.requestId,
|
|
2132
|
+
event
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
flush() {}
|
|
2136
|
+
};
|
|
2137
|
+
const DISCARD_OUTPUT_FORMATTER = {
|
|
2138
|
+
setContext() {},
|
|
2139
|
+
onAcpMessage() {},
|
|
2140
|
+
onError() {},
|
|
2141
|
+
onPermissionEscalation() {},
|
|
2142
|
+
flush() {}
|
|
2143
|
+
};
|
|
2144
|
+
function toPromptResult(stopReason, sessionId, client) {
|
|
2145
|
+
return {
|
|
2146
|
+
stopReason,
|
|
2147
|
+
sessionId,
|
|
2148
|
+
permissionStats: client.getPermissionStats()
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
async function applyPromptModelIfAdvertised(params) {
|
|
2152
|
+
const requestedModel = typeof params.requestedModel === "string" ? params.requestedModel.trim() : "";
|
|
2153
|
+
if (!requestedModel) return;
|
|
2154
|
+
const availableModels = params.record.acpx?.available_models;
|
|
2155
|
+
assertRequestedModelSupported({
|
|
2156
|
+
requestedModel,
|
|
2157
|
+
models: Array.isArray(availableModels) ? {
|
|
2158
|
+
currentModelId: params.record.acpx?.current_model_id ?? "",
|
|
2159
|
+
availableModels: availableModels.map((modelId) => ({
|
|
2160
|
+
modelId,
|
|
2161
|
+
name: modelId
|
|
2162
|
+
}))
|
|
2163
|
+
} : void 0,
|
|
2164
|
+
agentCommand: params.record.agentCommand,
|
|
2165
|
+
context: "apply"
|
|
2166
|
+
});
|
|
2167
|
+
if (!Array.isArray(availableModels)) return;
|
|
2168
|
+
if (params.record.acpx?.current_model_id === requestedModel) {
|
|
2169
|
+
setDesiredModelId(params.record, requestedModel);
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
await withTimeout(params.client.setSessionModel(params.sessionId, requestedModel), params.timeoutMs);
|
|
2173
|
+
setDesiredModelId(params.record, requestedModel);
|
|
2174
|
+
setCurrentModelId(params.record, requestedModel);
|
|
2175
|
+
}
|
|
2176
|
+
function jsonRpcIdKey$1(value) {
|
|
2177
|
+
if (typeof value === "string") return `s:${value}`;
|
|
2178
|
+
if (typeof value === "number" && Number.isFinite(value)) return `n:${value}`;
|
|
2179
|
+
}
|
|
2180
|
+
function extractJsonRpcRequestInfo(message) {
|
|
2181
|
+
const candidate = message;
|
|
2182
|
+
if (typeof candidate.method !== "string") return;
|
|
2183
|
+
const idKey = jsonRpcIdKey$1(candidate.id);
|
|
2184
|
+
if (!idKey) return;
|
|
2185
|
+
return {
|
|
2186
|
+
idKey,
|
|
2187
|
+
method: candidate.method
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
function extractJsonRpcResponseInfo(message) {
|
|
2191
|
+
const candidate = message;
|
|
2192
|
+
const idKey = jsonRpcIdKey$1(candidate.id);
|
|
2193
|
+
if (!idKey) return;
|
|
2194
|
+
const hasError = Object.hasOwn(candidate, "error");
|
|
2195
|
+
if (!hasError && !Object.hasOwn(candidate, "result")) return;
|
|
2196
|
+
return {
|
|
2197
|
+
idKey,
|
|
2198
|
+
hasError
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
function filterRecoverableLoadFallbackOutput(messages) {
|
|
2202
|
+
const requestMethodById = /* @__PURE__ */ new Map();
|
|
2203
|
+
const failedLoadRequestIds = /* @__PURE__ */ new Set();
|
|
2204
|
+
for (const message of messages) {
|
|
2205
|
+
const request = extractJsonRpcRequestInfo(message);
|
|
2206
|
+
if (request) {
|
|
2207
|
+
requestMethodById.set(request.idKey, request.method);
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
const response = extractJsonRpcResponseInfo(message);
|
|
2211
|
+
if (!response || !response.hasError) continue;
|
|
2212
|
+
if (requestMethodById.get(response.idKey) === "session/load") failedLoadRequestIds.add(response.idKey);
|
|
2213
|
+
}
|
|
2214
|
+
if (failedLoadRequestIds.size === 0) return messages;
|
|
2215
|
+
return messages.filter((message) => {
|
|
2216
|
+
const request = extractJsonRpcRequestInfo(message);
|
|
2217
|
+
if (request && request.method === "session/load" && failedLoadRequestIds.has(request.idKey)) return false;
|
|
2218
|
+
const response = extractJsonRpcResponseInfo(message);
|
|
2219
|
+
if (response && failedLoadRequestIds.has(response.idKey)) return false;
|
|
2220
|
+
return true;
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
function emitPromptRetryNotice(params) {
|
|
2224
|
+
if (params.suppressSdkConsoleErrors) return;
|
|
2225
|
+
process.stderr.write(`[acpx] prompt failed (${formatErrorMessage(params.error)}), retrying in ${params.delayMs}ms (attempt ${params.attempt}/${params.maxRetries})\n`);
|
|
2226
|
+
}
|
|
2227
|
+
async function runQueuedTask(sessionRecordId, task, options) {
|
|
2228
|
+
const outputFormatter = task.waitForCompletion ? new QueueTaskOutputFormatter(task) : DISCARD_OUTPUT_FORMATTER;
|
|
2229
|
+
try {
|
|
2230
|
+
const result = await runSessionPrompt({
|
|
2231
|
+
sessionRecordId,
|
|
2232
|
+
mcpServers: options.mcpServers,
|
|
2233
|
+
prompt: task.prompt ?? textPrompt(task.message),
|
|
2234
|
+
permissionMode: task.permissionMode,
|
|
2235
|
+
resumePolicy: task.resumePolicy,
|
|
2236
|
+
nonInteractivePermissions: task.nonInteractivePermissions ?? options.nonInteractivePermissions,
|
|
2237
|
+
permissionPolicy: task.permissionPolicy,
|
|
2238
|
+
authCredentials: options.authCredentials,
|
|
2239
|
+
authPolicy: options.authPolicy,
|
|
2240
|
+
outputFormatter,
|
|
2241
|
+
timeoutMs: task.timeoutMs,
|
|
2242
|
+
suppressSdkConsoleErrors: task.suppressSdkConsoleErrors ?? options.suppressSdkConsoleErrors,
|
|
2243
|
+
verbose: options.verbose,
|
|
2244
|
+
promptRetries: task.promptRetries ?? options.promptRetries ?? 0,
|
|
2245
|
+
sessionOptions: mergeSessionOptions(task.sessionOptions, options.sessionOptions),
|
|
2246
|
+
onClientAvailable: options.onClientAvailable,
|
|
2247
|
+
onClientClosed: options.onClientClosed,
|
|
2248
|
+
onPromptActive: options.onPromptActive,
|
|
2249
|
+
client: options.sharedClient
|
|
2250
|
+
});
|
|
2251
|
+
if (task.waitForCompletion) task.send({
|
|
2252
|
+
type: "result",
|
|
2253
|
+
requestId: task.requestId,
|
|
2254
|
+
result
|
|
2255
|
+
});
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
const normalizedError = normalizeOutputError(error, {
|
|
2258
|
+
origin: "runtime",
|
|
2259
|
+
detailCode: "QUEUE_RUNTIME_PROMPT_FAILED"
|
|
2260
|
+
});
|
|
2261
|
+
const alreadyEmitted = error.outputAlreadyEmitted === true;
|
|
2262
|
+
if (task.waitForCompletion) task.send({
|
|
2263
|
+
type: "error",
|
|
2264
|
+
requestId: task.requestId,
|
|
2265
|
+
code: normalizedError.code,
|
|
2266
|
+
detailCode: normalizedError.detailCode,
|
|
2267
|
+
origin: normalizedError.origin,
|
|
2268
|
+
message: normalizedError.message,
|
|
2269
|
+
retryable: normalizedError.retryable,
|
|
2270
|
+
acp: normalizedError.acp,
|
|
2271
|
+
outputAlreadyEmitted: alreadyEmitted
|
|
2272
|
+
});
|
|
2273
|
+
if (error instanceof InterruptedError) throw error;
|
|
2274
|
+
} finally {
|
|
2275
|
+
task.close();
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
async function runSessionPrompt(options) {
|
|
2279
|
+
const stopTotalTimer = startPerfTimer("runtime.prompt.total");
|
|
2280
|
+
const output = options.outputFormatter;
|
|
2281
|
+
const record = await measurePerf("session.resolve_prompt_record", async () => {
|
|
2282
|
+
return await resolveSessionRecord(options.sessionRecordId);
|
|
2283
|
+
});
|
|
2284
|
+
const conversation = cloneSessionConversation(record);
|
|
2285
|
+
let acpxState = cloneSessionAcpxState(record.acpx);
|
|
2286
|
+
const promptStartedAt = isoNow();
|
|
2287
|
+
const promptMessageId = recordPromptSubmission(conversation, options.prompt, promptStartedAt);
|
|
2288
|
+
record.lastPromptAt = promptStartedAt;
|
|
2289
|
+
record.lastUsedAt = promptStartedAt;
|
|
2290
|
+
applyConversation(record, conversation);
|
|
2291
|
+
record.acpx = acpxState;
|
|
2292
|
+
await writeSessionRecord(record);
|
|
2293
|
+
output.setContext({ sessionId: record.acpxRecordId });
|
|
2294
|
+
const eventWriter = await measurePerf("session.events.open", async () => {
|
|
2295
|
+
return await SessionEventWriter.open(record);
|
|
2296
|
+
});
|
|
2297
|
+
const pendingMessages = [];
|
|
2298
|
+
const pendingConnectOutputMessages = [];
|
|
2299
|
+
const sessionOptions = mergeSessionOptions(options.sessionOptions, sessionOptionsFromRecord(record));
|
|
2300
|
+
let bufferingConnectOutput = true;
|
|
2301
|
+
let promptTurnActive = false;
|
|
2302
|
+
let promptTurnHadSideEffects = false;
|
|
2303
|
+
let sawAcpMessage = false;
|
|
2304
|
+
let eventWriterClosed = false;
|
|
2305
|
+
const closeEventWriter = async (checkpoint) => {
|
|
2306
|
+
if (eventWriterClosed) return;
|
|
2307
|
+
eventWriterClosed = true;
|
|
2308
|
+
await eventWriter.close({ checkpoint });
|
|
2309
|
+
};
|
|
2310
|
+
const flushPendingMessages = async (checkpoint = false) => {
|
|
2311
|
+
if (pendingMessages.length === 0) return;
|
|
2312
|
+
const batch = pendingMessages.splice(0);
|
|
2313
|
+
await measurePerf("session.events.flush_pending", async () => {
|
|
2314
|
+
await eventWriter.appendMessages(batch, { checkpoint });
|
|
2315
|
+
});
|
|
2316
|
+
};
|
|
2317
|
+
const preserveClosedState = async () => {
|
|
2318
|
+
const latest = await resolveSessionRecord(record.acpxRecordId).catch(() => void 0);
|
|
2319
|
+
if (!latest?.closed) return;
|
|
2320
|
+
record.closed = true;
|
|
2321
|
+
record.closedAt = latest.closedAt ?? record.closedAt ?? isoNow();
|
|
2322
|
+
record.pid = latest.pid;
|
|
2323
|
+
if (latest.acpx) record.acpx = {
|
|
2324
|
+
...record.acpx,
|
|
2325
|
+
...latest.acpx
|
|
2326
|
+
};
|
|
2327
|
+
};
|
|
2328
|
+
const liveCheckpoint = new LiveSessionCheckpoint({
|
|
2329
|
+
save: async () => {
|
|
2330
|
+
await flushPendingMessages(false);
|
|
2331
|
+
record.lastUsedAt = isoNow();
|
|
2332
|
+
applyConversation(record, conversation);
|
|
2333
|
+
record.acpx = acpxState;
|
|
2334
|
+
await preserveClosedState();
|
|
2335
|
+
await eventWriter.checkpoint();
|
|
2336
|
+
},
|
|
2337
|
+
onError: (error) => {
|
|
2338
|
+
if (options.verbose) process.stderr.write("[acpx] live session checkpoint failed: " + formatErrorMessage(error) + "\n");
|
|
2339
|
+
}
|
|
2340
|
+
});
|
|
2341
|
+
const ownClient = options.client == null;
|
|
2342
|
+
const client = options.client ?? new AcpClient({
|
|
2343
|
+
agentCommand: record.agentCommand,
|
|
2344
|
+
cwd: absolutePath(record.cwd),
|
|
2345
|
+
mcpServers: options.mcpServers,
|
|
2346
|
+
permissionMode: options.permissionMode,
|
|
2347
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2348
|
+
permissionPolicy: options.permissionPolicy,
|
|
2349
|
+
authCredentials: options.authCredentials,
|
|
2350
|
+
authPolicy: options.authPolicy,
|
|
2351
|
+
terminal: options.terminal,
|
|
2352
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2353
|
+
verbose: options.verbose,
|
|
2354
|
+
sessionOptions
|
|
2355
|
+
});
|
|
2356
|
+
client.updateRuntimeOptions({
|
|
2357
|
+
permissionMode: options.permissionMode,
|
|
2358
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2359
|
+
permissionPolicy: options.permissionPolicy,
|
|
2360
|
+
terminal: options.terminal,
|
|
2361
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2362
|
+
verbose: options.verbose
|
|
2363
|
+
});
|
|
2364
|
+
client.setEventHandlers({
|
|
2365
|
+
onAcpMessage: (direction, message) => {
|
|
2366
|
+
sawAcpMessage = true;
|
|
2367
|
+
pendingMessages.push(message);
|
|
2368
|
+
options.onAcpMessage?.(direction, message);
|
|
2369
|
+
},
|
|
2370
|
+
onAcpOutputMessage: (_direction, message) => {
|
|
2371
|
+
if (bufferingConnectOutput) {
|
|
2372
|
+
pendingConnectOutputMessages.push(message);
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
output.onAcpMessage(message);
|
|
2376
|
+
},
|
|
2377
|
+
onSessionUpdate: (notification) => {
|
|
2378
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
2379
|
+
acpxState = recordSessionUpdate(conversation, acpxState, notification);
|
|
2380
|
+
trimConversationForRuntime(conversation);
|
|
2381
|
+
liveCheckpoint.request();
|
|
2382
|
+
options.onSessionUpdate?.(notification);
|
|
2383
|
+
},
|
|
2384
|
+
onClientOperation: (operation) => {
|
|
2385
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
2386
|
+
acpxState = recordClientOperation(conversation, acpxState, operation);
|
|
2387
|
+
trimConversationForRuntime(conversation);
|
|
2388
|
+
liveCheckpoint.request();
|
|
2389
|
+
options.onClientOperation?.(operation);
|
|
2390
|
+
},
|
|
2391
|
+
onPermissionEscalation: (event) => {
|
|
2392
|
+
output.onPermissionEscalation(event);
|
|
2393
|
+
options.onPermissionEscalation?.(event);
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
let activeSessionIdForControl = record.acpSessionId;
|
|
2397
|
+
let notifiedClientAvailable = false;
|
|
2398
|
+
const activeController = {
|
|
2399
|
+
hasActivePrompt: () => client.hasActivePrompt(),
|
|
2400
|
+
requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
|
|
2401
|
+
setSessionMode: async (modeId) => {
|
|
2402
|
+
await client.setSessionMode(activeSessionIdForControl, modeId);
|
|
2403
|
+
},
|
|
2404
|
+
setSessionModel: async (modelId) => {
|
|
2405
|
+
await client.setSessionModel(activeSessionIdForControl, modelId);
|
|
2406
|
+
},
|
|
2407
|
+
setSessionConfigOption: async (configId, value) => {
|
|
2408
|
+
return await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
try {
|
|
2412
|
+
return await withInterrupt(async () => {
|
|
2413
|
+
const connectStartedAt = Date.now();
|
|
2414
|
+
const { sessionId: activeSessionId, resumed, loadError } = await measurePerf("runtime.connect_and_load", async () => {
|
|
2415
|
+
try {
|
|
2416
|
+
return await connectAndLoadSession({
|
|
2417
|
+
client,
|
|
2418
|
+
record,
|
|
2419
|
+
resumePolicy: options.resumePolicy,
|
|
2420
|
+
timeoutMs: options.timeoutMs,
|
|
2421
|
+
verbose: options.verbose,
|
|
2422
|
+
activeController,
|
|
2423
|
+
onClientAvailable: (controller) => {
|
|
2424
|
+
options.onClientAvailable?.(controller);
|
|
2425
|
+
notifiedClientAvailable = true;
|
|
2426
|
+
},
|
|
2427
|
+
onConnectedRecord: (connectedRecord) => {
|
|
2428
|
+
connectedRecord.lastPromptAt = isoNow();
|
|
2429
|
+
},
|
|
2430
|
+
onSessionIdResolved: (sessionId) => {
|
|
2431
|
+
activeSessionIdForControl = sessionId;
|
|
2432
|
+
}
|
|
2433
|
+
});
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
bufferingConnectOutput = false;
|
|
2436
|
+
for (const message of pendingConnectOutputMessages) output.onAcpMessage(message);
|
|
2437
|
+
pendingConnectOutputMessages.length = 0;
|
|
2438
|
+
throw error;
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
bufferingConnectOutput = false;
|
|
2442
|
+
const connectOutputMessages = loadError == null ? pendingConnectOutputMessages : filterRecoverableLoadFallbackOutput(pendingConnectOutputMessages);
|
|
2443
|
+
for (const message of connectOutputMessages) output.onAcpMessage(message);
|
|
2444
|
+
pendingConnectOutputMessages.length = 0;
|
|
2445
|
+
if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.connect_and_load", Date.now() - connectStartedAt)}\n`);
|
|
2446
|
+
await applyPromptModelIfAdvertised({
|
|
2447
|
+
client,
|
|
2448
|
+
sessionId: activeSessionId,
|
|
2449
|
+
requestedModel: sessionOptions?.model,
|
|
2450
|
+
record,
|
|
2451
|
+
timeoutMs: options.timeoutMs
|
|
2452
|
+
});
|
|
2453
|
+
output.setContext({ sessionId: record.acpxRecordId });
|
|
2454
|
+
await liveCheckpoint.checkpoint();
|
|
2455
|
+
const maxRetries = options.promptRetries ?? 0;
|
|
2456
|
+
let response;
|
|
2457
|
+
promptTurnActive = true;
|
|
2458
|
+
for (let attempt = 0;; attempt++) try {
|
|
2459
|
+
const promptStartedAt = Date.now();
|
|
2460
|
+
response = await measurePerf("runtime.prompt.agent_turn", async () => {
|
|
2461
|
+
return await runPromptTurn({
|
|
2462
|
+
client,
|
|
2463
|
+
sessionId: activeSessionId,
|
|
2464
|
+
prompt: options.prompt,
|
|
2465
|
+
timeoutMs: options.timeoutMs,
|
|
2466
|
+
conversation,
|
|
2467
|
+
promptMessageId,
|
|
2468
|
+
onPromptStarted: attempt === 0 && options.onPromptActive ? async () => {
|
|
2469
|
+
try {
|
|
2470
|
+
await options.onPromptActive?.();
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
if (options.verbose) process.stderr.write("[acpx] onPromptActive hook failed: " + formatErrorMessage(error) + "\n");
|
|
2473
|
+
}
|
|
2474
|
+
} : void 0
|
|
2475
|
+
});
|
|
2476
|
+
});
|
|
2477
|
+
if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.agent_turn", Date.now() - promptStartedAt)}\n`);
|
|
2478
|
+
break;
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
const snapshot = client.getAgentLifecycleSnapshot();
|
|
2481
|
+
const agentCrashed = snapshot.lastExit?.unexpectedDuringPrompt === true;
|
|
2482
|
+
if (attempt < maxRetries && !agentCrashed && !promptTurnHadSideEffects && isRetryablePromptError(error)) {
|
|
2483
|
+
const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
2484
|
+
emitPromptRetryNotice({
|
|
2485
|
+
error,
|
|
2486
|
+
delayMs,
|
|
2487
|
+
attempt: attempt + 1,
|
|
2488
|
+
maxRetries,
|
|
2489
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors
|
|
2490
|
+
});
|
|
2491
|
+
await waitMs(delayMs);
|
|
2492
|
+
if (!promptTurnHadSideEffects) continue;
|
|
2493
|
+
}
|
|
2494
|
+
promptTurnActive = false;
|
|
2495
|
+
applyLifecycleSnapshotToRecord(record, snapshot);
|
|
2496
|
+
const lastExit = snapshot.lastExit;
|
|
2497
|
+
if (lastExit?.unexpectedDuringPrompt && options.verbose) process.stderr.write("[acpx] agent disconnected during prompt (" + lastExit.reason + ", exit=" + lastExit.exitCode + ", signal=" + (lastExit.signal ?? "none") + ")\n");
|
|
2498
|
+
const normalizedError = normalizeOutputError(error, { origin: "runtime" });
|
|
2499
|
+
await flushPendingMessages(false).catch(() => {});
|
|
2500
|
+
output.flush();
|
|
2501
|
+
record.lastUsedAt = isoNow();
|
|
2502
|
+
applyConversation(record, conversation);
|
|
2503
|
+
record.acpx = acpxState;
|
|
2504
|
+
const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
|
|
2505
|
+
propagated.outputAlreadyEmitted = sawAcpMessage;
|
|
2506
|
+
propagated.normalizedOutputError = normalizedError;
|
|
2507
|
+
throw propagated;
|
|
2508
|
+
}
|
|
2509
|
+
promptTurnActive = false;
|
|
2510
|
+
await flushPendingMessages(false);
|
|
2511
|
+
output.flush();
|
|
2512
|
+
record.lastUsedAt = isoNow();
|
|
2513
|
+
record.closed = false;
|
|
2514
|
+
record.closedAt = void 0;
|
|
2515
|
+
record.protocolVersion = client.initializeResult?.protocolVersion;
|
|
2516
|
+
record.agentCapabilities = client.initializeResult?.agentCapabilities;
|
|
2517
|
+
applyConversation(record, conversation);
|
|
2518
|
+
record.acpx = acpxState;
|
|
2519
|
+
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
2520
|
+
stopTotalTimer();
|
|
2521
|
+
return {
|
|
2522
|
+
...toPromptResult(response.stopReason, record.acpxRecordId, client),
|
|
2523
|
+
record,
|
|
2524
|
+
resumed,
|
|
2525
|
+
loadError
|
|
2526
|
+
};
|
|
2527
|
+
}, async () => {
|
|
2528
|
+
await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
|
|
2529
|
+
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
2530
|
+
record.lastUsedAt = isoNow();
|
|
2531
|
+
applyConversation(record, conversation);
|
|
2532
|
+
record.acpx = acpxState;
|
|
2533
|
+
await flushPendingMessages(false).catch(() => {});
|
|
2534
|
+
if (ownClient) await client.close();
|
|
2535
|
+
});
|
|
2536
|
+
} finally {
|
|
2537
|
+
if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", stopTotalTimer())}\n`);
|
|
2538
|
+
else stopTotalTimer();
|
|
2539
|
+
if (notifiedClientAvailable) options.onClientClosed?.();
|
|
2540
|
+
client.clearEventHandlers();
|
|
2541
|
+
if (ownClient) await client.close();
|
|
2542
|
+
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
2543
|
+
applyConversation(record, conversation);
|
|
2544
|
+
record.acpx = acpxState;
|
|
2545
|
+
await liveCheckpoint.flush().catch(() => {});
|
|
2546
|
+
await flushPendingMessages(false).catch(() => {});
|
|
2547
|
+
await preserveClosedState().catch(() => {});
|
|
2548
|
+
await closeEventWriter(true).catch(() => {});
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
async function runOnce(options) {
|
|
2552
|
+
const output = options.outputFormatter;
|
|
2553
|
+
let promptTurnActive = false;
|
|
2554
|
+
let promptTurnHadSideEffects = false;
|
|
2555
|
+
const client = new AcpClient({
|
|
2556
|
+
agentCommand: options.agentCommand,
|
|
2557
|
+
cwd: absolutePath(options.cwd),
|
|
2558
|
+
mcpServers: options.mcpServers,
|
|
2559
|
+
permissionMode: options.permissionMode,
|
|
2560
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2561
|
+
permissionPolicy: options.permissionPolicy,
|
|
2562
|
+
authCredentials: options.authCredentials,
|
|
2563
|
+
authPolicy: options.authPolicy,
|
|
2564
|
+
terminal: options.terminal,
|
|
2565
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2566
|
+
verbose: options.verbose,
|
|
2567
|
+
onAcpMessage: options.onAcpMessage,
|
|
2568
|
+
onAcpOutputMessage: (_direction, message) => output.onAcpMessage(message),
|
|
2569
|
+
onSessionUpdate: (notification) => {
|
|
2570
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
2571
|
+
options.onSessionUpdate?.(notification);
|
|
2572
|
+
},
|
|
2573
|
+
onClientOperation: (operation) => {
|
|
2574
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
2575
|
+
options.onClientOperation?.(operation);
|
|
2576
|
+
},
|
|
2577
|
+
onPermissionEscalation: (event) => {
|
|
2578
|
+
output.onPermissionEscalation(event);
|
|
2579
|
+
options.onPermissionEscalation?.(event);
|
|
2580
|
+
},
|
|
2581
|
+
sessionOptions: options.sessionOptions
|
|
2582
|
+
});
|
|
2583
|
+
try {
|
|
2584
|
+
return await withInterrupt(async () => {
|
|
2585
|
+
await measurePerf("runtime.exec.start", async () => {
|
|
2586
|
+
await withTimeout(client.start(), options.timeoutMs);
|
|
2587
|
+
});
|
|
2588
|
+
const createdSession = await measurePerf("runtime.exec.create_session", async () => {
|
|
2589
|
+
return await withTimeout(client.createSession(absolutePath(options.cwd)), options.timeoutMs);
|
|
2590
|
+
});
|
|
2591
|
+
const sessionId = createdSession.sessionId;
|
|
2592
|
+
await applyRequestedModelIfAdvertised({
|
|
2593
|
+
client,
|
|
2594
|
+
sessionId,
|
|
2595
|
+
requestedModel: options.sessionOptions?.model,
|
|
2596
|
+
models: createdSession.models,
|
|
2597
|
+
agentCommand: options.agentCommand,
|
|
2598
|
+
timeoutMs: options.timeoutMs
|
|
2599
|
+
});
|
|
2600
|
+
output.setContext({ sessionId });
|
|
2601
|
+
const maxRetries = options.promptRetries ?? 0;
|
|
2602
|
+
let response;
|
|
2603
|
+
promptTurnActive = true;
|
|
2604
|
+
for (let attempt = 0;; attempt++) try {
|
|
2605
|
+
response = await measurePerf("runtime.exec.prompt", async () => {
|
|
2606
|
+
return await withTimeout(client.prompt(sessionId, options.prompt), options.timeoutMs);
|
|
2607
|
+
});
|
|
2608
|
+
break;
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
if (attempt < maxRetries && !promptTurnHadSideEffects && isRetryablePromptError(error)) {
|
|
2611
|
+
const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
2612
|
+
emitPromptRetryNotice({
|
|
2613
|
+
error,
|
|
2614
|
+
delayMs,
|
|
2615
|
+
attempt: attempt + 1,
|
|
2616
|
+
maxRetries,
|
|
2617
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors
|
|
2618
|
+
});
|
|
2619
|
+
await waitMs(delayMs);
|
|
2620
|
+
if (!promptTurnHadSideEffects) continue;
|
|
2621
|
+
}
|
|
2622
|
+
promptTurnActive = false;
|
|
2623
|
+
throw error;
|
|
2624
|
+
}
|
|
2625
|
+
promptTurnActive = false;
|
|
2626
|
+
output.flush();
|
|
2627
|
+
return toPromptResult(response.stopReason, sessionId, client);
|
|
2628
|
+
}, async () => {
|
|
2629
|
+
await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
|
|
2630
|
+
await client.close();
|
|
2631
|
+
});
|
|
2632
|
+
} finally {
|
|
2633
|
+
await client.close();
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
async function sendSessionDirect(options) {
|
|
2637
|
+
return await runSessionPrompt({
|
|
2638
|
+
sessionRecordId: options.sessionId,
|
|
2639
|
+
prompt: options.prompt,
|
|
2640
|
+
mcpServers: options.mcpServers,
|
|
2641
|
+
permissionMode: options.permissionMode,
|
|
2642
|
+
resumePolicy: options.resumePolicy,
|
|
2643
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2644
|
+
permissionPolicy: options.permissionPolicy,
|
|
2645
|
+
authCredentials: options.authCredentials,
|
|
2646
|
+
authPolicy: options.authPolicy,
|
|
2647
|
+
terminal: options.terminal,
|
|
2648
|
+
outputFormatter: options.outputFormatter,
|
|
2649
|
+
onAcpMessage: options.onAcpMessage,
|
|
2650
|
+
onSessionUpdate: options.onSessionUpdate,
|
|
2651
|
+
onClientOperation: options.onClientOperation,
|
|
2652
|
+
onPermissionEscalation: options.onPermissionEscalation,
|
|
2653
|
+
timeoutMs: options.timeoutMs,
|
|
2654
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2655
|
+
verbose: options.verbose,
|
|
2656
|
+
client: options.client
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2659
|
+
//#endregion
|
|
2660
|
+
//#region src/cli/session/queue-owner-runtime.ts
|
|
2661
|
+
const QUEUE_OWNER_STARTUP_MAX_ATTEMPTS = 120;
|
|
2662
|
+
const QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2663
|
+
async function submitToRunningOwner(options, waitForCompletion) {
|
|
2664
|
+
return await trySubmitToRunningOwner({
|
|
2665
|
+
sessionId: options.sessionId,
|
|
2666
|
+
message: promptToDisplayText(options.prompt),
|
|
2667
|
+
prompt: options.prompt,
|
|
2668
|
+
permissionMode: options.permissionMode,
|
|
2669
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2670
|
+
permissionPolicy: options.permissionPolicy,
|
|
2671
|
+
outputFormatter: options.outputFormatter,
|
|
2672
|
+
errorEmissionPolicy: options.errorEmissionPolicy,
|
|
2673
|
+
timeoutMs: options.timeoutMs,
|
|
2674
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2675
|
+
promptRetries: options.promptRetries,
|
|
2676
|
+
waitForCompletion,
|
|
2677
|
+
verbose: options.verbose,
|
|
2678
|
+
sessionOptions: options.sessionOptions
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
async function runSessionQueueOwner(options) {
|
|
2682
|
+
const lease = await tryAcquireQueueOwnerLease(options.sessionId);
|
|
2683
|
+
if (!lease) return;
|
|
2684
|
+
const sessionRecord = await resolveSessionRecord(options.sessionId);
|
|
2685
|
+
let owner;
|
|
2686
|
+
let heartbeatTimer;
|
|
2687
|
+
const sharedClient = new AcpClient({
|
|
2688
|
+
agentCommand: sessionRecord.agentCommand,
|
|
2689
|
+
cwd: absolutePath(sessionRecord.cwd),
|
|
2690
|
+
mcpServers: options.mcpServers,
|
|
2691
|
+
permissionMode: options.permissionMode,
|
|
2692
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2693
|
+
authCredentials: options.authCredentials,
|
|
2694
|
+
authPolicy: options.authPolicy,
|
|
2695
|
+
terminal: options.terminal,
|
|
2696
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2697
|
+
verbose: options.verbose,
|
|
2698
|
+
sessionOptions: mergeSessionOptions(options.sessionOptions, sessionOptionsFromRecord(sessionRecord))
|
|
2699
|
+
});
|
|
2700
|
+
const ttlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
|
|
2701
|
+
const maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth ?? 16));
|
|
2702
|
+
const taskPollTimeoutMs = ttlMs === 0 ? void 0 : ttlMs;
|
|
2703
|
+
const initialTaskPollTimeoutMs = taskPollTimeoutMs == null ? void 0 : Math.max(taskPollTimeoutMs, 1e3);
|
|
2704
|
+
const turnController = new QueueOwnerTurnController({
|
|
2705
|
+
withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
|
|
2706
|
+
setSessionModeFallback: async (modeId, timeoutMs) => {
|
|
2707
|
+
await runSessionSetModeDirect({
|
|
2708
|
+
sessionRecordId: options.sessionId,
|
|
2709
|
+
modeId,
|
|
2710
|
+
mcpServers: options.mcpServers,
|
|
2711
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2712
|
+
authCredentials: options.authCredentials,
|
|
2713
|
+
authPolicy: options.authPolicy,
|
|
2714
|
+
terminal: options.terminal,
|
|
2715
|
+
timeoutMs,
|
|
2716
|
+
verbose: options.verbose
|
|
2717
|
+
});
|
|
2718
|
+
},
|
|
2719
|
+
setSessionModelFallback: async (modelId, timeoutMs) => {
|
|
2720
|
+
await runSessionSetModelDirect({
|
|
2721
|
+
sessionRecordId: options.sessionId,
|
|
2722
|
+
modelId,
|
|
2723
|
+
mcpServers: options.mcpServers,
|
|
2724
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2725
|
+
authCredentials: options.authCredentials,
|
|
2726
|
+
authPolicy: options.authPolicy,
|
|
2727
|
+
terminal: options.terminal,
|
|
2728
|
+
timeoutMs,
|
|
2729
|
+
verbose: options.verbose
|
|
2730
|
+
});
|
|
2731
|
+
},
|
|
2732
|
+
setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
|
|
2733
|
+
return (await runSessionSetConfigOptionDirect({
|
|
2734
|
+
sessionRecordId: options.sessionId,
|
|
2735
|
+
configId,
|
|
2736
|
+
value,
|
|
2737
|
+
mcpServers: options.mcpServers,
|
|
2738
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2739
|
+
authCredentials: options.authCredentials,
|
|
2740
|
+
authPolicy: options.authPolicy,
|
|
2741
|
+
terminal: options.terminal,
|
|
2742
|
+
timeoutMs,
|
|
2743
|
+
verbose: options.verbose
|
|
2744
|
+
})).response;
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
const applyPendingCancel = async () => {
|
|
2748
|
+
return await turnController.applyPendingCancel();
|
|
2749
|
+
};
|
|
2750
|
+
const scheduleApplyPendingCancel = () => {
|
|
2751
|
+
applyPendingCancel().catch((error) => {
|
|
2752
|
+
if (options.verbose) process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
|
|
2753
|
+
});
|
|
2754
|
+
};
|
|
2755
|
+
const setActiveController = (controller) => {
|
|
2756
|
+
turnController.setActiveController(controller);
|
|
2757
|
+
scheduleApplyPendingCancel();
|
|
2758
|
+
};
|
|
2759
|
+
const clearActiveController = () => {
|
|
2760
|
+
turnController.clearActiveController();
|
|
2761
|
+
};
|
|
2762
|
+
const closeActiveBackendSession = async (timeoutMs) => {
|
|
2763
|
+
const latestRecord = await resolveSessionRecord(options.sessionId);
|
|
2764
|
+
if (!sharedClient.supportsCloseSession()) return false;
|
|
2765
|
+
await withTimeout(sharedClient.closeSession(latestRecord.acpSessionId), timeoutMs);
|
|
2766
|
+
return true;
|
|
2767
|
+
};
|
|
2768
|
+
const runPromptTurn = async (run) => {
|
|
2769
|
+
turnController.beginTurn();
|
|
2770
|
+
try {
|
|
2771
|
+
return await run();
|
|
2772
|
+
} finally {
|
|
2773
|
+
turnController.endTurn();
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
try {
|
|
2777
|
+
owner = await SessionQueueOwner.start(lease, {
|
|
2778
|
+
cancelPrompt: async () => {
|
|
2779
|
+
if (!await turnController.requestCancel()) return false;
|
|
2780
|
+
await applyPendingCancel();
|
|
2781
|
+
return true;
|
|
2782
|
+
},
|
|
2783
|
+
closeSession: async (timeoutMs) => await closeActiveBackendSession(timeoutMs),
|
|
2784
|
+
setSessionMode: async (modeId, timeoutMs) => {
|
|
2785
|
+
await turnController.setSessionMode(modeId, timeoutMs);
|
|
2786
|
+
},
|
|
2787
|
+
setSessionModel: async (modelId, timeoutMs) => {
|
|
2788
|
+
await turnController.setSessionModel(modelId, timeoutMs);
|
|
2789
|
+
},
|
|
2790
|
+
setSessionConfigOption: async (configId, value, timeoutMs) => {
|
|
2791
|
+
return await turnController.setSessionConfigOption(configId, value, timeoutMs);
|
|
2792
|
+
}
|
|
2793
|
+
}, {
|
|
2794
|
+
maxQueueDepth,
|
|
2795
|
+
onQueueDepthChanged: (queueDepth) => {
|
|
2796
|
+
setPerfGauge("queue.owner.depth", queueDepth);
|
|
2797
|
+
refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
if (options.verbose) process.stderr.write(`[acpx] queue owner ready for session ${options.sessionId} (ttlMs=${ttlMs}, maxQueueDepth=${maxQueueDepth})\n`);
|
|
2801
|
+
await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
|
|
2802
|
+
heartbeatTimer = setInterval(() => {
|
|
2803
|
+
refreshQueueOwnerLease(lease, { queueDepth: owner?.queueDepth() ?? 0 }).catch(() => {});
|
|
2804
|
+
}, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
|
|
2805
|
+
let isFirstTask = true;
|
|
2806
|
+
while (true) {
|
|
2807
|
+
const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
|
|
2808
|
+
const task = await owner.nextTask(pollTimeoutMs);
|
|
2809
|
+
if (!task) break;
|
|
2810
|
+
isFirstTask = false;
|
|
2811
|
+
await runPromptTurn(async () => {
|
|
2812
|
+
try {
|
|
2813
|
+
await runQueuedTask(options.sessionId, task, {
|
|
2814
|
+
sharedClient,
|
|
2815
|
+
verbose: options.verbose,
|
|
2816
|
+
mcpServers: options.mcpServers,
|
|
2817
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
2818
|
+
authCredentials: options.authCredentials,
|
|
2819
|
+
authPolicy: options.authPolicy,
|
|
2820
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
2821
|
+
promptRetries: task.promptRetries ?? 0,
|
|
2822
|
+
sessionOptions: options.sessionOptions,
|
|
2823
|
+
onClientAvailable: setActiveController,
|
|
2824
|
+
onClientClosed: clearActiveController,
|
|
2825
|
+
onPromptActive: async () => {
|
|
2826
|
+
turnController.markPromptActive();
|
|
2827
|
+
await applyPendingCancel();
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
} finally {
|
|
2831
|
+
checkpointPerfMetricsCapture();
|
|
2832
|
+
}
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
} finally {
|
|
2836
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
2837
|
+
turnController.beginClosing();
|
|
2838
|
+
if (owner) await owner.close();
|
|
2839
|
+
await sharedClient.close().catch(() => {});
|
|
2840
|
+
try {
|
|
2841
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
2842
|
+
applyLifecycleSnapshotToRecord(record, sharedClient.getAgentLifecycleSnapshot());
|
|
2843
|
+
await writeSessionRecord(record);
|
|
2844
|
+
} catch {}
|
|
2845
|
+
await releaseQueueOwnerLease(lease);
|
|
2846
|
+
if (options.verbose) process.stderr.write(`[acpx] queue owner stopped for session ${options.sessionId}\n`);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
async function sendSession(options) {
|
|
2850
|
+
const waitForCompletion = options.waitForCompletion !== false;
|
|
2851
|
+
const queuedToOwner = await submitToRunningOwner(options, waitForCompletion);
|
|
2852
|
+
if (queuedToOwner) return queuedToOwner;
|
|
2853
|
+
spawnQueueOwnerProcess(queueOwnerRuntimeOptionsFromSend(options));
|
|
2854
|
+
for (let attempt = 0; attempt < QUEUE_OWNER_STARTUP_MAX_ATTEMPTS; attempt += 1) {
|
|
2855
|
+
const queued = await submitToRunningOwner(options, waitForCompletion);
|
|
2856
|
+
if (queued) return queued;
|
|
2857
|
+
await waitMs(50);
|
|
2858
|
+
}
|
|
2859
|
+
throw new Error(`Session queue owner failed to start for session ${options.sessionId}`);
|
|
2860
|
+
}
|
|
2861
|
+
//#endregion
|
|
2862
|
+
//#region src/session/session.ts
|
|
2863
|
+
var session_exports = /* @__PURE__ */ __exportAll({
|
|
2864
|
+
DEFAULT_HISTORY_LIMIT: () => 20,
|
|
2865
|
+
DEFAULT_QUEUE_OWNER_TTL_MS: () => DEFAULT_QUEUE_OWNER_TTL_MS,
|
|
2866
|
+
InterruptedError: () => InterruptedError,
|
|
2867
|
+
TimeoutError: () => TimeoutError,
|
|
2868
|
+
cancelSessionPrompt: () => cancelSessionPrompt,
|
|
2869
|
+
closeSession: () => closeSession,
|
|
2870
|
+
createSession: () => createSession,
|
|
2871
|
+
createSessionWithClient: () => createSessionWithClient,
|
|
2872
|
+
ensureSession: () => ensureSession,
|
|
2873
|
+
findGitRepositoryRoot: () => findGitRepositoryRoot,
|
|
2874
|
+
findSession: () => findSession,
|
|
2875
|
+
findSessionByDirectoryWalk: () => findSessionByDirectoryWalk,
|
|
2876
|
+
isProcessAlive: () => isProcessAlive,
|
|
2877
|
+
listSessions: () => listSessions,
|
|
2878
|
+
listSessionsForAgent: () => listSessionsForAgent,
|
|
2879
|
+
normalizeQueueOwnerTtlMs: () => normalizeQueueOwnerTtlMs,
|
|
2880
|
+
pruneSessions: () => pruneSessions,
|
|
2881
|
+
runOnce: () => runOnce,
|
|
2882
|
+
runQueuedTask: () => runQueuedTask,
|
|
2883
|
+
runSessionQueueOwner: () => runSessionQueueOwner,
|
|
2884
|
+
sendSession: () => sendSession,
|
|
2885
|
+
sendSessionDirect: () => sendSessionDirect,
|
|
2886
|
+
setSessionConfigOption: () => setSessionConfigOption,
|
|
2887
|
+
setSessionMode: () => setSessionMode,
|
|
2888
|
+
setSessionModel: () => setSessionModel
|
|
2889
|
+
});
|
|
2890
|
+
//#endregion
|
|
2891
|
+
//#region src/acp/jsonrpc-error.ts
|
|
2892
|
+
const OUTPUT_ERROR_JSONRPC_CODES = {
|
|
2893
|
+
NO_SESSION: -32002,
|
|
2894
|
+
TIMEOUT: -32070,
|
|
2895
|
+
PERMISSION_DENIED: -32071,
|
|
2896
|
+
PERMISSION_PROMPT_UNAVAILABLE: -32072,
|
|
2897
|
+
RUNTIME: -32603,
|
|
2898
|
+
USAGE: -32602
|
|
2899
|
+
};
|
|
2900
|
+
function hasValidAcpError(acp) {
|
|
2901
|
+
return Boolean(acp && Number.isFinite(acp.code) && typeof acp.message === "string" && acp.message.trim().length > 0);
|
|
2902
|
+
}
|
|
2903
|
+
function buildFallbackData(params) {
|
|
2904
|
+
const data = {
|
|
2905
|
+
acpxCode: params.outputCode,
|
|
2906
|
+
detailCode: params.detailCode,
|
|
2907
|
+
origin: params.origin,
|
|
2908
|
+
retryable: params.retryable,
|
|
2909
|
+
timestamp: params.timestamp,
|
|
2910
|
+
sessionId: params.sessionId
|
|
2911
|
+
};
|
|
2912
|
+
for (const [key, value] of Object.entries(data)) if (value === void 0) delete data[key];
|
|
2913
|
+
return data;
|
|
2914
|
+
}
|
|
2915
|
+
function mergeAcpErrorData(acpData, fallbackData) {
|
|
2916
|
+
if (Object.keys(fallbackData).length === 0) return acpData;
|
|
2917
|
+
if (acpData === void 0) return fallbackData;
|
|
2918
|
+
if (acpData && typeof acpData === "object" && !Array.isArray(acpData)) return {
|
|
2919
|
+
...fallbackData,
|
|
2920
|
+
...acpData
|
|
2921
|
+
};
|
|
2922
|
+
return acpData;
|
|
2923
|
+
}
|
|
2924
|
+
function buildErrorObject(params) {
|
|
2925
|
+
const fallbackData = buildFallbackData(params);
|
|
2926
|
+
if (hasValidAcpError(params.acp)) {
|
|
2927
|
+
const data = mergeAcpErrorData(params.acp.data, fallbackData);
|
|
2928
|
+
return {
|
|
2929
|
+
code: params.acp.code,
|
|
2930
|
+
message: params.acp.message,
|
|
2931
|
+
...data !== void 0 ? { data } : {}
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
const data = fallbackData;
|
|
2935
|
+
return {
|
|
2936
|
+
code: OUTPUT_ERROR_JSONRPC_CODES[params.outputCode] ?? -32603,
|
|
2937
|
+
message: params.message,
|
|
2938
|
+
...Object.keys(data).length > 0 ? { data } : {}
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
function buildJsonRpcErrorResponse(params) {
|
|
2942
|
+
return {
|
|
2943
|
+
jsonrpc: "2.0",
|
|
2944
|
+
id: params.id ?? null,
|
|
2945
|
+
error: buildErrorObject(params)
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
//#endregion
|
|
2949
|
+
//#region src/cli/output/read-suppression.ts
|
|
2950
|
+
const SUPPRESSED_READ_OUTPUT = "[read output suppressed]";
|
|
2951
|
+
function inferToolKindFromTitle(title) {
|
|
2952
|
+
const normalized = title?.trim().toLowerCase();
|
|
2953
|
+
if (!normalized) return;
|
|
2954
|
+
const head = normalized.split(":", 1)[0]?.trim();
|
|
2955
|
+
if (!head) return;
|
|
2956
|
+
if (head.includes("read") || head.includes("cat") || head.includes("open") || head.includes("view")) return "read";
|
|
2957
|
+
}
|
|
2958
|
+
function isReadLikeTool(tool) {
|
|
2959
|
+
return tool.kind?.trim().toLowerCase() === "read" || inferToolKindFromTitle(tool.title) === "read";
|
|
2960
|
+
}
|
|
2961
|
+
//#endregion
|
|
2962
|
+
//#region src/cli/output/json-formatter.ts
|
|
2963
|
+
const DEFAULT_JSON_SESSION_ID = "unknown";
|
|
2964
|
+
function asRecord$1(value) {
|
|
2965
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
2966
|
+
return value;
|
|
2967
|
+
}
|
|
2968
|
+
function jsonRpcIdKey(value) {
|
|
2969
|
+
if (typeof value === "string") return `s:${value}`;
|
|
2970
|
+
if (typeof value === "number" && Number.isFinite(value)) return `n:${value}`;
|
|
2971
|
+
}
|
|
2972
|
+
function sanitizeReadResult(result) {
|
|
2973
|
+
const record = asRecord$1(result);
|
|
2974
|
+
if (!record || typeof record.content !== "string") return result;
|
|
2975
|
+
return {
|
|
2976
|
+
...record,
|
|
2977
|
+
content: SUPPRESSED_READ_OUTPUT
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
function sanitizeToolContent(content) {
|
|
2981
|
+
if (!Array.isArray(content)) return content;
|
|
2982
|
+
return [{
|
|
2983
|
+
type: "content",
|
|
2984
|
+
content: {
|
|
2985
|
+
type: "text",
|
|
2986
|
+
text: SUPPRESSED_READ_OUTPUT
|
|
2987
|
+
}
|
|
2988
|
+
}];
|
|
2989
|
+
}
|
|
2990
|
+
function sanitizeToolMessage(message) {
|
|
2991
|
+
const root = asRecord$1(message);
|
|
2992
|
+
const params = asRecord$1(root?.params);
|
|
2993
|
+
const update = asRecord$1(params?.update);
|
|
2994
|
+
if (!root || !params || !update) return message;
|
|
2995
|
+
return {
|
|
2996
|
+
...root,
|
|
2997
|
+
params: {
|
|
2998
|
+
...params,
|
|
2999
|
+
update: {
|
|
3000
|
+
...update,
|
|
3001
|
+
rawOutput: Object.prototype.hasOwnProperty.call(update, "rawOutput") && update.rawOutput !== void 0 ? { content: SUPPRESSED_READ_OUTPUT } : update.rawOutput,
|
|
3002
|
+
content: Object.prototype.hasOwnProperty.call(update, "content") && update.content !== void 0 ? sanitizeToolContent(update.content) : update.content
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
var JsonOutputFormatter = class {
|
|
3008
|
+
stdout;
|
|
3009
|
+
suppressReads;
|
|
3010
|
+
sessionId;
|
|
3011
|
+
requestMethodById = /* @__PURE__ */ new Map();
|
|
3012
|
+
toolStateById = /* @__PURE__ */ new Map();
|
|
3013
|
+
constructor(stdout, suppressReads, context) {
|
|
3014
|
+
this.stdout = stdout;
|
|
3015
|
+
this.suppressReads = suppressReads;
|
|
3016
|
+
this.sessionId = context?.sessionId?.trim() || DEFAULT_JSON_SESSION_ID;
|
|
3017
|
+
}
|
|
3018
|
+
setContext(context) {
|
|
3019
|
+
this.sessionId = context.sessionId?.trim() || this.sessionId || DEFAULT_JSON_SESSION_ID;
|
|
3020
|
+
}
|
|
3021
|
+
onAcpMessage(message) {
|
|
3022
|
+
this.stdout.write(`${JSON.stringify(this.sanitizeMessage(message))}\n`);
|
|
3023
|
+
}
|
|
3024
|
+
sanitizeMessage(message) {
|
|
3025
|
+
if (!this.suppressReads) return message;
|
|
3026
|
+
const sanitizedResponse = this.sanitizeReadResponse(message);
|
|
3027
|
+
if (sanitizedResponse !== message) return sanitizedResponse;
|
|
3028
|
+
const sanitizedToolMessage = this.sanitizeReadToolMessage(message);
|
|
3029
|
+
if (sanitizedToolMessage !== message) return sanitizedToolMessage;
|
|
3030
|
+
this.trackRequestMethod(message);
|
|
3031
|
+
return message;
|
|
3032
|
+
}
|
|
3033
|
+
trackRequestMethod(message) {
|
|
3034
|
+
const candidate = message;
|
|
3035
|
+
if (typeof candidate.method !== "string") return;
|
|
3036
|
+
const idKey = jsonRpcIdKey(candidate.id);
|
|
3037
|
+
if (!idKey) return;
|
|
3038
|
+
this.requestMethodById.set(idKey, candidate.method);
|
|
3039
|
+
}
|
|
3040
|
+
sanitizeReadResponse(message) {
|
|
3041
|
+
const candidate = message;
|
|
3042
|
+
const idKey = jsonRpcIdKey(candidate.id);
|
|
3043
|
+
if (!idKey || !Object.hasOwn(candidate, "result")) return message;
|
|
3044
|
+
const method = this.requestMethodById.get(idKey);
|
|
3045
|
+
this.requestMethodById.delete(idKey);
|
|
3046
|
+
if (method !== "fs/read_text_file") return message;
|
|
3047
|
+
const root = asRecord$1(message);
|
|
3048
|
+
if (!root) return message;
|
|
3049
|
+
return {
|
|
3050
|
+
...root,
|
|
3051
|
+
result: sanitizeReadResult(candidate.result)
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
sanitizeReadToolMessage(message) {
|
|
3055
|
+
const root = asRecord$1(message);
|
|
3056
|
+
if (root?.method !== "session/update") return message;
|
|
3057
|
+
const params = asRecord$1(root.params);
|
|
3058
|
+
const update = asRecord$1(params?.update);
|
|
3059
|
+
if (!params || !update) return message;
|
|
3060
|
+
const sessionUpdate = update.sessionUpdate;
|
|
3061
|
+
if (sessionUpdate !== "tool_call" && sessionUpdate !== "tool_call_update") return message;
|
|
3062
|
+
const toolCallId = typeof update.toolCallId === "string" ? update.toolCallId : void 0;
|
|
3063
|
+
if (!toolCallId) return message;
|
|
3064
|
+
const previous = this.toolStateById.get(toolCallId) ?? {};
|
|
3065
|
+
const current = {
|
|
3066
|
+
title: typeof update.title === "string" ? update.title : previous.title,
|
|
3067
|
+
kind: typeof update.kind === "string" || update.kind === null ? update.kind : previous.kind
|
|
3068
|
+
};
|
|
3069
|
+
this.toolStateById.set(toolCallId, current);
|
|
3070
|
+
if (!isReadLikeTool(current)) return message;
|
|
3071
|
+
return sanitizeToolMessage(message);
|
|
3072
|
+
}
|
|
3073
|
+
onError(params) {
|
|
3074
|
+
this.stdout.write(`${JSON.stringify(buildJsonRpcErrorResponse({
|
|
3075
|
+
outputCode: params.code,
|
|
3076
|
+
detailCode: params.detailCode,
|
|
3077
|
+
origin: params.origin,
|
|
3078
|
+
message: params.message,
|
|
3079
|
+
retryable: params.retryable,
|
|
3080
|
+
timestamp: params.timestamp,
|
|
3081
|
+
sessionId: this.sessionId,
|
|
3082
|
+
acp: params.acp
|
|
3083
|
+
}))}\n`);
|
|
3084
|
+
}
|
|
3085
|
+
onPermissionEscalation() {}
|
|
3086
|
+
flush() {}
|
|
3087
|
+
};
|
|
3088
|
+
function createJsonOutputFormatter(stdout, suppressReads = false, context) {
|
|
3089
|
+
return new JsonOutputFormatter(stdout, suppressReads, context);
|
|
3090
|
+
}
|
|
3091
|
+
//#endregion
|
|
3092
|
+
//#region src/cli/output/output.ts
|
|
3093
|
+
var output_exports = /* @__PURE__ */ __exportAll({
|
|
3094
|
+
createOutputFormatter: () => createOutputFormatter,
|
|
3095
|
+
getTextErrorRemediationHints: () => getTextErrorRemediationHints
|
|
3096
|
+
});
|
|
3097
|
+
const MAX_THOUGHT_CHARS = 900;
|
|
3098
|
+
const MAX_INLINE_CHARS = 220;
|
|
3099
|
+
const MAX_OUTPUT_CHARS = 2e3;
|
|
3100
|
+
const MAX_OUTPUT_LINES = 28;
|
|
3101
|
+
const MAX_LOCATION_ITEMS = 5;
|
|
3102
|
+
const OUTPUT_PRIORITY_KEYS = [
|
|
3103
|
+
"stdout",
|
|
3104
|
+
"stderr",
|
|
3105
|
+
"output",
|
|
3106
|
+
"content",
|
|
3107
|
+
"text",
|
|
3108
|
+
"message",
|
|
3109
|
+
"result",
|
|
3110
|
+
"response",
|
|
3111
|
+
"value"
|
|
3112
|
+
];
|
|
3113
|
+
function asStatus(status) {
|
|
3114
|
+
return status ?? "unknown";
|
|
3115
|
+
}
|
|
3116
|
+
function isFinalStatus(status) {
|
|
3117
|
+
return status === "completed" || status === "failed";
|
|
3118
|
+
}
|
|
3119
|
+
function toStatusLabel(status) {
|
|
3120
|
+
switch (status) {
|
|
3121
|
+
case "in_progress": return "running";
|
|
3122
|
+
case "pending": return "pending";
|
|
3123
|
+
case "completed": return "completed";
|
|
3124
|
+
case "failed": return "failed";
|
|
3125
|
+
default: return "running";
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
function asRecord(value) {
|
|
3129
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
3130
|
+
return value;
|
|
3131
|
+
}
|
|
3132
|
+
function extractJsonRpcMethod(message) {
|
|
3133
|
+
return Object.hasOwn(message, "method") ? message.method?.toString() : void 0;
|
|
3134
|
+
}
|
|
3135
|
+
function collapseWhitespace(value) {
|
|
3136
|
+
return value.replace(/\s+/g, " ").trim();
|
|
3137
|
+
}
|
|
3138
|
+
function normalizeLineEndings(value) {
|
|
3139
|
+
return value.replace(/\r\n?/g, "\n");
|
|
3140
|
+
}
|
|
3141
|
+
function truncate(value, maxChars) {
|
|
3142
|
+
if (value.length <= maxChars) return value;
|
|
3143
|
+
if (maxChars <= 3) return value.slice(0, maxChars);
|
|
3144
|
+
return `${value.slice(0, maxChars - 3)}...`;
|
|
3145
|
+
}
|
|
3146
|
+
function toInline(value, maxChars = MAX_INLINE_CHARS) {
|
|
3147
|
+
return truncate(collapseWhitespace(value), maxChars);
|
|
3148
|
+
}
|
|
3149
|
+
function indentBlock(value, prefix) {
|
|
3150
|
+
return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
|
|
3151
|
+
}
|
|
3152
|
+
function dedupeStrings(values) {
|
|
3153
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3154
|
+
const result = [];
|
|
3155
|
+
for (const value of values) {
|
|
3156
|
+
if (seen.has(value)) continue;
|
|
3157
|
+
seen.add(value);
|
|
3158
|
+
result.push(value);
|
|
3159
|
+
}
|
|
3160
|
+
return result;
|
|
3161
|
+
}
|
|
3162
|
+
function safeJson(value, spacing) {
|
|
3163
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3164
|
+
try {
|
|
3165
|
+
return JSON.stringify(value, (_key, entry) => {
|
|
3166
|
+
if (typeof entry === "bigint") return `${entry}n`;
|
|
3167
|
+
if (typeof entry === "function") return `[Function ${entry.name || "anonymous"}]`;
|
|
3168
|
+
if (typeof entry === "symbol") return entry.toString();
|
|
3169
|
+
if (entry && typeof entry === "object") {
|
|
3170
|
+
if (seen.has(entry)) return "[Circular]";
|
|
3171
|
+
seen.add(entry);
|
|
3172
|
+
}
|
|
3173
|
+
return entry;
|
|
3174
|
+
}, spacing);
|
|
3175
|
+
} catch {
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
function readFirstString(source, keys) {
|
|
3180
|
+
for (const key of keys) {
|
|
3181
|
+
const value = source[key];
|
|
3182
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
function readFirstFiniteNumber(source, keys) {
|
|
3186
|
+
for (const key of keys) {
|
|
3187
|
+
const value = source[key];
|
|
3188
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
function formatMetadataNumber(value) {
|
|
3192
|
+
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(8)));
|
|
3193
|
+
}
|
|
3194
|
+
function readFirstStringArray(source, keys) {
|
|
3195
|
+
for (const key of keys) {
|
|
3196
|
+
const value = source[key];
|
|
3197
|
+
if (!Array.isArray(value)) continue;
|
|
3198
|
+
const entries = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter((entry) => entry.length > 0);
|
|
3199
|
+
if (entries.length > 0) return entries;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
function formatDisjunction(values) {
|
|
3203
|
+
if (values.length <= 1) return values[0] ?? "";
|
|
3204
|
+
if (values.length === 2) return `${values[0]} or ${values[1]}`;
|
|
3205
|
+
return `${values.slice(0, -1).join(", ")}, or ${values.at(-1)}`;
|
|
3206
|
+
}
|
|
3207
|
+
function parseAuthMethodIdsFromMessage(message) {
|
|
3208
|
+
const methods = [];
|
|
3209
|
+
const methodListMatch = message.match(/auth methods \[([^\]]+)\]/iu);
|
|
3210
|
+
if (methodListMatch) methods.push(...methodListMatch[1].split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0));
|
|
3211
|
+
const singleMethodMatch = message.match(/auth method ([\w.-]+)/iu);
|
|
3212
|
+
if (singleMethodMatch) methods.push(singleMethodMatch[1]);
|
|
3213
|
+
return dedupeStrings(methods);
|
|
3214
|
+
}
|
|
3215
|
+
function parseAuthMethodIdsFromAcpData(data) {
|
|
3216
|
+
const record = asRecord(data);
|
|
3217
|
+
if (!record) return [];
|
|
3218
|
+
const methodIds = [];
|
|
3219
|
+
if (typeof record.methodId === "string" && record.methodId.trim().length > 0) methodIds.push(record.methodId.trim());
|
|
3220
|
+
if (Array.isArray(record.methods)) for (const entry of record.methods) {
|
|
3221
|
+
if (typeof entry === "string" && entry.trim().length > 0) {
|
|
3222
|
+
methodIds.push(entry.trim());
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
const id = asRecord(entry)?.id;
|
|
3226
|
+
if (typeof id === "string" && id.trim().length > 0) methodIds.push(id.trim());
|
|
3227
|
+
}
|
|
3228
|
+
return dedupeStrings(methodIds);
|
|
3229
|
+
}
|
|
3230
|
+
function renderAuthRequiredHint(params) {
|
|
3231
|
+
const methodIds = dedupeStrings([...parseAuthMethodIdsFromAcpData(params.acp?.data), ...parseAuthMethodIdsFromMessage(params.message)]);
|
|
3232
|
+
if (methodIds.length === 0) return "hint: run `acpx config show` to locate the active config, then add the required credential under `auth` and retry.";
|
|
3233
|
+
return `hint: run \`acpx config show\` to locate the active config, then add ${formatDisjunction(methodIds.map((methodId) => `\`auth.${methodId}\``))} and retry.`;
|
|
3234
|
+
}
|
|
3235
|
+
function getTextErrorRemediationHints(params) {
|
|
3236
|
+
const lowerMessage = params.message.toLowerCase();
|
|
3237
|
+
if (params.detailCode === "AUTH_REQUIRED") return [renderAuthRequiredHint(params)];
|
|
3238
|
+
if (params.code === "TIMEOUT") return ["hint: increase `--timeout <seconds>` for long-running prompts, or check whether the agent/provider is stalled."];
|
|
3239
|
+
if (params.code === "NO_SESSION") {
|
|
3240
|
+
if (lowerMessage.includes("create one:")) return [];
|
|
3241
|
+
return ["hint: the saved ACP session is missing or stale; start a fresh session with `acpx <agent> sessions new`, then retry."];
|
|
3242
|
+
}
|
|
3243
|
+
if (lowerMessage.includes("does not support session/load")) return ["hint: this adapter cannot resume saved ACP sessions; create a fresh one with `acpx <agent> sessions new` instead of reusing `--resume-session`."];
|
|
3244
|
+
if (lowerMessage.includes("failed to resume acp session") || lowerMessage.includes("session/load")) return ["hint: rerun with `--verbose` to capture the ACP load failure details.", "hint: if you do not need the old backend session, start a fresh one with `acpx <agent> sessions new` and retry."];
|
|
3245
|
+
if (/\b429\b/u.test(params.message) || lowerMessage.includes("rate limit") || lowerMessage.includes("quota exceeded")) return ["hint: the provider appears rate-limited; retry later, switch model, or check provider quota/billing."];
|
|
3246
|
+
if (lowerMessage.includes("model not found") || lowerMessage.includes("unknown model") || lowerMessage.includes("invalid model")) return ["hint: check the configured model name for this agent, then retry with `--model <model>` or `sessions set-model <model>`."];
|
|
3247
|
+
if (lowerMessage.includes("session/set_mode") || lowerMessage.includes("session/set_model") || lowerMessage.includes("session/set_config_option")) return ["hint: rerun with `--verbose` to capture the ACP method/error details before retrying."];
|
|
3248
|
+
if (params.origin === "acp" && params.code === "RUNTIME" && (params.acp?.code === -32602 || params.acp?.code === -32603 || lowerMessage.includes("internal error"))) return ["hint: rerun with `--verbose` to capture the underlying ACP error details."];
|
|
3249
|
+
return [];
|
|
3250
|
+
}
|
|
3251
|
+
function summarizeToolInput(rawInput) {
|
|
3252
|
+
if (rawInput == null) return;
|
|
3253
|
+
if (typeof rawInput === "string" || typeof rawInput === "number" || typeof rawInput === "boolean") return toInline(String(rawInput));
|
|
3254
|
+
const record = asRecord(rawInput);
|
|
3255
|
+
if (record) {
|
|
3256
|
+
const command = readFirstString(record, [
|
|
3257
|
+
"command",
|
|
3258
|
+
"cmd",
|
|
3259
|
+
"program"
|
|
3260
|
+
]);
|
|
3261
|
+
const args = readFirstStringArray(record, ["args", "arguments"]);
|
|
3262
|
+
if (command) return toInline([command, ...args ?? []].join(" "));
|
|
3263
|
+
const location = readFirstString(record, [
|
|
3264
|
+
"path",
|
|
3265
|
+
"file",
|
|
3266
|
+
"filePath",
|
|
3267
|
+
"filepath",
|
|
3268
|
+
"target",
|
|
3269
|
+
"uri",
|
|
3270
|
+
"url"
|
|
3271
|
+
]);
|
|
3272
|
+
if (location) return toInline(location);
|
|
3273
|
+
const query = readFirstString(record, [
|
|
3274
|
+
"query",
|
|
3275
|
+
"pattern",
|
|
3276
|
+
"text",
|
|
3277
|
+
"search"
|
|
3278
|
+
]);
|
|
3279
|
+
if (query) return toInline(query);
|
|
3280
|
+
}
|
|
3281
|
+
const json = safeJson(rawInput, 0);
|
|
3282
|
+
return json ? toInline(json) : void 0;
|
|
3283
|
+
}
|
|
3284
|
+
function formatLocations(locations) {
|
|
3285
|
+
if (!locations || locations.length === 0) return;
|
|
3286
|
+
const unique = /* @__PURE__ */ new Set();
|
|
3287
|
+
for (const location of locations) {
|
|
3288
|
+
const path = location.path?.trim();
|
|
3289
|
+
if (!path) continue;
|
|
3290
|
+
const line = typeof location.line === "number" && Number.isFinite(location.line) ? `:${Math.max(1, Math.trunc(location.line))}` : "";
|
|
3291
|
+
unique.add(`${path}${line}`);
|
|
3292
|
+
}
|
|
3293
|
+
const items = [...unique];
|
|
3294
|
+
if (items.length === 0) return;
|
|
3295
|
+
const visible = items.slice(0, MAX_LOCATION_ITEMS);
|
|
3296
|
+
const hidden = items.length - visible.length;
|
|
3297
|
+
if (hidden <= 0) return visible.join(", ");
|
|
3298
|
+
return `${visible.join(", ")}, +${hidden} more`;
|
|
3299
|
+
}
|
|
3300
|
+
function summarizeDiff(path, oldText, newText) {
|
|
3301
|
+
const oldLines = oldText ? oldText.split("\n").length : 0;
|
|
3302
|
+
const delta = newText.split("\n").length - oldLines;
|
|
3303
|
+
if (delta === 0) return `diff ${path} (line count unchanged)`;
|
|
3304
|
+
return `diff ${path} (${`${delta > 0 ? "+" : ""}${delta}`} lines)`;
|
|
3305
|
+
}
|
|
3306
|
+
function textFromContentBlock(content) {
|
|
3307
|
+
switch (content.type) {
|
|
3308
|
+
case "text": return content.text;
|
|
3309
|
+
case "resource_link": return content.title ?? content.name ?? content.uri;
|
|
3310
|
+
case "resource": {
|
|
3311
|
+
if ("text" in content.resource && typeof content.resource.text === "string") return content.resource.text;
|
|
3312
|
+
const uri = content.resource.uri;
|
|
3313
|
+
const mimeType = content.resource.mimeType;
|
|
3314
|
+
return `[resource] ${uri}${mimeType ? ` (${mimeType})` : ""}`;
|
|
3315
|
+
}
|
|
3316
|
+
case "image": return `[image] ${content.mimeType}`;
|
|
3317
|
+
case "audio": return `[audio] ${content.mimeType}`;
|
|
3318
|
+
default: return;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
function summarizeToolContent(content) {
|
|
3322
|
+
if (!content || content.length === 0) return;
|
|
3323
|
+
const fragments = [];
|
|
3324
|
+
for (const entry of content) {
|
|
3325
|
+
if (entry.type === "content") {
|
|
3326
|
+
const text = textFromContentBlock(entry.content);
|
|
3327
|
+
if (text && text.trim()) fragments.push(text.trimEnd());
|
|
3328
|
+
continue;
|
|
3329
|
+
}
|
|
3330
|
+
if (entry.type === "diff") {
|
|
3331
|
+
fragments.push(summarizeDiff(entry.path, entry.oldText, entry.newText));
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3334
|
+
if (entry.type === "terminal") fragments.push(`[terminal] ${entry.terminalId}`);
|
|
3335
|
+
}
|
|
3336
|
+
const unique = dedupeStrings(fragments.map((fragment) => fragment.trim()).filter((fragment) => fragment.length > 0));
|
|
3337
|
+
if (unique.length === 0) return;
|
|
3338
|
+
return unique.join("\n\n");
|
|
3339
|
+
}
|
|
3340
|
+
function extractOutputText(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
3341
|
+
if (value == null) return;
|
|
3342
|
+
if (typeof value === "string") {
|
|
3343
|
+
const trimmed = value.trimEnd();
|
|
3344
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
3345
|
+
}
|
|
3346
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
3347
|
+
if (depth >= 4) return;
|
|
3348
|
+
if (Array.isArray(value)) {
|
|
3349
|
+
const parts = value.map((entry) => extractOutputText(entry, depth + 1, seen)).filter((entry) => Boolean(entry));
|
|
3350
|
+
if (parts.length === 0) return;
|
|
3351
|
+
return dedupeStrings(parts).join("\n");
|
|
3352
|
+
}
|
|
3353
|
+
const record = asRecord(value);
|
|
3354
|
+
if (!record) return;
|
|
3355
|
+
if (seen.has(record)) return;
|
|
3356
|
+
seen.add(record);
|
|
3357
|
+
const preferred = [];
|
|
3358
|
+
for (const key of OUTPUT_PRIORITY_KEYS) {
|
|
3359
|
+
if (!(key in record)) continue;
|
|
3360
|
+
const extracted = extractOutputText(record[key], depth + 1, seen);
|
|
3361
|
+
if (extracted) preferred.push(extracted);
|
|
3362
|
+
}
|
|
3363
|
+
const uniquePreferred = dedupeStrings(preferred);
|
|
3364
|
+
if (uniquePreferred.length > 0) return uniquePreferred.join("\n");
|
|
3365
|
+
const json = safeJson(record, 2);
|
|
3366
|
+
if (!json || json === "{}") return;
|
|
3367
|
+
return json;
|
|
3368
|
+
}
|
|
3369
|
+
function summarizeToolOutput(rawOutput, content) {
|
|
3370
|
+
const fragments = dedupeStrings([extractOutputText(rawOutput), summarizeToolContent(content)].map((fragment) => fragment?.trim()).filter((fragment) => Boolean(fragment)));
|
|
3371
|
+
if (fragments.length === 0) return;
|
|
3372
|
+
return fragments.join("\n\n");
|
|
3373
|
+
}
|
|
3374
|
+
function renderToolOutput(state, suppressReads) {
|
|
3375
|
+
if (suppressReads && isReadLikeTool(state)) return SUPPRESSED_READ_OUTPUT;
|
|
3376
|
+
return summarizeToolOutput(state.rawOutput, state.content);
|
|
3377
|
+
}
|
|
3378
|
+
function limitOutputBlock(value) {
|
|
3379
|
+
const normalized = value.replace(/\r\n/g, "\n").trim();
|
|
3380
|
+
if (!normalized) return "";
|
|
3381
|
+
const lines = normalized.split("\n");
|
|
3382
|
+
const visible = lines.slice(0, MAX_OUTPUT_LINES);
|
|
3383
|
+
let result = visible.join("\n");
|
|
3384
|
+
if (lines.length > visible.length) {
|
|
3385
|
+
const hidden = lines.length - visible.length;
|
|
3386
|
+
result += `\n... (${hidden} more lines)`;
|
|
3387
|
+
}
|
|
3388
|
+
if (result.length > MAX_OUTPUT_CHARS) result = `${result.slice(0, MAX_OUTPUT_CHARS - 3)}...`;
|
|
3389
|
+
return result;
|
|
3390
|
+
}
|
|
3391
|
+
var TextOutputFormatter = class {
|
|
3392
|
+
stdout;
|
|
3393
|
+
useColor;
|
|
3394
|
+
suppressReads;
|
|
3395
|
+
toolStates = /* @__PURE__ */ new Map();
|
|
3396
|
+
thoughtBuffer = "";
|
|
3397
|
+
wroteAny = false;
|
|
3398
|
+
atLineStart = true;
|
|
3399
|
+
section = null;
|
|
3400
|
+
constructor(stdout, suppressReads) {
|
|
3401
|
+
this.stdout = stdout;
|
|
3402
|
+
this.useColor = Boolean(stdout.isTTY);
|
|
3403
|
+
this.suppressReads = suppressReads;
|
|
3404
|
+
}
|
|
3405
|
+
setContext(_context) {}
|
|
3406
|
+
onAcpMessage(message) {
|
|
3407
|
+
const notification = extractSessionUpdateNotification(message);
|
|
3408
|
+
if (notification) {
|
|
3409
|
+
this.renderSessionUpdate(notification);
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
const method = extractJsonRpcMethod(message);
|
|
3413
|
+
if (method && method !== "session/prompt" && method !== "session/cancel") {
|
|
3414
|
+
this.onClientOperation({
|
|
3415
|
+
method,
|
|
3416
|
+
status: "running",
|
|
3417
|
+
summary: method,
|
|
3418
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3419
|
+
});
|
|
3420
|
+
return;
|
|
3421
|
+
}
|
|
3422
|
+
const stopReason = parsePromptStopReason(message);
|
|
3423
|
+
if (stopReason) {
|
|
3424
|
+
this.renderDone(stopReason);
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
const errorMessage = parseJsonRpcErrorMessage(message);
|
|
3428
|
+
if (errorMessage) this.onError({
|
|
3429
|
+
code: "RUNTIME",
|
|
3430
|
+
origin: "acp",
|
|
3431
|
+
message: errorMessage
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
renderSessionUpdate(notification) {
|
|
3435
|
+
const update = notification.update;
|
|
3436
|
+
if (update.sessionUpdate !== "agent_thought_chunk") this.flushThoughtBuffer();
|
|
3437
|
+
switch (update.sessionUpdate) {
|
|
3438
|
+
case "agent_message_chunk":
|
|
3439
|
+
if (update.content.type === "text") this.writeAssistantChunk(update.content.text);
|
|
3440
|
+
return;
|
|
3441
|
+
case "agent_thought_chunk":
|
|
3442
|
+
if (update.content.type === "text") this.thoughtBuffer += update.content.text;
|
|
3443
|
+
return;
|
|
3444
|
+
case "tool_call":
|
|
3445
|
+
this.renderToolUpdate(update);
|
|
3446
|
+
return;
|
|
3447
|
+
case "tool_call_update":
|
|
3448
|
+
this.renderToolUpdate(update);
|
|
3449
|
+
return;
|
|
3450
|
+
case "plan":
|
|
3451
|
+
this.beginSection("plan");
|
|
3452
|
+
this.writeLine(this.bold("[plan]"));
|
|
3453
|
+
for (const entry of update.entries) this.writeLine(` - [${entry.status}] ${entry.content}`);
|
|
3454
|
+
return;
|
|
3455
|
+
default: return;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
renderDone(stopReason) {
|
|
3459
|
+
this.flushThoughtBuffer();
|
|
3460
|
+
this.beginSection("done");
|
|
3461
|
+
this.writeLine(this.dim(`[done] ${stopReason}`));
|
|
3462
|
+
}
|
|
3463
|
+
onError(params) {
|
|
3464
|
+
this.flushThoughtBuffer();
|
|
3465
|
+
this.beginSection("done");
|
|
3466
|
+
this.writeLine(this.formatAnsi(`[error] ${params.code}: ${params.message}`, "31"));
|
|
3467
|
+
for (const hint of getTextErrorRemediationHints(params)) this.writeLine(this.dim(hint));
|
|
3468
|
+
}
|
|
3469
|
+
onClientOperation(operation) {
|
|
3470
|
+
this.flushThoughtBuffer();
|
|
3471
|
+
this.beginSection("client");
|
|
3472
|
+
const normalizedStatus = operation.status === "completed" ? "completed" : operation.status === "failed" ? "failed" : "in_progress";
|
|
3473
|
+
const statusText = this.colorStatus(operation.status, normalizedStatus);
|
|
3474
|
+
this.writeLine(`${this.bold("[client]")} ${operation.summary} (${statusText})`);
|
|
3475
|
+
if (operation.details && operation.details.trim().length > 0) {
|
|
3476
|
+
this.writeLine(" details:");
|
|
3477
|
+
this.writeLine(indentBlock(operation.details, " "));
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
onPermissionEscalation(event) {
|
|
3481
|
+
this.flushThoughtBuffer();
|
|
3482
|
+
this.beginSection("client");
|
|
3483
|
+
this.writeLine(`${this.bold("[permission]")} ${event.message}`);
|
|
3484
|
+
const details = [
|
|
3485
|
+
`sessionId: ${event.sessionId}`,
|
|
3486
|
+
`toolCallId: ${event.toolCallId}`,
|
|
3487
|
+
event.toolName ? `toolName: ${event.toolName}` : void 0,
|
|
3488
|
+
`toolTitle: ${event.toolTitle}`,
|
|
3489
|
+
event.toolInput !== void 0 ? `toolInput: ${summarizeToolInput(event.toolInput) ?? "(structured input)"}` : void 0,
|
|
3490
|
+
event.toolKind ? `toolKind: ${event.toolKind}` : void 0,
|
|
3491
|
+
event.matchedRule ? `matchedRule: ${event.matchedRule}` : void 0
|
|
3492
|
+
].filter((line) => Boolean(line));
|
|
3493
|
+
this.writeLine(indentBlock(details.join("\n"), " "));
|
|
3494
|
+
}
|
|
3495
|
+
flush() {
|
|
3496
|
+
this.flushThoughtBuffer();
|
|
3497
|
+
if (!this.atLineStart) this.write("\n");
|
|
3498
|
+
}
|
|
3499
|
+
write(chunk) {
|
|
3500
|
+
if (!chunk) return;
|
|
3501
|
+
this.stdout.write(chunk);
|
|
3502
|
+
this.wroteAny = true;
|
|
3503
|
+
this.atLineStart = chunk.endsWith("\n");
|
|
3504
|
+
}
|
|
3505
|
+
writeLine(line) {
|
|
3506
|
+
this.write(`${line}\n`);
|
|
3507
|
+
}
|
|
3508
|
+
beginSection(next) {
|
|
3509
|
+
if (!this.atLineStart) this.write("\n");
|
|
3510
|
+
if (this.wroteAny) this.write("\n");
|
|
3511
|
+
this.section = next;
|
|
3512
|
+
}
|
|
3513
|
+
writeAssistantChunk(text) {
|
|
3514
|
+
if (!text) return;
|
|
3515
|
+
this.section = "assistant";
|
|
3516
|
+
this.write(text);
|
|
3517
|
+
}
|
|
3518
|
+
flushThoughtBuffer() {
|
|
3519
|
+
const thought = truncate(normalizeLineEndings(this.thoughtBuffer).trim(), MAX_THOUGHT_CHARS);
|
|
3520
|
+
this.thoughtBuffer = "";
|
|
3521
|
+
if (!thought) return;
|
|
3522
|
+
this.beginSection("thought");
|
|
3523
|
+
const [firstLine, ...restLines] = thought.split("\n");
|
|
3524
|
+
this.writeLine(this.dim(`[thinking] ${firstLine}`));
|
|
3525
|
+
for (const line of restLines) this.writeLine(this.dim(` ${line}`));
|
|
3526
|
+
}
|
|
3527
|
+
renderToolUpdate(update) {
|
|
3528
|
+
const state = this.getOrCreateToolState(update.toolCallId);
|
|
3529
|
+
this.mergeToolState(state, update);
|
|
3530
|
+
const status = asStatus(state.status);
|
|
3531
|
+
if (isFinalStatus(status)) {
|
|
3532
|
+
const signature = this.toolSignature(state);
|
|
3533
|
+
if (signature !== state.finalSignature) {
|
|
3534
|
+
state.finalSignature = signature;
|
|
3535
|
+
this.renderFinalToolState(state, status);
|
|
3536
|
+
}
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
if (state.startedPrinted) return;
|
|
3540
|
+
state.startedPrinted = true;
|
|
3541
|
+
this.renderStartingToolState(state, status);
|
|
3542
|
+
}
|
|
3543
|
+
getOrCreateToolState(toolCallId) {
|
|
3544
|
+
const existing = this.toolStates.get(toolCallId);
|
|
3545
|
+
if (existing) return existing;
|
|
3546
|
+
const created = {
|
|
3547
|
+
id: toolCallId,
|
|
3548
|
+
startedPrinted: false
|
|
3549
|
+
};
|
|
3550
|
+
this.toolStates.set(toolCallId, created);
|
|
3551
|
+
return created;
|
|
3552
|
+
}
|
|
3553
|
+
mergeToolState(state, update) {
|
|
3554
|
+
if (typeof update.title === "string" && update.title.trim().length > 0) state.title = update.title;
|
|
3555
|
+
if (update.status !== void 0) state.status = update.status;
|
|
3556
|
+
if (update.kind !== void 0) state.kind = update.kind;
|
|
3557
|
+
if (update.locations !== void 0) state.locations = update.locations;
|
|
3558
|
+
if (update.rawInput !== void 0) state.rawInput = update.rawInput;
|
|
3559
|
+
if (update.rawOutput !== void 0) state.rawOutput = update.rawOutput;
|
|
3560
|
+
if (update.content !== void 0) state.content = update.content;
|
|
3561
|
+
}
|
|
3562
|
+
toolSignature(state) {
|
|
3563
|
+
const signaturePayload = {
|
|
3564
|
+
title: state.title,
|
|
3565
|
+
status: state.status,
|
|
3566
|
+
kind: state.kind,
|
|
3567
|
+
input: summarizeToolInput(state.rawInput),
|
|
3568
|
+
files: formatLocations(state.locations),
|
|
3569
|
+
output: renderToolOutput(state, this.suppressReads)
|
|
3570
|
+
};
|
|
3571
|
+
return safeJson(signaturePayload, 0) ?? JSON.stringify(signaturePayload);
|
|
3572
|
+
}
|
|
3573
|
+
renderStartingToolState(state, status) {
|
|
3574
|
+
this.beginSection("tool");
|
|
3575
|
+
const title = state.title ?? state.id;
|
|
3576
|
+
const label = status === "pending" ? "pending" : "running";
|
|
3577
|
+
const statusText = this.colorStatus(label, status);
|
|
3578
|
+
this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
|
|
3579
|
+
const input = summarizeToolInput(state.rawInput);
|
|
3580
|
+
if (input) this.writeLine(` input: ${input}`);
|
|
3581
|
+
const files = formatLocations(state.locations);
|
|
3582
|
+
if (files) this.writeLine(` files: ${files}`);
|
|
3583
|
+
}
|
|
3584
|
+
renderFinalToolState(state, status) {
|
|
3585
|
+
this.beginSection("tool");
|
|
3586
|
+
const title = state.title ?? state.id;
|
|
3587
|
+
const statusText = this.colorStatus(toStatusLabel(status), status);
|
|
3588
|
+
this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
|
|
3589
|
+
if (state.kind) this.writeLine(` kind: ${state.kind}`);
|
|
3590
|
+
const input = summarizeToolInput(state.rawInput);
|
|
3591
|
+
if (input) this.writeLine(` input: ${input}`);
|
|
3592
|
+
const files = formatLocations(state.locations);
|
|
3593
|
+
if (files) this.writeLine(` files: ${files}`);
|
|
3594
|
+
const output = renderToolOutput(state, this.suppressReads);
|
|
3595
|
+
if (output) {
|
|
3596
|
+
this.writeLine(" output:");
|
|
3597
|
+
this.writeLine(indentBlock(limitOutputBlock(output), " "));
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
formatAnsi(text, code) {
|
|
3601
|
+
if (!this.useColor) return text;
|
|
3602
|
+
return `\u001b[${code}m${text}\u001b[0m`;
|
|
3603
|
+
}
|
|
3604
|
+
bold(text) {
|
|
3605
|
+
return this.formatAnsi(text, "1");
|
|
3606
|
+
}
|
|
3607
|
+
dim(text) {
|
|
3608
|
+
return this.formatAnsi(text, "2");
|
|
3609
|
+
}
|
|
3610
|
+
colorStatus(text, status) {
|
|
3611
|
+
if (!this.useColor) return text;
|
|
3612
|
+
switch (status) {
|
|
3613
|
+
case "completed": return this.formatAnsi(text, "32");
|
|
3614
|
+
case "failed": return this.formatAnsi(text, "31");
|
|
3615
|
+
default: return this.formatAnsi(text, "33");
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3619
|
+
var QuietOutputFormatter = class {
|
|
3620
|
+
stdout;
|
|
3621
|
+
stderr;
|
|
3622
|
+
chunks = [];
|
|
3623
|
+
flushed = false;
|
|
3624
|
+
metadataFlushed = false;
|
|
3625
|
+
constructor(stdout, stderr) {
|
|
3626
|
+
this.stdout = stdout;
|
|
3627
|
+
this.stderr = stderr;
|
|
3628
|
+
}
|
|
3629
|
+
setContext(_context) {}
|
|
3630
|
+
onAcpMessage(message) {
|
|
3631
|
+
const update = extractSessionUpdateNotification(message);
|
|
3632
|
+
if (update?.update.sessionUpdate === "agent_message_chunk" && update.update.content.type === "text") {
|
|
3633
|
+
this.chunks.push(update.update.content.text);
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
if (parsePromptStopReason(message)) {
|
|
3637
|
+
this.flushBufferedOutput();
|
|
3638
|
+
this.flushMetadata(message);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
onError(_params) {}
|
|
3642
|
+
onPermissionEscalation(_event) {}
|
|
3643
|
+
flush() {}
|
|
3644
|
+
flushBufferedOutput() {
|
|
3645
|
+
if (this.flushed) return;
|
|
3646
|
+
this.flushed = true;
|
|
3647
|
+
const text = this.chunks.join("");
|
|
3648
|
+
this.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
3649
|
+
}
|
|
3650
|
+
flushMetadata(message) {
|
|
3651
|
+
if (this.metadataFlushed) return;
|
|
3652
|
+
this.metadataFlushed = true;
|
|
3653
|
+
const result = asRecord(message.result);
|
|
3654
|
+
if (!result) return;
|
|
3655
|
+
const usageLine = this.formatUsageLine(asRecord(result.usage));
|
|
3656
|
+
if (usageLine) this.stderr.write(`${usageLine}\n`);
|
|
3657
|
+
const costLine = this.formatCostLine(result.cost);
|
|
3658
|
+
if (costLine) this.stderr.write(`${costLine}\n`);
|
|
3659
|
+
}
|
|
3660
|
+
formatUsageLine(usage) {
|
|
3661
|
+
if (!usage) return;
|
|
3662
|
+
const parts = [];
|
|
3663
|
+
for (const [label, keys] of [
|
|
3664
|
+
["input", ["inputTokens", "input_tokens"]],
|
|
3665
|
+
["output", ["outputTokens", "output_tokens"]],
|
|
3666
|
+
["cache_read", [
|
|
3667
|
+
"cachedReadTokens",
|
|
3668
|
+
"cacheReadInputTokens",
|
|
3669
|
+
"cache_read_input_tokens"
|
|
3670
|
+
]],
|
|
3671
|
+
["cache_write", [
|
|
3672
|
+
"cachedWriteTokens",
|
|
3673
|
+
"cacheCreationInputTokens",
|
|
3674
|
+
"cache_creation_input_tokens"
|
|
3675
|
+
]],
|
|
3676
|
+
["total", ["totalTokens", "total_tokens"]]
|
|
3677
|
+
]) {
|
|
3678
|
+
const value = readFirstFiniteNumber(usage, keys);
|
|
3679
|
+
if (value !== void 0) parts.push(`${label}=${formatMetadataNumber(value)}`);
|
|
3680
|
+
}
|
|
3681
|
+
return parts.length > 0 ? `[acpx] tokens: ${parts.join(" ")}` : void 0;
|
|
3682
|
+
}
|
|
3683
|
+
formatCostLine(cost) {
|
|
3684
|
+
if (typeof cost === "number" && Number.isFinite(cost)) return `[acpx] cost: ${formatMetadataNumber(cost)}`;
|
|
3685
|
+
if (typeof cost === "string" && cost.trim()) return `[acpx] cost: ${cost.trim()}`;
|
|
3686
|
+
const record = asRecord(cost);
|
|
3687
|
+
if (!record) return;
|
|
3688
|
+
const amount = readFirstFiniteNumber(record, [
|
|
3689
|
+
"amount",
|
|
3690
|
+
"value",
|
|
3691
|
+
"total"
|
|
3692
|
+
]);
|
|
3693
|
+
if (amount === void 0) return;
|
|
3694
|
+
const currency = typeof record.currency === "string" && record.currency.trim() ? ` ${record.currency.trim()}` : "";
|
|
3695
|
+
return `[acpx] cost: ${formatMetadataNumber(amount)}${currency}`;
|
|
3696
|
+
}
|
|
3697
|
+
};
|
|
3698
|
+
function createOutputFormatter(format, options = {}) {
|
|
3699
|
+
const stdout = options.stdout ?? process.stdout;
|
|
3700
|
+
const stderr = options.stderr ?? process.stderr;
|
|
3701
|
+
const suppressReads = options.suppressReads === true;
|
|
3702
|
+
switch (format) {
|
|
3703
|
+
case "text": return new TextOutputFormatter(stdout, suppressReads);
|
|
3704
|
+
case "json": return createJsonOutputFormatter(stdout, suppressReads, options.jsonContext);
|
|
3705
|
+
case "quiet": return new QuietOutputFormatter(stdout, stderr);
|
|
3706
|
+
default: throw new Error("Unsupported output format");
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
//#endregion
|
|
3710
|
+
export { runSessionQueueOwner as a, buildQueueOwnerArgOverride as c, createSessionWithClient as d, cancelSessionPrompt as f, __exportAll as h, session_exports as i, flushPerfMetricsCapture as l, DEFAULT_QUEUE_OWNER_TTL_MS as m, getTextErrorRemediationHints as n, runOnce as o, probeQueueOwnerHealth as p, output_exports as r, sendSessionDirect as s, createOutputFormatter as t, installPerfMetricsCapture as u };
|
|
3711
|
+
|
|
3712
|
+
//# sourceMappingURL=output-BL9XRWzS.js.map
|