acpx 0.4.0 → 0.5.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/dist/agent-registry-DGw0-3Tc.js +54 -0
- package/dist/agent-registry-DGw0-3Tc.js.map +1 -0
- package/dist/{cli-5s-E-Y99.js → cli-CLRrs6eQ.js} +8 -12
- package/dist/cli-CLRrs6eQ.js.map +1 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1018 -1009
- package/dist/cli.js.map +1 -1
- package/dist/client-DLTWuu4w.d.ts +116 -0
- package/dist/client-DLTWuu4w.d.ts.map +1 -0
- package/dist/{flags-BkWInxAq.js → flags-BmubjvOw.js} +5 -55
- package/dist/flags-BmubjvOw.js.map +1 -0
- package/dist/{flows-DnIYoHI1.js → flows-CR7xCmkR.js} +471 -281
- package/dist/flows-CR7xCmkR.js.map +1 -0
- package/dist/flows.d.ts +5 -9
- package/dist/flows.d.ts.map +1 -1
- package/dist/flows.js +1 -1
- package/dist/{queue-ipc-CgWf63GN.js → ipc-DN6M4Ui9.js} +12 -571
- package/dist/ipc-DN6M4Ui9.js.map +1 -0
- package/dist/{acp-jsonrpc-C7pPk9Tw.js → jsonrpc-M3y-qzy8.js} +16 -3
- package/dist/jsonrpc-M3y-qzy8.js.map +1 -0
- package/dist/{output-C58ukIo3.js → output-Di0M9Et8.js} +8 -22
- package/dist/output-Di0M9Et8.js.map +1 -0
- package/dist/perf-metrics-D9QC81lB.js +568 -0
- package/dist/perf-metrics-D9QC81lB.js.map +1 -0
- package/dist/{session-BtpTC2pM.js → prompt-turn-Bt8T3SRR.js} +2304 -3632
- package/dist/prompt-turn-Bt8T3SRR.js.map +1 -0
- package/dist/{output-render-C7w9NZ2H.js → render-BL5ynRkN.js} +7 -6
- package/dist/render-BL5ynRkN.js.map +1 -0
- package/dist/runtime.d.ts +266 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +984 -0
- package/dist/runtime.js.map +1 -0
- package/dist/session-BbN0SBgf.js +1488 -0
- package/dist/session-BbN0SBgf.js.map +1 -0
- package/dist/{types-CeRKmEQ1.d.ts → types-DXxLBQc3.d.ts} +40 -3
- package/dist/types-DXxLBQc3.d.ts.map +1 -0
- package/package.json +20 -9
- package/dist/acp-jsonrpc-C7pPk9Tw.js.map +0 -1
- package/dist/cli-5s-E-Y99.js.map +0 -1
- package/dist/flags-BkWInxAq.js.map +0 -1
- package/dist/flows-DnIYoHI1.js.map +0 -1
- package/dist/output-C58ukIo3.js.map +0 -1
- package/dist/output-render-C7w9NZ2H.js.map +0 -1
- package/dist/queue-ipc-CgWf63GN.js.map +0 -1
- package/dist/session-BtpTC2pM.js.map +0 -1
- package/dist/types-CeRKmEQ1.d.ts.map +0 -1
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-CiIaOW0V.js";
|
|
2
|
+
import { B as QueueConnectionError, b as isRetryablePromptError, c as startPerfTimer, g as textPrompt, h as promptToDisplayText, i as measurePerf, n as getPerfMetricsSnapshot, o as resetPerfMetrics, r as incrementPerfCounter, s as setPerfGauge, t as formatPerfMetric, u as normalizeRuntimeSessionId, v as formatErrorMessage, x as normalizeOutputError } from "./perf-metrics-D9QC81lB.js";
|
|
3
|
+
import { A as resolveSessionRecord, C as findGitRepositoryRoot, D as listSessions, E as isoNow, F as sessionBaseDir, H as TimeoutError, I as sessionEventActivePath, L as sessionEventLockPath, O as listSessionsForAgent, P as defaultSessionEventLog, R as sessionEventSegmentPath, S as absolutePath, T as findSessionByDirectoryWalk, U as withInterrupt, V as InterruptedError, W as withTimeout, _ as recordSessionUpdate, a as applyConversation, c as setCurrentModelId, d as syncAdvertisedModelState, f as cloneSessionAcpxState, g as recordPromptSubmission, h as recordClientOperation, i as connectAndLoadSession, j as writeSessionRecord, k as normalizeName, l as setDesiredModeId, m as createSessionConversation, n as withConnectedSession, o as applyLifecycleSnapshotToRecord, p as cloneSessionConversation, r as sessionOptionsFromRecord, t as runPromptTurn, u as setDesiredModelId, v as trimConversationForRuntime, w as findSession, y as AcpClient } from "./prompt-turn-Bt8T3SRR.js";
|
|
4
|
+
import { n as isAcpJsonRpcMessage } from "./jsonrpc-M3y-qzy8.js";
|
|
5
|
+
import { a as trySubmitToRunningOwner, c as isProcessAlive, d as terminateProcess, f as terminateQueueOwnerForSession, i as trySetModelOnRunningOwner, l as refreshQueueOwnerLease, m as waitMs, n as trySetConfigOptionOnRunningOwner, o as SessionQueueOwner, p as tryAcquireQueueOwnerLease, r as trySetModeOnRunningOwner, t as tryCancelOnRunningOwner, u as releaseQueueOwnerLease } from "./ipc-DN6M4Ui9.js";
|
|
6
|
+
import fs, { realpathSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs$1 from "node:fs/promises";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
//#region src/cli/session/contracts.ts
|
|
11
|
+
const DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
|
|
12
|
+
function normalizeQueueOwnerTtlMs(ttlMs) {
|
|
13
|
+
if (ttlMs == null) return DEFAULT_QUEUE_OWNER_TTL_MS;
|
|
14
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) return DEFAULT_QUEUE_OWNER_TTL_MS;
|
|
15
|
+
return Math.round(ttlMs);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/cli/session/prompt-runner.ts
|
|
19
|
+
async function runSessionSetModeDirect(options) {
|
|
20
|
+
const result = await withConnectedSession({
|
|
21
|
+
sessionRecordId: options.sessionRecordId,
|
|
22
|
+
loadRecord: resolveSessionRecord,
|
|
23
|
+
saveRecord: writeSessionRecord,
|
|
24
|
+
mcpServers: options.mcpServers,
|
|
25
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
26
|
+
authCredentials: options.authCredentials,
|
|
27
|
+
authPolicy: options.authPolicy,
|
|
28
|
+
timeoutMs: options.timeoutMs,
|
|
29
|
+
verbose: options.verbose,
|
|
30
|
+
onClientAvailable: (controller) => {
|
|
31
|
+
options.onClientAvailable?.(controller);
|
|
32
|
+
},
|
|
33
|
+
onClientClosed: options.onClientClosed,
|
|
34
|
+
run: async ({ client, sessionId, record }) => {
|
|
35
|
+
await withTimeout(client.setSessionMode(sessionId, options.modeId), options.timeoutMs);
|
|
36
|
+
setDesiredModeId(record, options.modeId);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
record: result.record,
|
|
41
|
+
resumed: result.resumed,
|
|
42
|
+
loadError: result.loadError
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function runSessionSetModelDirect(options) {
|
|
46
|
+
const result = await withConnectedSession({
|
|
47
|
+
sessionRecordId: options.sessionRecordId,
|
|
48
|
+
loadRecord: resolveSessionRecord,
|
|
49
|
+
saveRecord: writeSessionRecord,
|
|
50
|
+
mcpServers: options.mcpServers,
|
|
51
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
52
|
+
authCredentials: options.authCredentials,
|
|
53
|
+
authPolicy: options.authPolicy,
|
|
54
|
+
timeoutMs: options.timeoutMs,
|
|
55
|
+
verbose: options.verbose,
|
|
56
|
+
onClientAvailable: (controller) => {
|
|
57
|
+
options.onClientAvailable?.(controller);
|
|
58
|
+
},
|
|
59
|
+
onClientClosed: options.onClientClosed,
|
|
60
|
+
run: async ({ client, sessionId, record }) => {
|
|
61
|
+
await withTimeout(client.setSessionModel(sessionId, options.modelId), options.timeoutMs);
|
|
62
|
+
setDesiredModelId(record, options.modelId);
|
|
63
|
+
setCurrentModelId(record, options.modelId);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
record: result.record,
|
|
68
|
+
resumed: result.resumed,
|
|
69
|
+
loadError: result.loadError
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function runSessionSetConfigOptionDirect(options) {
|
|
73
|
+
const result = await withConnectedSession({
|
|
74
|
+
sessionRecordId: options.sessionRecordId,
|
|
75
|
+
loadRecord: resolveSessionRecord,
|
|
76
|
+
saveRecord: writeSessionRecord,
|
|
77
|
+
mcpServers: options.mcpServers,
|
|
78
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
79
|
+
authCredentials: options.authCredentials,
|
|
80
|
+
authPolicy: options.authPolicy,
|
|
81
|
+
timeoutMs: options.timeoutMs,
|
|
82
|
+
verbose: options.verbose,
|
|
83
|
+
onClientAvailable: (controller) => {
|
|
84
|
+
options.onClientAvailable?.(controller);
|
|
85
|
+
},
|
|
86
|
+
onClientClosed: options.onClientClosed,
|
|
87
|
+
run: async ({ client, sessionId, record }) => {
|
|
88
|
+
const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value), options.timeoutMs);
|
|
89
|
+
if (options.configId === "mode") setDesiredModeId(record, options.value);
|
|
90
|
+
return response;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
record: result.record,
|
|
95
|
+
response: result.value,
|
|
96
|
+
resumed: result.resumed,
|
|
97
|
+
loadError: result.loadError
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/cli/session/session-control.ts
|
|
102
|
+
async function cancelSessionPrompt(options) {
|
|
103
|
+
const cancelled = await tryCancelOnRunningOwner(options);
|
|
104
|
+
return {
|
|
105
|
+
sessionId: options.sessionId,
|
|
106
|
+
cancelled: cancelled === true
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function setSessionMode(options) {
|
|
110
|
+
if (await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose)) {
|
|
111
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
112
|
+
setDesiredModeId(record, options.modeId);
|
|
113
|
+
await writeSessionRecord(record);
|
|
114
|
+
return {
|
|
115
|
+
record,
|
|
116
|
+
resumed: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return await runSessionSetModeDirect({
|
|
120
|
+
sessionRecordId: options.sessionId,
|
|
121
|
+
modeId: options.modeId,
|
|
122
|
+
mcpServers: options.mcpServers,
|
|
123
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
124
|
+
authCredentials: options.authCredentials,
|
|
125
|
+
authPolicy: options.authPolicy,
|
|
126
|
+
timeoutMs: options.timeoutMs,
|
|
127
|
+
verbose: options.verbose
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function setSessionModel(options) {
|
|
131
|
+
if (await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose)) {
|
|
132
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
133
|
+
setDesiredModelId(record, options.modelId);
|
|
134
|
+
setCurrentModelId(record, options.modelId);
|
|
135
|
+
await writeSessionRecord(record);
|
|
136
|
+
return {
|
|
137
|
+
record,
|
|
138
|
+
resumed: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return await runSessionSetModelDirect({
|
|
142
|
+
sessionRecordId: options.sessionId,
|
|
143
|
+
modelId: options.modelId,
|
|
144
|
+
mcpServers: options.mcpServers,
|
|
145
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
146
|
+
authCredentials: options.authCredentials,
|
|
147
|
+
authPolicy: options.authPolicy,
|
|
148
|
+
timeoutMs: options.timeoutMs,
|
|
149
|
+
verbose: options.verbose
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function setSessionConfigOption(options) {
|
|
153
|
+
const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose);
|
|
154
|
+
if (ownerResponse) {
|
|
155
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
156
|
+
if (options.configId === "mode") {
|
|
157
|
+
setDesiredModeId(record, options.value);
|
|
158
|
+
await writeSessionRecord(record);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
record,
|
|
162
|
+
response: ownerResponse,
|
|
163
|
+
resumed: false
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return await runSessionSetConfigOptionDirect({
|
|
167
|
+
sessionRecordId: options.sessionId,
|
|
168
|
+
configId: options.configId,
|
|
169
|
+
value: options.value,
|
|
170
|
+
mcpServers: options.mcpServers,
|
|
171
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
172
|
+
authCredentials: options.authCredentials,
|
|
173
|
+
authPolicy: options.authPolicy,
|
|
174
|
+
timeoutMs: options.timeoutMs,
|
|
175
|
+
verbose: options.verbose
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function firstAgentCommandToken(command) {
|
|
179
|
+
const trimmed = command.trim();
|
|
180
|
+
if (!trimmed) return;
|
|
181
|
+
const token = trimmed.split(/\s+/, 1)[0];
|
|
182
|
+
return token.length > 0 ? token : void 0;
|
|
183
|
+
}
|
|
184
|
+
async function isLikelyMatchingProcess(pid, agentCommand) {
|
|
185
|
+
const expectedToken = firstAgentCommandToken(agentCommand);
|
|
186
|
+
if (!expectedToken) return false;
|
|
187
|
+
const procCmdline = `/proc/${pid}/cmdline`;
|
|
188
|
+
try {
|
|
189
|
+
const argv = (await fs$1.readFile(procCmdline, "utf8")).split("\0").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
190
|
+
if (argv.length === 0) return false;
|
|
191
|
+
const executableBase = path.basename(argv[0]);
|
|
192
|
+
const expectedBase = path.basename(expectedToken);
|
|
193
|
+
return executableBase === expectedBase || argv.some((entry) => path.basename(entry) === expectedBase);
|
|
194
|
+
} catch {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function closeSession(sessionId) {
|
|
199
|
+
const record = await resolveSessionRecord(sessionId);
|
|
200
|
+
await terminateQueueOwnerForSession(record.acpxRecordId);
|
|
201
|
+
if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) await terminateProcess(record.pid);
|
|
202
|
+
record.pid = void 0;
|
|
203
|
+
record.closed = true;
|
|
204
|
+
record.closedAt = isoNow();
|
|
205
|
+
await writeSessionRecord(record);
|
|
206
|
+
return record;
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/cli/session/session-management.ts
|
|
210
|
+
function persistSessionOptions(record, options) {
|
|
211
|
+
const next = options && {
|
|
212
|
+
model: typeof options.model === "string" ? options.model : void 0,
|
|
213
|
+
allowed_tools: Array.isArray(options.allowedTools) ? [...options.allowedTools] : void 0,
|
|
214
|
+
max_turns: typeof options.maxTurns === "number" ? options.maxTurns : void 0
|
|
215
|
+
};
|
|
216
|
+
if (Boolean(next && (typeof next.model === "string" && next.model.trim().length > 0 || Array.isArray(next.allowed_tools) && next.allowed_tools.length > 0 || typeof next.max_turns === "number")) && next) {
|
|
217
|
+
record.acpx = {
|
|
218
|
+
...record.acpx,
|
|
219
|
+
session_options: next
|
|
220
|
+
};
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!record.acpx) return;
|
|
224
|
+
delete record.acpx.session_options;
|
|
225
|
+
}
|
|
226
|
+
async function applyRequestedModelIfAdvertised$1(params) {
|
|
227
|
+
const requestedModel = typeof params.requestedModel === "string" ? params.requestedModel.trim() : "";
|
|
228
|
+
if (!requestedModel || !params.models) return false;
|
|
229
|
+
if (params.models.currentModelId === requestedModel) return true;
|
|
230
|
+
await withTimeout(params.client.setSessionModel(params.sessionId, requestedModel), params.timeoutMs);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
async function createSessionRecordWithClient(client, options) {
|
|
234
|
+
const cwd = absolutePath(options.cwd);
|
|
235
|
+
await withTimeout(client.start(), options.timeoutMs);
|
|
236
|
+
let sessionId;
|
|
237
|
+
let agentSessionId;
|
|
238
|
+
let sessionModels;
|
|
239
|
+
let requestedModelApplied = false;
|
|
240
|
+
if (options.resumeSessionId) {
|
|
241
|
+
if (!client.supportsLoadSession()) throw new Error(`Agent command "${options.agentCommand}" does not support session/load; cannot resume session ${options.resumeSessionId}`);
|
|
242
|
+
try {
|
|
243
|
+
const loadedSession = await withTimeout(client.loadSession(options.resumeSessionId, cwd), options.timeoutMs);
|
|
244
|
+
sessionId = options.resumeSessionId;
|
|
245
|
+
agentSessionId = normalizeRuntimeSessionId(loadedSession.agentSessionId);
|
|
246
|
+
sessionModels = loadedSession.models;
|
|
247
|
+
requestedModelApplied = await applyRequestedModelIfAdvertised$1({
|
|
248
|
+
client,
|
|
249
|
+
sessionId,
|
|
250
|
+
requestedModel: options.sessionOptions?.model,
|
|
251
|
+
models: sessionModels,
|
|
252
|
+
timeoutMs: options.timeoutMs
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
throw new Error(`Failed to resume ACP session ${options.resumeSessionId}: ${formatErrorMessage(error)}`, { cause: error });
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
const createdSession = await withTimeout(client.createSession(cwd), options.timeoutMs);
|
|
259
|
+
sessionId = createdSession.sessionId;
|
|
260
|
+
agentSessionId = normalizeRuntimeSessionId(createdSession.agentSessionId);
|
|
261
|
+
sessionModels = createdSession.models;
|
|
262
|
+
requestedModelApplied = await applyRequestedModelIfAdvertised$1({
|
|
263
|
+
client,
|
|
264
|
+
sessionId,
|
|
265
|
+
requestedModel: options.sessionOptions?.model,
|
|
266
|
+
models: sessionModels,
|
|
267
|
+
timeoutMs: options.timeoutMs
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const lifecycle = client.getAgentLifecycleSnapshot();
|
|
271
|
+
const now = isoNow();
|
|
272
|
+
const record = {
|
|
273
|
+
schema: "acpx.session.v1",
|
|
274
|
+
acpxRecordId: sessionId,
|
|
275
|
+
acpSessionId: sessionId,
|
|
276
|
+
agentSessionId,
|
|
277
|
+
agentCommand: options.agentCommand,
|
|
278
|
+
cwd,
|
|
279
|
+
name: normalizeName(options.name),
|
|
280
|
+
createdAt: now,
|
|
281
|
+
lastUsedAt: now,
|
|
282
|
+
lastSeq: 0,
|
|
283
|
+
lastRequestId: void 0,
|
|
284
|
+
eventLog: defaultSessionEventLog(sessionId),
|
|
285
|
+
closed: false,
|
|
286
|
+
closedAt: void 0,
|
|
287
|
+
pid: lifecycle.pid,
|
|
288
|
+
agentStartedAt: lifecycle.startedAt,
|
|
289
|
+
protocolVersion: client.initializeResult?.protocolVersion,
|
|
290
|
+
agentCapabilities: client.initializeResult?.agentCapabilities,
|
|
291
|
+
...createSessionConversation(now),
|
|
292
|
+
acpx: {}
|
|
293
|
+
};
|
|
294
|
+
persistSessionOptions(record, options.sessionOptions);
|
|
295
|
+
syncAdvertisedModelState(record, sessionModels);
|
|
296
|
+
if (requestedModelApplied) setCurrentModelId(record, options.sessionOptions?.model);
|
|
297
|
+
await writeSessionRecord(record);
|
|
298
|
+
return record;
|
|
299
|
+
}
|
|
300
|
+
async function createSessionWithClient(options) {
|
|
301
|
+
const client = new AcpClient({
|
|
302
|
+
agentCommand: options.agentCommand,
|
|
303
|
+
cwd: absolutePath(options.cwd),
|
|
304
|
+
mcpServers: options.mcpServers,
|
|
305
|
+
permissionMode: options.permissionMode,
|
|
306
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
307
|
+
authCredentials: options.authCredentials,
|
|
308
|
+
authPolicy: options.authPolicy,
|
|
309
|
+
verbose: options.verbose,
|
|
310
|
+
sessionOptions: options.sessionOptions
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
return {
|
|
314
|
+
record: await withInterrupt(async () => await createSessionRecordWithClient(client, options), async () => {
|
|
315
|
+
await client.close();
|
|
316
|
+
}),
|
|
317
|
+
client
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
await client.close();
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function createSession(options) {
|
|
325
|
+
const { record, client } = await createSessionWithClient(options);
|
|
326
|
+
try {
|
|
327
|
+
return record;
|
|
328
|
+
} finally {
|
|
329
|
+
await client.close();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function ensureSession(options) {
|
|
333
|
+
const cwd = absolutePath(options.cwd);
|
|
334
|
+
const gitRoot = findGitRepositoryRoot(cwd);
|
|
335
|
+
const walkBoundary = options.walkBoundary ?? gitRoot ?? cwd;
|
|
336
|
+
const existing = await findSessionByDirectoryWalk({
|
|
337
|
+
agentCommand: options.agentCommand,
|
|
338
|
+
cwd,
|
|
339
|
+
name: options.name,
|
|
340
|
+
boundary: walkBoundary
|
|
341
|
+
});
|
|
342
|
+
if (existing) {
|
|
343
|
+
const requestedModel = options.sessionOptions?.model;
|
|
344
|
+
if (requestedModel) return {
|
|
345
|
+
record: (await setSessionModel({
|
|
346
|
+
sessionId: existing.acpxRecordId,
|
|
347
|
+
modelId: requestedModel,
|
|
348
|
+
mcpServers: options.mcpServers,
|
|
349
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
350
|
+
authCredentials: options.authCredentials,
|
|
351
|
+
authPolicy: options.authPolicy,
|
|
352
|
+
timeoutMs: options.timeoutMs,
|
|
353
|
+
verbose: options.verbose
|
|
354
|
+
})).record,
|
|
355
|
+
created: false
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
record: existing,
|
|
359
|
+
created: false
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
record: await createSession({
|
|
364
|
+
agentCommand: options.agentCommand,
|
|
365
|
+
cwd,
|
|
366
|
+
name: options.name,
|
|
367
|
+
resumeSessionId: options.resumeSessionId,
|
|
368
|
+
mcpServers: options.mcpServers,
|
|
369
|
+
permissionMode: options.permissionMode,
|
|
370
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
371
|
+
authCredentials: options.authCredentials,
|
|
372
|
+
authPolicy: options.authPolicy,
|
|
373
|
+
timeoutMs: options.timeoutMs,
|
|
374
|
+
verbose: options.verbose,
|
|
375
|
+
sessionOptions: options.sessionOptions
|
|
376
|
+
}),
|
|
377
|
+
created: true
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/perf-metrics-capture.ts
|
|
382
|
+
const PERF_METRICS_FILE_ENV = "ACPX_PERF_METRICS_FILE";
|
|
383
|
+
let installed = false;
|
|
384
|
+
let flushed = false;
|
|
385
|
+
let captureFilePath;
|
|
386
|
+
let captureRole = "cli";
|
|
387
|
+
let captureArgv = [];
|
|
388
|
+
let captureSequence = 0;
|
|
389
|
+
function shouldCapture() {
|
|
390
|
+
return typeof captureFilePath === "string" && captureFilePath.trim().length > 0;
|
|
391
|
+
}
|
|
392
|
+
function buildPayload(reason) {
|
|
393
|
+
return {
|
|
394
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
395
|
+
pid: process.pid,
|
|
396
|
+
ppid: process.ppid,
|
|
397
|
+
role: captureRole,
|
|
398
|
+
argv: captureArgv,
|
|
399
|
+
cwd: process.cwd(),
|
|
400
|
+
sequence: captureSequence,
|
|
401
|
+
reason,
|
|
402
|
+
metrics: getPerfMetricsSnapshot()
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function writePerfMetricsCapture(reason, resetAfterWrite) {
|
|
406
|
+
if (!shouldCapture()) return false;
|
|
407
|
+
const payload = buildPayload(reason);
|
|
408
|
+
const metrics = payload.metrics;
|
|
409
|
+
if (!(Object.keys(metrics.counters ?? {}).length > 0 || Object.keys(metrics.gauges ?? {}).length > 0 || Object.keys(metrics.timings ?? {}).length > 0)) return false;
|
|
410
|
+
try {
|
|
411
|
+
fs.mkdirSync(path.dirname(captureFilePath), { recursive: true });
|
|
412
|
+
fs.appendFileSync(captureFilePath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
413
|
+
captureSequence += 1;
|
|
414
|
+
if (resetAfterWrite) resetPerfMetrics();
|
|
415
|
+
return true;
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function checkpointPerfMetricsCapture() {
|
|
421
|
+
flushed = false;
|
|
422
|
+
writePerfMetricsCapture("checkpoint", true);
|
|
423
|
+
}
|
|
424
|
+
function flushPerfMetricsCapture(reason = "exit") {
|
|
425
|
+
if (flushed || !shouldCapture()) return;
|
|
426
|
+
flushed = true;
|
|
427
|
+
writePerfMetricsCapture(reason, false);
|
|
428
|
+
}
|
|
429
|
+
function installPerfMetricsCapture(options = {}) {
|
|
430
|
+
captureFilePath = options.filePath ?? process.env[PERF_METRICS_FILE_ENV];
|
|
431
|
+
if (!shouldCapture()) return;
|
|
432
|
+
resetPerfMetrics();
|
|
433
|
+
captureRole = options.role ?? captureRole;
|
|
434
|
+
captureArgv = options.argv ?? [];
|
|
435
|
+
captureSequence = 0;
|
|
436
|
+
flushed = false;
|
|
437
|
+
if (installed) return;
|
|
438
|
+
installed = true;
|
|
439
|
+
process.once("exit", () => {
|
|
440
|
+
flushPerfMetricsCapture("exit");
|
|
441
|
+
});
|
|
442
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
443
|
+
const handler = () => {
|
|
444
|
+
flushPerfMetricsCapture("signal");
|
|
445
|
+
process.removeListener(signal, handler);
|
|
446
|
+
process.kill(process.pid, signal);
|
|
447
|
+
};
|
|
448
|
+
process.once(signal, handler);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/cli/queue/owner-turn-controller.ts
|
|
453
|
+
var QueueOwnerTurnController = class {
|
|
454
|
+
options;
|
|
455
|
+
state = "idle";
|
|
456
|
+
pendingCancel = false;
|
|
457
|
+
activeController;
|
|
458
|
+
constructor(options) {
|
|
459
|
+
this.options = options;
|
|
460
|
+
}
|
|
461
|
+
get lifecycleState() {
|
|
462
|
+
return this.state;
|
|
463
|
+
}
|
|
464
|
+
get hasPendingCancel() {
|
|
465
|
+
return this.pendingCancel;
|
|
466
|
+
}
|
|
467
|
+
beginTurn() {
|
|
468
|
+
this.state = "starting";
|
|
469
|
+
this.pendingCancel = false;
|
|
470
|
+
}
|
|
471
|
+
markPromptActive() {
|
|
472
|
+
if (this.state === "starting" || this.state === "active") this.state = "active";
|
|
473
|
+
}
|
|
474
|
+
endTurn() {
|
|
475
|
+
this.state = "idle";
|
|
476
|
+
this.pendingCancel = false;
|
|
477
|
+
}
|
|
478
|
+
beginClosing() {
|
|
479
|
+
this.state = "closing";
|
|
480
|
+
this.pendingCancel = false;
|
|
481
|
+
this.activeController = void 0;
|
|
482
|
+
}
|
|
483
|
+
setActiveController(controller) {
|
|
484
|
+
this.activeController = controller;
|
|
485
|
+
}
|
|
486
|
+
clearActiveController() {
|
|
487
|
+
this.activeController = void 0;
|
|
488
|
+
}
|
|
489
|
+
assertCanHandleControlRequest() {
|
|
490
|
+
if (this.state === "closing") throw new QueueConnectionError("Queue owner is closing", {
|
|
491
|
+
detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
|
|
492
|
+
origin: "queue",
|
|
493
|
+
retryable: true
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
async requestCancel() {
|
|
497
|
+
const activeController = this.activeController;
|
|
498
|
+
if (activeController?.hasActivePrompt()) {
|
|
499
|
+
const cancelled = await activeController.requestCancelActivePrompt();
|
|
500
|
+
if (cancelled) this.pendingCancel = false;
|
|
501
|
+
return cancelled;
|
|
502
|
+
}
|
|
503
|
+
if (this.state === "starting" || this.state === "active") {
|
|
504
|
+
this.pendingCancel = true;
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
async applyPendingCancel() {
|
|
510
|
+
const activeController = this.activeController;
|
|
511
|
+
if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) return false;
|
|
512
|
+
const cancelled = await activeController.requestCancelActivePrompt();
|
|
513
|
+
if (cancelled) this.pendingCancel = false;
|
|
514
|
+
return cancelled;
|
|
515
|
+
}
|
|
516
|
+
async setSessionMode(modeId, timeoutMs) {
|
|
517
|
+
this.assertCanHandleControlRequest();
|
|
518
|
+
const activeController = this.activeController;
|
|
519
|
+
if (activeController) {
|
|
520
|
+
await this.options.withTimeout(async () => await activeController.setSessionMode(modeId), timeoutMs);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
await this.options.setSessionModeFallback(modeId, timeoutMs);
|
|
524
|
+
}
|
|
525
|
+
async setSessionModel(modelId, timeoutMs) {
|
|
526
|
+
this.assertCanHandleControlRequest();
|
|
527
|
+
const activeController = this.activeController;
|
|
528
|
+
if (activeController) {
|
|
529
|
+
await this.options.withTimeout(async () => await activeController.setSessionModel(modelId), timeoutMs);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
await this.options.setSessionModelFallback(modelId, timeoutMs);
|
|
533
|
+
}
|
|
534
|
+
async setSessionConfigOption(configId, value, timeoutMs) {
|
|
535
|
+
this.assertCanHandleControlRequest();
|
|
536
|
+
const activeController = this.activeController;
|
|
537
|
+
if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionConfigOption(configId, value), timeoutMs);
|
|
538
|
+
return await this.options.setSessionConfigOptionFallback(configId, value, timeoutMs);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/cli/session/queue-owner-process.ts
|
|
543
|
+
function sanitizeQueueOwnerExecArgv(execArgv = process.execArgv) {
|
|
544
|
+
const sanitized = [];
|
|
545
|
+
for (let index = 0; index < execArgv.length; index += 1) {
|
|
546
|
+
const value = execArgv[index];
|
|
547
|
+
if (value === "--experimental-test-coverage" || value === "--test") continue;
|
|
548
|
+
if (value === "--test-name-pattern" || value === "--test-reporter" || value === "--test-reporter-destination") {
|
|
549
|
+
index += 1;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (value.startsWith("--test-")) continue;
|
|
553
|
+
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=")) {
|
|
554
|
+
if (value === "--inspect" || value === "--inspect-brk" || value === "--inspect-port" || value === "--inspect-publish-uid" || value === "--debug-port") index += 1;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
sanitized.push(value);
|
|
558
|
+
}
|
|
559
|
+
return sanitized;
|
|
560
|
+
}
|
|
561
|
+
function buildQueueOwnerArgOverride(entryPath, execArgv = process.execArgv) {
|
|
562
|
+
const sanitized = sanitizeQueueOwnerExecArgv(execArgv);
|
|
563
|
+
if (sanitized.length === 0) return null;
|
|
564
|
+
return JSON.stringify([
|
|
565
|
+
...sanitized,
|
|
566
|
+
entryPath,
|
|
567
|
+
"__queue-owner"
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
function resolveQueueOwnerSpawnArgs(argv = process.argv) {
|
|
571
|
+
const override = process.env.ACPX_QUEUE_OWNER_ARGS;
|
|
572
|
+
if (override) {
|
|
573
|
+
const parsed = JSON.parse(override);
|
|
574
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed.every((value) => typeof value === "string" && value.length > 0)) return [...parsed];
|
|
575
|
+
throw new Error("acpx self-spawn failed: invalid ACPX_QUEUE_OWNER_ARGS");
|
|
576
|
+
}
|
|
577
|
+
const entry = argv[1];
|
|
578
|
+
if (!entry || entry.trim().length === 0) throw new Error("acpx self-spawn failed: missing CLI entry path");
|
|
579
|
+
return [realpathSync(entry), "__queue-owner"];
|
|
580
|
+
}
|
|
581
|
+
function queueOwnerRuntimeOptionsFromSend(options) {
|
|
582
|
+
return {
|
|
583
|
+
sessionId: options.sessionId,
|
|
584
|
+
mcpServers: options.mcpServers,
|
|
585
|
+
permissionMode: options.permissionMode,
|
|
586
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
587
|
+
authCredentials: options.authCredentials,
|
|
588
|
+
authPolicy: options.authPolicy,
|
|
589
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
590
|
+
verbose: options.verbose,
|
|
591
|
+
ttlMs: options.ttlMs,
|
|
592
|
+
maxQueueDepth: options.maxQueueDepth,
|
|
593
|
+
promptRetries: options.promptRetries
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function buildQueueOwnerSpawnOptions(payload) {
|
|
597
|
+
return {
|
|
598
|
+
detached: true,
|
|
599
|
+
stdio: "ignore",
|
|
600
|
+
env: {
|
|
601
|
+
...process.env,
|
|
602
|
+
ACPX_QUEUE_OWNER_PAYLOAD: payload
|
|
603
|
+
},
|
|
604
|
+
windowsHide: true
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function spawnQueueOwnerProcess(options) {
|
|
608
|
+
const payload = JSON.stringify(options);
|
|
609
|
+
spawn(process.execPath, resolveQueueOwnerSpawnArgs(), buildQueueOwnerSpawnOptions(payload)).unref();
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/session/events.ts
|
|
613
|
+
const LOCK_RETRY_MS = 15;
|
|
614
|
+
const EVENT_LOCK_STALE_MS = 15e3;
|
|
615
|
+
async function ensureSessionDir() {
|
|
616
|
+
await fs$1.mkdir(sessionBaseDir(), { recursive: true });
|
|
617
|
+
}
|
|
618
|
+
async function pathExists(filePath) {
|
|
619
|
+
try {
|
|
620
|
+
await fs$1.access(filePath);
|
|
621
|
+
return true;
|
|
622
|
+
} catch {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async function statSize(filePath) {
|
|
627
|
+
try {
|
|
628
|
+
return (await fs$1.stat(filePath)).size;
|
|
629
|
+
} catch {
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function countExistingSegments(sessionId, maxSegments) {
|
|
634
|
+
let count = 0;
|
|
635
|
+
for (let segment = 1; segment <= maxSegments; segment += 1) if (await pathExists(sessionEventSegmentPath(sessionId, segment))) count += 1;
|
|
636
|
+
if (await pathExists(sessionEventActivePath(sessionId))) count += 1;
|
|
637
|
+
return count;
|
|
638
|
+
}
|
|
639
|
+
async function rotateSegments(sessionId, maxSegments) {
|
|
640
|
+
const active = sessionEventActivePath(sessionId);
|
|
641
|
+
const overflow = sessionEventSegmentPath(sessionId, maxSegments);
|
|
642
|
+
await fs$1.unlink(overflow).catch((error) => {
|
|
643
|
+
if (error.code !== "ENOENT") throw error;
|
|
644
|
+
});
|
|
645
|
+
for (let segment = maxSegments - 1; segment >= 1; segment -= 1) {
|
|
646
|
+
const from = sessionEventSegmentPath(sessionId, segment);
|
|
647
|
+
const to = sessionEventSegmentPath(sessionId, segment + 1);
|
|
648
|
+
if (!await pathExists(from)) continue;
|
|
649
|
+
await fs$1.rename(from, to);
|
|
650
|
+
}
|
|
651
|
+
if (await pathExists(active)) await fs$1.rename(active, sessionEventSegmentPath(sessionId, 1));
|
|
652
|
+
}
|
|
653
|
+
function parseEventLockPayload(raw) {
|
|
654
|
+
try {
|
|
655
|
+
const parsed = JSON.parse(raw);
|
|
656
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
657
|
+
const record = parsed;
|
|
658
|
+
return {
|
|
659
|
+
pid: typeof record.pid === "number" ? record.pid : void 0,
|
|
660
|
+
created_at: typeof record.created_at === "string" ? record.created_at : void 0
|
|
661
|
+
};
|
|
662
|
+
} catch {
|
|
663
|
+
return {};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function removeStaleEventLock(lockPath) {
|
|
667
|
+
try {
|
|
668
|
+
const parsed = parseEventLockPayload(await fs$1.readFile(lockPath, "utf8"));
|
|
669
|
+
const createdAtMs = parsed.created_at ? Date.parse(parsed.created_at) : NaN;
|
|
670
|
+
const lockAgeMs = Number.isFinite(createdAtMs) ? Date.now() - createdAtMs : Number.POSITIVE_INFINITY;
|
|
671
|
+
if (isProcessAlive(parsed.pid) && lockAgeMs <= EVENT_LOCK_STALE_MS) return false;
|
|
672
|
+
await fs$1.unlink(lockPath);
|
|
673
|
+
incrementPerfCounter("session.events.stale_lock_recovered");
|
|
674
|
+
return true;
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (error.code === "ENOENT") return true;
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function acquireEventsLock(sessionId) {
|
|
681
|
+
await ensureSessionDir();
|
|
682
|
+
const lockPath = sessionEventLockPath(sessionId);
|
|
683
|
+
const payload = JSON.stringify({
|
|
684
|
+
pid: process.pid,
|
|
685
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
686
|
+
}, null, 2);
|
|
687
|
+
for (;;) try {
|
|
688
|
+
await fs$1.writeFile(lockPath, `${payload}\n`, {
|
|
689
|
+
encoding: "utf8",
|
|
690
|
+
flag: "wx"
|
|
691
|
+
});
|
|
692
|
+
return { filePath: lockPath };
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (error.code !== "EEXIST") throw error;
|
|
695
|
+
if (await removeStaleEventLock(lockPath)) continue;
|
|
696
|
+
await new Promise((resolve) => {
|
|
697
|
+
setTimeout(resolve, LOCK_RETRY_MS);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function releaseEventsLock(lock) {
|
|
702
|
+
await fs$1.unlink(lock.filePath).catch((error) => {
|
|
703
|
+
if (error.code !== "ENOENT") throw error;
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
var SessionEventWriter = class SessionEventWriter {
|
|
707
|
+
record;
|
|
708
|
+
lock;
|
|
709
|
+
maxSegmentBytes;
|
|
710
|
+
maxSegments;
|
|
711
|
+
activePath;
|
|
712
|
+
activeSizeBytes;
|
|
713
|
+
segmentCount;
|
|
714
|
+
closed = false;
|
|
715
|
+
constructor(record, lock, options, state) {
|
|
716
|
+
this.record = record;
|
|
717
|
+
this.lock = lock;
|
|
718
|
+
this.maxSegmentBytes = options.maxSegmentBytes;
|
|
719
|
+
this.maxSegments = options.maxSegments;
|
|
720
|
+
this.activePath = state.activePath;
|
|
721
|
+
this.activeSizeBytes = state.activeSizeBytes;
|
|
722
|
+
this.segmentCount = state.segmentCount;
|
|
723
|
+
}
|
|
724
|
+
static async open(record, options = {}) {
|
|
725
|
+
const lock = await acquireEventsLock(record.acpxRecordId);
|
|
726
|
+
const maxSegmentBytes = options.maxSegmentBytes ?? record.eventLog.max_segment_bytes ?? 67108864;
|
|
727
|
+
const maxSegments = options.maxSegments ?? record.eventLog.max_segments ?? 5;
|
|
728
|
+
const activePath = sessionEventActivePath(record.acpxRecordId);
|
|
729
|
+
const activeSizeBytes = await statSize(activePath);
|
|
730
|
+
const segmentCount = Number.isInteger(record.eventLog.segment_count) && record.eventLog.segment_count > 0 ? record.eventLog.segment_count : await countExistingSegments(record.acpxRecordId, maxSegments) || 1;
|
|
731
|
+
return new SessionEventWriter(record, lock, {
|
|
732
|
+
maxSegmentBytes,
|
|
733
|
+
maxSegments
|
|
734
|
+
}, {
|
|
735
|
+
activePath,
|
|
736
|
+
activeSizeBytes,
|
|
737
|
+
segmentCount
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
getRecord() {
|
|
741
|
+
return this.record;
|
|
742
|
+
}
|
|
743
|
+
async appendMessage(message, options = {}) {
|
|
744
|
+
await this.appendMessages([message], options);
|
|
745
|
+
}
|
|
746
|
+
async appendMessages(messages, options = {}) {
|
|
747
|
+
if (this.closed) throw new Error("SessionEventWriter is closed");
|
|
748
|
+
if (messages.length === 0) return;
|
|
749
|
+
await ensureSessionDir();
|
|
750
|
+
await measurePerf("session.events.append_batch", async () => {
|
|
751
|
+
for (const message of messages) {
|
|
752
|
+
if (!isAcpJsonRpcMessage(message)) throw new Error("Attempted to persist invalid ACP JSON-RPC payload");
|
|
753
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
754
|
+
const lineBytes = Buffer.byteLength(line);
|
|
755
|
+
if (this.activeSizeBytes > 0 && this.activeSizeBytes + lineBytes > this.maxSegmentBytes) {
|
|
756
|
+
await rotateSegments(this.record.acpxRecordId, this.maxSegments);
|
|
757
|
+
this.activePath = sessionEventActivePath(this.record.acpxRecordId);
|
|
758
|
+
this.activeSizeBytes = 0;
|
|
759
|
+
this.segmentCount = Math.min(this.segmentCount + 1, this.maxSegments);
|
|
760
|
+
incrementPerfCounter("session.events.rotate");
|
|
761
|
+
}
|
|
762
|
+
await fs$1.appendFile(this.activePath, line, "utf8");
|
|
763
|
+
this.activeSizeBytes += lineBytes;
|
|
764
|
+
this.record.lastSeq += 1;
|
|
765
|
+
if (Object.hasOwn(message, "id")) {
|
|
766
|
+
const id = message.id;
|
|
767
|
+
if (typeof id === "string" || typeof id === "number") this.record.lastRequestId = String(id);
|
|
768
|
+
}
|
|
769
|
+
const writeTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
770
|
+
this.record.lastUsedAt = writeTs;
|
|
771
|
+
this.record.eventLog = {
|
|
772
|
+
active_path: this.activePath,
|
|
773
|
+
segment_count: this.segmentCount,
|
|
774
|
+
max_segment_bytes: this.maxSegmentBytes,
|
|
775
|
+
max_segments: this.maxSegments,
|
|
776
|
+
last_write_at: writeTs,
|
|
777
|
+
last_write_error: null
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
if (options.checkpoint === true) await writeSessionRecord(this.record);
|
|
782
|
+
}
|
|
783
|
+
async checkpoint() {
|
|
784
|
+
if (this.closed) throw new Error("SessionEventWriter is closed");
|
|
785
|
+
await writeSessionRecord(this.record);
|
|
786
|
+
}
|
|
787
|
+
async close(options = {}) {
|
|
788
|
+
if (this.closed) return;
|
|
789
|
+
try {
|
|
790
|
+
if (options.checkpoint !== false) await writeSessionRecord(this.record);
|
|
791
|
+
} finally {
|
|
792
|
+
this.closed = true;
|
|
793
|
+
await releaseEventsLock(this.lock);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/cli/session/runtime.ts
|
|
799
|
+
const INTERRUPT_CANCEL_WAIT_MS = 2500;
|
|
800
|
+
var QueueTaskOutputFormatter = class {
|
|
801
|
+
requestId;
|
|
802
|
+
send;
|
|
803
|
+
constructor(task) {
|
|
804
|
+
this.requestId = task.requestId;
|
|
805
|
+
this.send = task.send;
|
|
806
|
+
}
|
|
807
|
+
setContext(_context) {}
|
|
808
|
+
onAcpMessage(message) {
|
|
809
|
+
this.send({
|
|
810
|
+
type: "event",
|
|
811
|
+
requestId: this.requestId,
|
|
812
|
+
message
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
onError(params) {
|
|
816
|
+
this.send({
|
|
817
|
+
type: "error",
|
|
818
|
+
requestId: this.requestId,
|
|
819
|
+
code: params.code,
|
|
820
|
+
detailCode: params.detailCode,
|
|
821
|
+
origin: params.origin,
|
|
822
|
+
message: params.message,
|
|
823
|
+
retryable: params.retryable,
|
|
824
|
+
acp: params.acp
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
flush() {}
|
|
828
|
+
};
|
|
829
|
+
const DISCARD_OUTPUT_FORMATTER = {
|
|
830
|
+
setContext() {},
|
|
831
|
+
onAcpMessage() {},
|
|
832
|
+
onError() {},
|
|
833
|
+
flush() {}
|
|
834
|
+
};
|
|
835
|
+
function toPromptResult(stopReason, sessionId, client) {
|
|
836
|
+
return {
|
|
837
|
+
stopReason,
|
|
838
|
+
sessionId,
|
|
839
|
+
permissionStats: client.getPermissionStats()
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async function applyRequestedModelIfAdvertised(params) {
|
|
843
|
+
const requestedModel = typeof params.requestedModel === "string" ? params.requestedModel.trim() : "";
|
|
844
|
+
if (!requestedModel || !params.models) return false;
|
|
845
|
+
if (params.models.currentModelId === requestedModel) return true;
|
|
846
|
+
await withTimeout(params.client.setSessionModel(params.sessionId, requestedModel), params.timeoutMs);
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
function jsonRpcIdKey(value) {
|
|
850
|
+
if (typeof value === "string") return `s:${value}`;
|
|
851
|
+
if (typeof value === "number" && Number.isFinite(value)) return `n:${value}`;
|
|
852
|
+
}
|
|
853
|
+
function extractJsonRpcRequestInfo(message) {
|
|
854
|
+
const candidate = message;
|
|
855
|
+
if (typeof candidate.method !== "string") return;
|
|
856
|
+
const idKey = jsonRpcIdKey(candidate.id);
|
|
857
|
+
if (!idKey) return;
|
|
858
|
+
return {
|
|
859
|
+
idKey,
|
|
860
|
+
method: candidate.method
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function extractJsonRpcResponseInfo(message) {
|
|
864
|
+
const candidate = message;
|
|
865
|
+
const idKey = jsonRpcIdKey(candidate.id);
|
|
866
|
+
if (!idKey) return;
|
|
867
|
+
const hasError = Object.hasOwn(candidate, "error");
|
|
868
|
+
if (!hasError && !Object.hasOwn(candidate, "result")) return;
|
|
869
|
+
return {
|
|
870
|
+
idKey,
|
|
871
|
+
hasError
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function filterRecoverableLoadFallbackOutput(messages) {
|
|
875
|
+
const requestMethodById = /* @__PURE__ */ new Map();
|
|
876
|
+
const failedLoadRequestIds = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const message of messages) {
|
|
878
|
+
const request = extractJsonRpcRequestInfo(message);
|
|
879
|
+
if (request) {
|
|
880
|
+
requestMethodById.set(request.idKey, request.method);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const response = extractJsonRpcResponseInfo(message);
|
|
884
|
+
if (!response || !response.hasError) continue;
|
|
885
|
+
if (requestMethodById.get(response.idKey) === "session/load") failedLoadRequestIds.add(response.idKey);
|
|
886
|
+
}
|
|
887
|
+
if (failedLoadRequestIds.size === 0) return messages;
|
|
888
|
+
return messages.filter((message) => {
|
|
889
|
+
const request = extractJsonRpcRequestInfo(message);
|
|
890
|
+
if (request && request.method === "session/load" && failedLoadRequestIds.has(request.idKey)) return false;
|
|
891
|
+
const response = extractJsonRpcResponseInfo(message);
|
|
892
|
+
if (response && failedLoadRequestIds.has(response.idKey)) return false;
|
|
893
|
+
return true;
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
function emitPromptRetryNotice(params) {
|
|
897
|
+
if (params.suppressSdkConsoleErrors) return;
|
|
898
|
+
process.stderr.write(`[acpx] prompt failed (${formatErrorMessage(params.error)}), retrying in ${params.delayMs}ms (attempt ${params.attempt}/${params.maxRetries})\n`);
|
|
899
|
+
}
|
|
900
|
+
async function runQueuedTask(sessionRecordId, task, options) {
|
|
901
|
+
const outputFormatter = task.waitForCompletion ? new QueueTaskOutputFormatter(task) : DISCARD_OUTPUT_FORMATTER;
|
|
902
|
+
try {
|
|
903
|
+
const result = await runSessionPrompt({
|
|
904
|
+
sessionRecordId,
|
|
905
|
+
mcpServers: options.mcpServers,
|
|
906
|
+
prompt: task.prompt ?? textPrompt(task.message),
|
|
907
|
+
permissionMode: task.permissionMode,
|
|
908
|
+
resumePolicy: task.resumePolicy,
|
|
909
|
+
nonInteractivePermissions: task.nonInteractivePermissions ?? options.nonInteractivePermissions,
|
|
910
|
+
authCredentials: options.authCredentials,
|
|
911
|
+
authPolicy: options.authPolicy,
|
|
912
|
+
outputFormatter,
|
|
913
|
+
timeoutMs: task.timeoutMs,
|
|
914
|
+
suppressSdkConsoleErrors: task.suppressSdkConsoleErrors ?? options.suppressSdkConsoleErrors,
|
|
915
|
+
verbose: options.verbose,
|
|
916
|
+
promptRetries: options.promptRetries,
|
|
917
|
+
onClientAvailable: options.onClientAvailable,
|
|
918
|
+
onClientClosed: options.onClientClosed,
|
|
919
|
+
onPromptActive: options.onPromptActive,
|
|
920
|
+
client: options.sharedClient
|
|
921
|
+
});
|
|
922
|
+
if (task.waitForCompletion) task.send({
|
|
923
|
+
type: "result",
|
|
924
|
+
requestId: task.requestId,
|
|
925
|
+
result
|
|
926
|
+
});
|
|
927
|
+
} catch (error) {
|
|
928
|
+
const normalizedError = normalizeOutputError(error, {
|
|
929
|
+
origin: "runtime",
|
|
930
|
+
detailCode: "QUEUE_RUNTIME_PROMPT_FAILED"
|
|
931
|
+
});
|
|
932
|
+
const alreadyEmitted = error.outputAlreadyEmitted === true;
|
|
933
|
+
if (task.waitForCompletion) task.send({
|
|
934
|
+
type: "error",
|
|
935
|
+
requestId: task.requestId,
|
|
936
|
+
code: normalizedError.code,
|
|
937
|
+
detailCode: normalizedError.detailCode,
|
|
938
|
+
origin: normalizedError.origin,
|
|
939
|
+
message: normalizedError.message,
|
|
940
|
+
retryable: normalizedError.retryable,
|
|
941
|
+
acp: normalizedError.acp,
|
|
942
|
+
outputAlreadyEmitted: alreadyEmitted
|
|
943
|
+
});
|
|
944
|
+
if (error instanceof InterruptedError) throw error;
|
|
945
|
+
} finally {
|
|
946
|
+
task.close();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async function runSessionPrompt(options) {
|
|
950
|
+
const stopTotalTimer = startPerfTimer("runtime.prompt.total");
|
|
951
|
+
const output = options.outputFormatter;
|
|
952
|
+
const record = await measurePerf("session.resolve_prompt_record", async () => {
|
|
953
|
+
return await resolveSessionRecord(options.sessionRecordId);
|
|
954
|
+
});
|
|
955
|
+
const conversation = cloneSessionConversation(record);
|
|
956
|
+
let acpxState = cloneSessionAcpxState(record.acpx);
|
|
957
|
+
const promptMessageId = recordPromptSubmission(conversation, options.prompt, isoNow());
|
|
958
|
+
output.setContext({ sessionId: record.acpxRecordId });
|
|
959
|
+
const eventWriter = await measurePerf("session.events.open", async () => {
|
|
960
|
+
return await SessionEventWriter.open(record);
|
|
961
|
+
});
|
|
962
|
+
const pendingMessages = [];
|
|
963
|
+
const pendingConnectOutputMessages = [];
|
|
964
|
+
let bufferingConnectOutput = true;
|
|
965
|
+
let promptTurnActive = false;
|
|
966
|
+
let promptTurnHadSideEffects = false;
|
|
967
|
+
let sawAcpMessage = false;
|
|
968
|
+
let eventWriterClosed = false;
|
|
969
|
+
const closeEventWriter = async (checkpoint) => {
|
|
970
|
+
if (eventWriterClosed) return;
|
|
971
|
+
eventWriterClosed = true;
|
|
972
|
+
await eventWriter.close({ checkpoint });
|
|
973
|
+
};
|
|
974
|
+
const flushPendingMessages = async (checkpoint = false) => {
|
|
975
|
+
if (pendingMessages.length === 0) return;
|
|
976
|
+
const batch = pendingMessages.splice(0, pendingMessages.length);
|
|
977
|
+
await measurePerf("session.events.flush_pending", async () => {
|
|
978
|
+
await eventWriter.appendMessages(batch, { checkpoint });
|
|
979
|
+
});
|
|
980
|
+
};
|
|
981
|
+
const ownClient = options.client == null;
|
|
982
|
+
const client = options.client ?? new AcpClient({
|
|
983
|
+
agentCommand: record.agentCommand,
|
|
984
|
+
cwd: absolutePath(record.cwd),
|
|
985
|
+
mcpServers: options.mcpServers,
|
|
986
|
+
permissionMode: options.permissionMode,
|
|
987
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
988
|
+
authCredentials: options.authCredentials,
|
|
989
|
+
authPolicy: options.authPolicy,
|
|
990
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
991
|
+
verbose: options.verbose,
|
|
992
|
+
sessionOptions: sessionOptionsFromRecord(record)
|
|
993
|
+
});
|
|
994
|
+
client.updateRuntimeOptions({
|
|
995
|
+
permissionMode: options.permissionMode,
|
|
996
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
997
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
998
|
+
verbose: options.verbose
|
|
999
|
+
});
|
|
1000
|
+
client.setEventHandlers({
|
|
1001
|
+
onAcpMessage: (direction, message) => {
|
|
1002
|
+
sawAcpMessage = true;
|
|
1003
|
+
pendingMessages.push(message);
|
|
1004
|
+
options.onAcpMessage?.(direction, message);
|
|
1005
|
+
},
|
|
1006
|
+
onAcpOutputMessage: (_direction, message) => {
|
|
1007
|
+
if (bufferingConnectOutput) {
|
|
1008
|
+
pendingConnectOutputMessages.push(message);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
output.onAcpMessage(message);
|
|
1012
|
+
},
|
|
1013
|
+
onSessionUpdate: (notification) => {
|
|
1014
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
1015
|
+
acpxState = recordSessionUpdate(conversation, acpxState, notification);
|
|
1016
|
+
trimConversationForRuntime(conversation);
|
|
1017
|
+
options.onSessionUpdate?.(notification);
|
|
1018
|
+
},
|
|
1019
|
+
onClientOperation: (operation) => {
|
|
1020
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
1021
|
+
acpxState = recordClientOperation(conversation, acpxState, operation);
|
|
1022
|
+
trimConversationForRuntime(conversation);
|
|
1023
|
+
options.onClientOperation?.(operation);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
let activeSessionIdForControl = record.acpSessionId;
|
|
1027
|
+
let notifiedClientAvailable = false;
|
|
1028
|
+
const activeController = {
|
|
1029
|
+
hasActivePrompt: () => client.hasActivePrompt(),
|
|
1030
|
+
requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
|
|
1031
|
+
setSessionMode: async (modeId) => {
|
|
1032
|
+
await client.setSessionMode(activeSessionIdForControl, modeId);
|
|
1033
|
+
},
|
|
1034
|
+
setSessionModel: async (modelId) => {
|
|
1035
|
+
await client.setSessionModel(activeSessionIdForControl, modelId);
|
|
1036
|
+
},
|
|
1037
|
+
setSessionConfigOption: async (configId, value) => {
|
|
1038
|
+
return await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
try {
|
|
1042
|
+
return await withInterrupt(async () => {
|
|
1043
|
+
const connectStartedAt = Date.now();
|
|
1044
|
+
const { sessionId: activeSessionId, resumed, loadError } = await measurePerf("runtime.connect_and_load", async () => {
|
|
1045
|
+
try {
|
|
1046
|
+
return await connectAndLoadSession({
|
|
1047
|
+
client,
|
|
1048
|
+
record,
|
|
1049
|
+
resumePolicy: options.resumePolicy,
|
|
1050
|
+
timeoutMs: options.timeoutMs,
|
|
1051
|
+
verbose: options.verbose,
|
|
1052
|
+
activeController,
|
|
1053
|
+
onClientAvailable: (controller) => {
|
|
1054
|
+
options.onClientAvailable?.(controller);
|
|
1055
|
+
notifiedClientAvailable = true;
|
|
1056
|
+
},
|
|
1057
|
+
onConnectedRecord: (connectedRecord) => {
|
|
1058
|
+
connectedRecord.lastPromptAt = isoNow();
|
|
1059
|
+
},
|
|
1060
|
+
onSessionIdResolved: (sessionId) => {
|
|
1061
|
+
activeSessionIdForControl = sessionId;
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
bufferingConnectOutput = false;
|
|
1066
|
+
for (const message of pendingConnectOutputMessages) output.onAcpMessage(message);
|
|
1067
|
+
pendingConnectOutputMessages.length = 0;
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
bufferingConnectOutput = false;
|
|
1072
|
+
const connectOutputMessages = loadError == null ? pendingConnectOutputMessages : filterRecoverableLoadFallbackOutput(pendingConnectOutputMessages);
|
|
1073
|
+
for (const message of connectOutputMessages) output.onAcpMessage(message);
|
|
1074
|
+
pendingConnectOutputMessages.length = 0;
|
|
1075
|
+
if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.connect_and_load", Date.now() - connectStartedAt)}\n`);
|
|
1076
|
+
output.setContext({ sessionId: record.acpxRecordId });
|
|
1077
|
+
await flushPendingMessages(false);
|
|
1078
|
+
const maxRetries = options.promptRetries ?? 0;
|
|
1079
|
+
let response;
|
|
1080
|
+
promptTurnActive = true;
|
|
1081
|
+
for (let attempt = 0;; attempt++) try {
|
|
1082
|
+
const promptStartedAt = Date.now();
|
|
1083
|
+
response = await measurePerf("runtime.prompt.agent_turn", async () => {
|
|
1084
|
+
return await runPromptTurn({
|
|
1085
|
+
client,
|
|
1086
|
+
sessionId: activeSessionId,
|
|
1087
|
+
prompt: options.prompt,
|
|
1088
|
+
timeoutMs: options.timeoutMs,
|
|
1089
|
+
conversation,
|
|
1090
|
+
promptMessageId,
|
|
1091
|
+
onPromptStarted: attempt === 0 && options.onPromptActive ? async () => {
|
|
1092
|
+
try {
|
|
1093
|
+
await options.onPromptActive?.();
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (options.verbose) process.stderr.write("[acpx] onPromptActive hook failed: " + formatErrorMessage(error) + "\n");
|
|
1096
|
+
}
|
|
1097
|
+
} : void 0
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.agent_turn", Date.now() - promptStartedAt)}\n`);
|
|
1101
|
+
break;
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
const snapshot = client.getAgentLifecycleSnapshot();
|
|
1104
|
+
const agentCrashed = snapshot.lastExit?.unexpectedDuringPrompt === true;
|
|
1105
|
+
if (attempt < maxRetries && !agentCrashed && !promptTurnHadSideEffects && isRetryablePromptError(error)) {
|
|
1106
|
+
const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
1107
|
+
emitPromptRetryNotice({
|
|
1108
|
+
error,
|
|
1109
|
+
delayMs,
|
|
1110
|
+
attempt: attempt + 1,
|
|
1111
|
+
maxRetries,
|
|
1112
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors
|
|
1113
|
+
});
|
|
1114
|
+
await waitMs(delayMs);
|
|
1115
|
+
if (!promptTurnHadSideEffects) continue;
|
|
1116
|
+
}
|
|
1117
|
+
promptTurnActive = false;
|
|
1118
|
+
applyLifecycleSnapshotToRecord(record, snapshot);
|
|
1119
|
+
const lastExit = snapshot.lastExit;
|
|
1120
|
+
if (lastExit?.unexpectedDuringPrompt && options.verbose) process.stderr.write("[acpx] agent disconnected during prompt (" + lastExit.reason + ", exit=" + lastExit.exitCode + ", signal=" + (lastExit.signal ?? "none") + ")\n");
|
|
1121
|
+
const normalizedError = normalizeOutputError(error, { origin: "runtime" });
|
|
1122
|
+
await flushPendingMessages(false).catch(() => {});
|
|
1123
|
+
output.flush();
|
|
1124
|
+
record.lastUsedAt = isoNow();
|
|
1125
|
+
applyConversation(record, conversation);
|
|
1126
|
+
record.acpx = acpxState;
|
|
1127
|
+
const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
|
|
1128
|
+
propagated.outputAlreadyEmitted = sawAcpMessage;
|
|
1129
|
+
propagated.normalizedOutputError = normalizedError;
|
|
1130
|
+
throw propagated;
|
|
1131
|
+
}
|
|
1132
|
+
promptTurnActive = false;
|
|
1133
|
+
await flushPendingMessages(false);
|
|
1134
|
+
output.flush();
|
|
1135
|
+
record.lastUsedAt = isoNow();
|
|
1136
|
+
record.closed = false;
|
|
1137
|
+
record.closedAt = void 0;
|
|
1138
|
+
record.protocolVersion = client.initializeResult?.protocolVersion;
|
|
1139
|
+
record.agentCapabilities = client.initializeResult?.agentCapabilities;
|
|
1140
|
+
applyConversation(record, conversation);
|
|
1141
|
+
record.acpx = acpxState;
|
|
1142
|
+
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
1143
|
+
stopTotalTimer();
|
|
1144
|
+
return {
|
|
1145
|
+
...toPromptResult(response.stopReason, record.acpxRecordId, client),
|
|
1146
|
+
record,
|
|
1147
|
+
resumed,
|
|
1148
|
+
loadError
|
|
1149
|
+
};
|
|
1150
|
+
}, async () => {
|
|
1151
|
+
await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
|
|
1152
|
+
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
1153
|
+
record.lastUsedAt = isoNow();
|
|
1154
|
+
applyConversation(record, conversation);
|
|
1155
|
+
record.acpx = acpxState;
|
|
1156
|
+
await flushPendingMessages(false).catch(() => {});
|
|
1157
|
+
if (ownClient) await client.close();
|
|
1158
|
+
});
|
|
1159
|
+
} finally {
|
|
1160
|
+
if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", stopTotalTimer())}\n`);
|
|
1161
|
+
else stopTotalTimer();
|
|
1162
|
+
if (notifiedClientAvailable) options.onClientClosed?.();
|
|
1163
|
+
client.clearEventHandlers();
|
|
1164
|
+
if (ownClient) await client.close();
|
|
1165
|
+
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
|
|
1166
|
+
applyConversation(record, conversation);
|
|
1167
|
+
record.acpx = acpxState;
|
|
1168
|
+
await flushPendingMessages(false).catch(() => {});
|
|
1169
|
+
await closeEventWriter(true).catch(() => {});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function runOnce(options) {
|
|
1173
|
+
const output = options.outputFormatter;
|
|
1174
|
+
let promptTurnActive = false;
|
|
1175
|
+
let promptTurnHadSideEffects = false;
|
|
1176
|
+
const client = new AcpClient({
|
|
1177
|
+
agentCommand: options.agentCommand,
|
|
1178
|
+
cwd: absolutePath(options.cwd),
|
|
1179
|
+
mcpServers: options.mcpServers,
|
|
1180
|
+
permissionMode: options.permissionMode,
|
|
1181
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1182
|
+
authCredentials: options.authCredentials,
|
|
1183
|
+
authPolicy: options.authPolicy,
|
|
1184
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1185
|
+
verbose: options.verbose,
|
|
1186
|
+
onAcpMessage: options.onAcpMessage,
|
|
1187
|
+
onAcpOutputMessage: (_direction, message) => output.onAcpMessage(message),
|
|
1188
|
+
onSessionUpdate: (notification) => {
|
|
1189
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
1190
|
+
options.onSessionUpdate?.(notification);
|
|
1191
|
+
},
|
|
1192
|
+
onClientOperation: (operation) => {
|
|
1193
|
+
if (promptTurnActive) promptTurnHadSideEffects = true;
|
|
1194
|
+
options.onClientOperation?.(operation);
|
|
1195
|
+
},
|
|
1196
|
+
sessionOptions: options.sessionOptions
|
|
1197
|
+
});
|
|
1198
|
+
try {
|
|
1199
|
+
return await withInterrupt(async () => {
|
|
1200
|
+
await measurePerf("runtime.exec.start", async () => {
|
|
1201
|
+
await withTimeout(client.start(), options.timeoutMs);
|
|
1202
|
+
});
|
|
1203
|
+
const createdSession = await measurePerf("runtime.exec.create_session", async () => {
|
|
1204
|
+
return await withTimeout(client.createSession(absolutePath(options.cwd)), options.timeoutMs);
|
|
1205
|
+
});
|
|
1206
|
+
const sessionId = createdSession.sessionId;
|
|
1207
|
+
await applyRequestedModelIfAdvertised({
|
|
1208
|
+
client,
|
|
1209
|
+
sessionId,
|
|
1210
|
+
requestedModel: options.sessionOptions?.model,
|
|
1211
|
+
models: createdSession.models,
|
|
1212
|
+
timeoutMs: options.timeoutMs
|
|
1213
|
+
});
|
|
1214
|
+
output.setContext({ sessionId });
|
|
1215
|
+
const maxRetries = options.promptRetries ?? 0;
|
|
1216
|
+
let response;
|
|
1217
|
+
promptTurnActive = true;
|
|
1218
|
+
for (let attempt = 0;; attempt++) try {
|
|
1219
|
+
response = await measurePerf("runtime.exec.prompt", async () => {
|
|
1220
|
+
return await withTimeout(client.prompt(sessionId, options.prompt), options.timeoutMs);
|
|
1221
|
+
});
|
|
1222
|
+
break;
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (attempt < maxRetries && !promptTurnHadSideEffects && isRetryablePromptError(error)) {
|
|
1225
|
+
const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
1226
|
+
emitPromptRetryNotice({
|
|
1227
|
+
error,
|
|
1228
|
+
delayMs,
|
|
1229
|
+
attempt: attempt + 1,
|
|
1230
|
+
maxRetries,
|
|
1231
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors
|
|
1232
|
+
});
|
|
1233
|
+
await waitMs(delayMs);
|
|
1234
|
+
if (!promptTurnHadSideEffects) continue;
|
|
1235
|
+
}
|
|
1236
|
+
promptTurnActive = false;
|
|
1237
|
+
throw error;
|
|
1238
|
+
}
|
|
1239
|
+
promptTurnActive = false;
|
|
1240
|
+
output.flush();
|
|
1241
|
+
return toPromptResult(response.stopReason, sessionId, client);
|
|
1242
|
+
}, async () => {
|
|
1243
|
+
await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
|
|
1244
|
+
await client.close();
|
|
1245
|
+
});
|
|
1246
|
+
} finally {
|
|
1247
|
+
await client.close();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async function sendSessionDirect(options) {
|
|
1251
|
+
return await runSessionPrompt({
|
|
1252
|
+
sessionRecordId: options.sessionId,
|
|
1253
|
+
prompt: options.prompt,
|
|
1254
|
+
mcpServers: options.mcpServers,
|
|
1255
|
+
permissionMode: options.permissionMode,
|
|
1256
|
+
resumePolicy: options.resumePolicy,
|
|
1257
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1258
|
+
authCredentials: options.authCredentials,
|
|
1259
|
+
authPolicy: options.authPolicy,
|
|
1260
|
+
outputFormatter: options.outputFormatter,
|
|
1261
|
+
onAcpMessage: options.onAcpMessage,
|
|
1262
|
+
onSessionUpdate: options.onSessionUpdate,
|
|
1263
|
+
onClientOperation: options.onClientOperation,
|
|
1264
|
+
timeoutMs: options.timeoutMs,
|
|
1265
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1266
|
+
verbose: options.verbose,
|
|
1267
|
+
client: options.client
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
//#endregion
|
|
1271
|
+
//#region src/cli/session/queue-owner-runtime.ts
|
|
1272
|
+
const QUEUE_OWNER_STARTUP_MAX_ATTEMPTS = 120;
|
|
1273
|
+
const QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
1274
|
+
async function submitToRunningOwner(options, waitForCompletion) {
|
|
1275
|
+
return await trySubmitToRunningOwner({
|
|
1276
|
+
sessionId: options.sessionId,
|
|
1277
|
+
message: promptToDisplayText(options.prompt),
|
|
1278
|
+
prompt: options.prompt,
|
|
1279
|
+
permissionMode: options.permissionMode,
|
|
1280
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1281
|
+
outputFormatter: options.outputFormatter,
|
|
1282
|
+
errorEmissionPolicy: options.errorEmissionPolicy,
|
|
1283
|
+
timeoutMs: options.timeoutMs,
|
|
1284
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1285
|
+
waitForCompletion,
|
|
1286
|
+
verbose: options.verbose
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
async function runSessionQueueOwner(options) {
|
|
1290
|
+
const lease = await tryAcquireQueueOwnerLease(options.sessionId);
|
|
1291
|
+
if (!lease) return;
|
|
1292
|
+
const sessionRecord = await resolveSessionRecord(options.sessionId);
|
|
1293
|
+
let owner;
|
|
1294
|
+
let heartbeatTimer;
|
|
1295
|
+
const sharedClient = new AcpClient({
|
|
1296
|
+
agentCommand: sessionRecord.agentCommand,
|
|
1297
|
+
cwd: absolutePath(sessionRecord.cwd),
|
|
1298
|
+
mcpServers: options.mcpServers,
|
|
1299
|
+
permissionMode: options.permissionMode,
|
|
1300
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1301
|
+
authCredentials: options.authCredentials,
|
|
1302
|
+
authPolicy: options.authPolicy,
|
|
1303
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1304
|
+
verbose: options.verbose,
|
|
1305
|
+
sessionOptions: sessionOptionsFromRecord(sessionRecord)
|
|
1306
|
+
});
|
|
1307
|
+
const ttlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
|
|
1308
|
+
const maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth ?? 16));
|
|
1309
|
+
const taskPollTimeoutMs = ttlMs === 0 ? void 0 : ttlMs;
|
|
1310
|
+
const initialTaskPollTimeoutMs = taskPollTimeoutMs == null ? void 0 : Math.max(taskPollTimeoutMs, 1e3);
|
|
1311
|
+
const turnController = new QueueOwnerTurnController({
|
|
1312
|
+
withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
|
|
1313
|
+
setSessionModeFallback: async (modeId, timeoutMs) => {
|
|
1314
|
+
await runSessionSetModeDirect({
|
|
1315
|
+
sessionRecordId: options.sessionId,
|
|
1316
|
+
modeId,
|
|
1317
|
+
mcpServers: options.mcpServers,
|
|
1318
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1319
|
+
authCredentials: options.authCredentials,
|
|
1320
|
+
authPolicy: options.authPolicy,
|
|
1321
|
+
timeoutMs,
|
|
1322
|
+
verbose: options.verbose
|
|
1323
|
+
});
|
|
1324
|
+
},
|
|
1325
|
+
setSessionModelFallback: async (modelId, timeoutMs) => {
|
|
1326
|
+
await runSessionSetModelDirect({
|
|
1327
|
+
sessionRecordId: options.sessionId,
|
|
1328
|
+
modelId,
|
|
1329
|
+
mcpServers: options.mcpServers,
|
|
1330
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1331
|
+
authCredentials: options.authCredentials,
|
|
1332
|
+
authPolicy: options.authPolicy,
|
|
1333
|
+
timeoutMs,
|
|
1334
|
+
verbose: options.verbose
|
|
1335
|
+
});
|
|
1336
|
+
},
|
|
1337
|
+
setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
|
|
1338
|
+
return (await runSessionSetConfigOptionDirect({
|
|
1339
|
+
sessionRecordId: options.sessionId,
|
|
1340
|
+
configId,
|
|
1341
|
+
value,
|
|
1342
|
+
mcpServers: options.mcpServers,
|
|
1343
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1344
|
+
authCredentials: options.authCredentials,
|
|
1345
|
+
authPolicy: options.authPolicy,
|
|
1346
|
+
timeoutMs,
|
|
1347
|
+
verbose: options.verbose
|
|
1348
|
+
})).response;
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
const applyPendingCancel = async () => {
|
|
1352
|
+
return await turnController.applyPendingCancel();
|
|
1353
|
+
};
|
|
1354
|
+
const scheduleApplyPendingCancel = () => {
|
|
1355
|
+
applyPendingCancel().catch((error) => {
|
|
1356
|
+
if (options.verbose) process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
|
|
1357
|
+
});
|
|
1358
|
+
};
|
|
1359
|
+
const setActiveController = (controller) => {
|
|
1360
|
+
turnController.setActiveController(controller);
|
|
1361
|
+
scheduleApplyPendingCancel();
|
|
1362
|
+
};
|
|
1363
|
+
const clearActiveController = () => {
|
|
1364
|
+
turnController.clearActiveController();
|
|
1365
|
+
};
|
|
1366
|
+
const runPromptTurn = async (run) => {
|
|
1367
|
+
turnController.beginTurn();
|
|
1368
|
+
try {
|
|
1369
|
+
return await run();
|
|
1370
|
+
} finally {
|
|
1371
|
+
turnController.endTurn();
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
try {
|
|
1375
|
+
owner = await SessionQueueOwner.start(lease, {
|
|
1376
|
+
cancelPrompt: async () => {
|
|
1377
|
+
if (!await turnController.requestCancel()) return false;
|
|
1378
|
+
await applyPendingCancel();
|
|
1379
|
+
return true;
|
|
1380
|
+
},
|
|
1381
|
+
setSessionMode: async (modeId, timeoutMs) => {
|
|
1382
|
+
await turnController.setSessionMode(modeId, timeoutMs);
|
|
1383
|
+
},
|
|
1384
|
+
setSessionModel: async (modelId, timeoutMs) => {
|
|
1385
|
+
await turnController.setSessionModel(modelId, timeoutMs);
|
|
1386
|
+
},
|
|
1387
|
+
setSessionConfigOption: async (configId, value, timeoutMs) => {
|
|
1388
|
+
return await turnController.setSessionConfigOption(configId, value, timeoutMs);
|
|
1389
|
+
}
|
|
1390
|
+
}, {
|
|
1391
|
+
maxQueueDepth,
|
|
1392
|
+
onQueueDepthChanged: (queueDepth) => {
|
|
1393
|
+
setPerfGauge("queue.owner.depth", queueDepth);
|
|
1394
|
+
refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
if (options.verbose) process.stderr.write(`[acpx] queue owner ready for session ${options.sessionId} (ttlMs=${ttlMs}, maxQueueDepth=${maxQueueDepth})\n`);
|
|
1398
|
+
await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
|
|
1399
|
+
heartbeatTimer = setInterval(() => {
|
|
1400
|
+
refreshQueueOwnerLease(lease, { queueDepth: owner?.queueDepth() ?? 0 }).catch(() => {});
|
|
1401
|
+
}, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
|
|
1402
|
+
let isFirstTask = true;
|
|
1403
|
+
while (true) {
|
|
1404
|
+
const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
|
|
1405
|
+
const task = await owner.nextTask(pollTimeoutMs);
|
|
1406
|
+
if (!task) break;
|
|
1407
|
+
isFirstTask = false;
|
|
1408
|
+
await runPromptTurn(async () => {
|
|
1409
|
+
try {
|
|
1410
|
+
await runQueuedTask(options.sessionId, task, {
|
|
1411
|
+
sharedClient,
|
|
1412
|
+
verbose: options.verbose,
|
|
1413
|
+
mcpServers: options.mcpServers,
|
|
1414
|
+
nonInteractivePermissions: options.nonInteractivePermissions,
|
|
1415
|
+
authCredentials: options.authCredentials,
|
|
1416
|
+
authPolicy: options.authPolicy,
|
|
1417
|
+
suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
|
|
1418
|
+
promptRetries: options.promptRetries,
|
|
1419
|
+
onClientAvailable: setActiveController,
|
|
1420
|
+
onClientClosed: clearActiveController,
|
|
1421
|
+
onPromptActive: async () => {
|
|
1422
|
+
turnController.markPromptActive();
|
|
1423
|
+
await applyPendingCancel();
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
} finally {
|
|
1427
|
+
checkpointPerfMetricsCapture();
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
} finally {
|
|
1432
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1433
|
+
turnController.beginClosing();
|
|
1434
|
+
if (owner) await owner.close();
|
|
1435
|
+
await sharedClient.close().catch(() => {});
|
|
1436
|
+
try {
|
|
1437
|
+
const record = await resolveSessionRecord(options.sessionId);
|
|
1438
|
+
applyLifecycleSnapshotToRecord(record, sharedClient.getAgentLifecycleSnapshot());
|
|
1439
|
+
await writeSessionRecord(record);
|
|
1440
|
+
} catch {}
|
|
1441
|
+
await releaseQueueOwnerLease(lease);
|
|
1442
|
+
if (options.verbose) process.stderr.write(`[acpx] queue owner stopped for session ${options.sessionId}\n`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function sendSession(options) {
|
|
1446
|
+
const waitForCompletion = options.waitForCompletion !== false;
|
|
1447
|
+
const queuedToOwner = await submitToRunningOwner(options, waitForCompletion);
|
|
1448
|
+
if (queuedToOwner) return queuedToOwner;
|
|
1449
|
+
spawnQueueOwnerProcess(queueOwnerRuntimeOptionsFromSend(options));
|
|
1450
|
+
for (let attempt = 0; attempt < QUEUE_OWNER_STARTUP_MAX_ATTEMPTS; attempt += 1) {
|
|
1451
|
+
const queued = await submitToRunningOwner(options, waitForCompletion);
|
|
1452
|
+
if (queued) return queued;
|
|
1453
|
+
await waitMs(50);
|
|
1454
|
+
}
|
|
1455
|
+
throw new Error(`Session queue owner failed to start for session ${options.sessionId}`);
|
|
1456
|
+
}
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/session/session.ts
|
|
1459
|
+
var session_exports = /* @__PURE__ */ __exportAll({
|
|
1460
|
+
DEFAULT_HISTORY_LIMIT: () => 20,
|
|
1461
|
+
DEFAULT_QUEUE_OWNER_TTL_MS: () => DEFAULT_QUEUE_OWNER_TTL_MS,
|
|
1462
|
+
InterruptedError: () => InterruptedError,
|
|
1463
|
+
TimeoutError: () => TimeoutError,
|
|
1464
|
+
cancelSessionPrompt: () => cancelSessionPrompt,
|
|
1465
|
+
closeSession: () => closeSession,
|
|
1466
|
+
createSession: () => createSession,
|
|
1467
|
+
createSessionWithClient: () => createSessionWithClient,
|
|
1468
|
+
ensureSession: () => ensureSession,
|
|
1469
|
+
findGitRepositoryRoot: () => findGitRepositoryRoot,
|
|
1470
|
+
findSession: () => findSession,
|
|
1471
|
+
findSessionByDirectoryWalk: () => findSessionByDirectoryWalk,
|
|
1472
|
+
isProcessAlive: () => isProcessAlive,
|
|
1473
|
+
listSessions: () => listSessions,
|
|
1474
|
+
listSessionsForAgent: () => listSessionsForAgent,
|
|
1475
|
+
normalizeQueueOwnerTtlMs: () => normalizeQueueOwnerTtlMs,
|
|
1476
|
+
runOnce: () => runOnce,
|
|
1477
|
+
runQueuedTask: () => runQueuedTask,
|
|
1478
|
+
runSessionQueueOwner: () => runSessionQueueOwner,
|
|
1479
|
+
sendSession: () => sendSession,
|
|
1480
|
+
sendSessionDirect: () => sendSessionDirect,
|
|
1481
|
+
setSessionConfigOption: () => setSessionConfigOption,
|
|
1482
|
+
setSessionMode: () => setSessionMode,
|
|
1483
|
+
setSessionModel: () => setSessionModel
|
|
1484
|
+
});
|
|
1485
|
+
//#endregion
|
|
1486
|
+
export { buildQueueOwnerArgOverride as a, createSessionWithClient as c, sendSessionDirect as i, cancelSessionPrompt as l, runSessionQueueOwner as n, flushPerfMetricsCapture as o, runOnce as r, installPerfMetricsCapture as s, session_exports as t, DEFAULT_QUEUE_OWNER_TTL_MS as u };
|
|
1487
|
+
|
|
1488
|
+
//# sourceMappingURL=session-BbN0SBgf.js.map
|