acpx 0.15.0 → 0.16.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.
Files changed (39) hide show
  1. package/dist/agent-registry-DU6mhZBL.js +785 -0
  2. package/dist/agent-registry-DU6mhZBL.js.map +1 -0
  3. package/dist/agent-registry.d.ts +70 -0
  4. package/dist/agent-registry.d.ts.map +1 -0
  5. package/dist/agent-registry.js +2 -0
  6. package/dist/{cli-Cen2Rb6S.js → cli-BkYIxj_N.js} +6 -14
  7. package/dist/cli-BkYIxj_N.js.map +1 -0
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +137 -333
  11. package/dist/cli.js.map +1 -1
  12. package/dist/{client-Cvz6msGc.d.ts → client-5levcBIs.d.ts} +11 -9
  13. package/dist/client-5levcBIs.d.ts.map +1 -0
  14. package/dist/{flows-D-F3Y2o9.js → flows-Bxs4udrs.js} +205 -277
  15. package/dist/flows-Bxs4udrs.js.map +1 -0
  16. package/dist/flows.d.ts +6 -17
  17. package/dist/flows.d.ts.map +1 -1
  18. package/dist/flows.js +1 -1
  19. package/dist/{flags-CRh8BJre.js → invocation-options-BY4lxkn3.js} +82 -58
  20. package/dist/invocation-options-BY4lxkn3.js.map +1 -0
  21. package/dist/{live-checkpoint-CYRs-D9t.js → live-checkpoint-BEfxBKCh.js} +1033 -1437
  22. package/dist/live-checkpoint-BEfxBKCh.js.map +1 -0
  23. package/dist/{output-CIVecnHU.js → output-uo5a27KB.js} +487 -490
  24. package/dist/output-uo5a27KB.js.map +1 -0
  25. package/dist/runtime.d.ts +105 -22
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +582 -366
  28. package/dist/runtime.js.map +1 -1
  29. package/dist/{session-options-DyIGRNfu.d.ts → session-options-t3d-F4dn.d.ts} +8 -6
  30. package/dist/session-options-t3d-F4dn.d.ts.map +1 -0
  31. package/package.json +27 -25
  32. package/skills/acpx/SKILL.md +19 -17
  33. package/dist/cli-Cen2Rb6S.js.map +0 -1
  34. package/dist/client-Cvz6msGc.d.ts.map +0 -1
  35. package/dist/flags-CRh8BJre.js.map +0 -1
  36. package/dist/flows-D-F3Y2o9.js.map +0 -1
  37. package/dist/live-checkpoint-CYRs-D9t.js.map +0 -1
  38. package/dist/output-CIVecnHU.js.map +0 -1
  39. package/dist/session-options-DyIGRNfu.d.ts.map +0 -1
@@ -1,11 +1,15 @@
1
- import { $ as formatPerfMetric, B as absolutePath, Bt as extractAcpError, C as sessionOptionsFromRecord, D as AcpClient, Dt as TimeoutError, Et as InterruptedError, F as runTimedExecFile, G as listSessions, H as findSession, Ht as toAcpErrorPayload, I as splitCommandLine, J as pruneSessions, K as listSessionsForAgent, Kt as OUTPUT_ERROR_CODES, Lt as formatErrorMessage, M as modelStateFromConfigOptions, Ot as withInterrupt, Rt as isRetryablePromptError, S as persistSessionOptions, T as applyLifecycleSnapshotToRecord, Tt as textPrompt, U as findSessionByDirectoryWalk, V as findGitRepositoryRoot, W as isoNow, X as writeSessionRecord, Y as resolveSessionRecord, _ as recordPromptSubmission, _t as isAcpJsonRpcMessage, a as applyConfigOptionSelection, at as setPerfGauge, b as advertisedModelState, c as applyRequestedModelIfAdvertised, d as setDesiredModeId, dt as defaultSessionEventLog, en as QueueConnectionError, et as getPerfMetricsSnapshot, f as syncAdvertisedModelState, ft as sessionBaseDir, g as recordClientOperation, gt as extractSessionUpdateNotification, h as createSessionConversation, ht as sessionEventSegmentPath, i as connectAndLoadSession, it as resetPerfMetrics, kt as withTimeout, l as currentModelIdFromSetModelResponse, lt as normalizeRuntimeSessionId, m as cloneSessionConversation, mt as sessionEventLockPath, n as runPromptTurn, nt as measurePerf, o as applyConfigOptionsToRecord, ot as startPerfTimer, p as cloneSessionAcpxState, pt as sessionEventActivePath, q as normalizeName, qt as OUTPUT_ERROR_ORIGINS, r as withConnectedSession, rt as recordPerfDuration, s as applyModelSelection, t as LiveSessionCheckpoint, tn as QueueProtocolError, tt as incrementPerfCounter, u as setCurrentModelId, v as recordSessionUpdate, vt as parseJsonRpcErrorMessage, w as applyConversation, wt as promptToDisplayText, x as mergeSessionOptions, xt as isPromptInput, y as trimConversationForRuntime, yt as parsePromptStopReason, zt as normalizeOutputError } from "./live-checkpoint-CYRs-D9t.js";
2
- import fs, { mkdtempSync, realpathSync, writeFileSync } from "node:fs";
1
+ import { $ as getPerfMetricsSnapshot, At as isRetryablePromptError, B as absolutePath, C as recordSessionUpdate, Dt as withTimeout, E as AcpClient, Et as withInterrupt, G as listSessions, H as findSession, I as promptToDisplayText, J as pruneSessions, K as listSessionsForAgent, Kt as QueueConnectionError, L as textPrompt, Mt as extractAcpError, N as isPromptInput, Pt as toAcpErrorPayload, Q as formatPerfMetric, Rt as OUTPUT_ERROR_CODES, S as recordPromptSubmission, St as parsePromptStopReason, T as advertisedModelState, Tt as TimeoutError, U as findSessionByDirectoryWalk, V as findGitRepositoryRoot, W as isoNow, X as writeSessionRecord, Y as resolveSessionRecord, _t as isProcessAlive, a as applyConfigOptionSelection, at as startPerfTimer, bt as parseJsonRpcErrorMessage, c as applyModelSelection, ct as normalizeAgentSessionId, d as mergeSessionOptions, dt as sessionBaseDir, et as incrementPerfCounter, f as persistSessionOptions, ft as sessionEventActivePath, g as createInitialSessionRecord, h as applyLifecycleSnapshotToRecord, i as connectAndLoadSession, it as setPerfGauge, jt as normalizeOutputError, kt as formatErrorMessage, l as applyRequestedModelIfAdvertised, m as applyConversation, mt as sessionEventSegmentPath, n as runPromptTurn, nt as recordPerfDuration, o as applyConfigOptionsToRecord, p as sessionOptionsFromRecord, pt as sessionEventLockPath, q as normalizeName, qt as QueueProtocolError, r as withConnectedSession, rt as resetPerfMetrics, s as applyInitialModelSelection, t as LiveSessionCheckpoint, tt as measurePerf, u as setDesiredModeId, v as cloneSessionAcpxState, vt as extractSessionUpdateNotification, w as trimConversationForRuntime, wt as InterruptedError, x as recordClientOperation, xt as parsePermissionNotice, y as cloneSessionConversation, yt as isAcpJsonRpcMessage, zt as OUTPUT_ERROR_ORIGINS } from "./live-checkpoint-BEfxBKCh.js";
2
+ import { M as runTimedExecFile, N as splitCommandLine } from "./agent-registry-DU6mhZBL.js";
3
+ import fs, { realpathSync } from "node:fs";
3
4
  import path from "node:path";
5
+ import { spawn } from "node:child_process";
4
6
  import fs$1 from "node:fs/promises";
5
7
  import os from "node:os";
8
+ import { appendRegularFile, appendRegularFileSync, withTempFile } from "@openclaw/fs-safe/advanced";
6
9
  import { createHash, randomInt, randomUUID } from "node:crypto";
7
- import { spawn } from "node:child_process";
10
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
11
  import net from "node:net";
12
+ import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
9
13
  //#region \0rolldown/runtime.js
10
14
  var __defProp = Object.defineProperty;
11
15
  var __exportAll = (all, no_symbols) => {
@@ -18,17 +22,6 @@ var __exportAll = (all, no_symbols) => {
18
22
  return target;
19
23
  };
20
24
  //#endregion
21
- //#region src/process-liveness.ts
22
- function isProcessAlive(pid) {
23
- if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
24
- try {
25
- process.kill(pid, 0);
26
- return true;
27
- } catch {
28
- return false;
29
- }
30
- }
31
- //#endregion
32
25
  //#region src/cli/session/contracts.ts
33
26
  const DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
34
27
  function normalizeQueueOwnerTtlMs(ttlMs) {
@@ -61,7 +54,7 @@ function queueSocketPath(sessionId, homeDir = os.homedir()) {
61
54
  }
62
55
  //#endregion
63
56
  //#region src/cli/queue/lease-store.ts
64
- const PROCESS_SIGTERM_GRACE_MS = 4e3;
57
+ const PROCESS_SIGTERM_GRACE_MS = process.platform === "win32" ? 4e3 : 12e3;
65
58
  const PROCESS_SIGKILL_GRACE_MS = 1500;
66
59
  const PROCESS_POLL_MS = 50;
67
60
  const QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
@@ -141,61 +134,62 @@ async function waitForProcessExit(pid, timeoutMs) {
141
134
  }
142
135
  return !isProcessAlive(pid);
143
136
  }
144
- async function cleanupStaleQueueOwner(sessionId, owner) {
145
- const lockPath = queueLockFilePath(sessionId);
146
- await removeSocketFile(owner?.socketPath ?? queueSocketPath(sessionId)).catch(() => {});
147
- await fs$1.unlink(lockPath).catch((error) => {
137
+ async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
138
+ if (!await isCurrent()) return;
139
+ await removeSocketFile(socketPath).catch(() => {});
140
+ if (!await isCurrent()) return;
141
+ await fs$1.unlink(queueLockFilePath(sessionId)).catch((error) => {
148
142
  if (error.code !== "ENOENT") throw error;
149
143
  });
150
144
  }
151
- async function retireStaleQueueOwner(sessionId, owner) {
152
- if (owner && isProcessAlive(owner.pid)) await terminateProcess(owner.pid);
153
- await cleanupStaleQueueOwner(sessionId, owner);
145
+ async function ownsQueueLease(owner, requireStale = false) {
146
+ const current = await readQueueOwnerRecord(owner.sessionId);
147
+ return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && current.sessionId === owner.sessionId && (!requireStale || !ownerIsAlive(current) || isQueueOwnerHeartbeatStale(current));
148
+ }
149
+ function ownerIsAlive(owner) {
150
+ return owner.pid === process.pid || isProcessAlive(owner.pid);
154
151
  }
155
152
  async function readQueueOwnerRecord(sessionId) {
156
153
  const lockPath = queueLockFilePath(sessionId);
157
154
  try {
158
155
  const payload = await fs$1.readFile(lockPath, "utf8");
159
- return parseQueueOwnerRecord(JSON.parse(payload)) ?? void 0;
156
+ const parsed = parseQueueOwnerRecord(JSON.parse(payload));
157
+ return parsed?.sessionId === sessionId ? parsed : void 0;
160
158
  } catch {
161
159
  return;
162
160
  }
163
161
  }
164
- async function terminateProcess(pid) {
162
+ async function terminateProcess(pid, beforeSignal) {
165
163
  if (!isProcessAlive(pid)) return false;
166
- try {
167
- process.kill(pid, "SIGTERM");
168
- } catch {
169
- return false;
170
- }
171
- if (await waitForProcessExit(pid, PROCESS_SIGTERM_GRACE_MS)) return true;
172
- try {
173
- process.kill(pid, "SIGKILL");
174
- } catch {
175
- return false;
164
+ for (const [signal, graceMs] of [["SIGTERM", PROCESS_SIGTERM_GRACE_MS], ["SIGKILL", PROCESS_SIGKILL_GRACE_MS]]) {
165
+ if (beforeSignal && !await beforeSignal(signal)) return false;
166
+ try {
167
+ process.kill(pid, signal);
168
+ } catch {
169
+ return false;
170
+ }
171
+ if (await waitForProcessExit(pid, graceMs)) return true;
176
172
  }
177
- await waitForProcessExit(pid, PROCESS_SIGKILL_GRACE_MS);
178
- return true;
179
- }
180
- async function ensureOwnerIsUsable(sessionId, owner) {
181
- const alive = isProcessAlive(owner.pid);
182
- const stale = isQueueOwnerHeartbeatStale(owner);
183
- if (alive && !stale) return true;
184
- await retireStaleQueueOwner(sessionId, owner);
185
173
  return false;
186
174
  }
175
+ async function resolveUsableQueueOwner(sessionId, owner) {
176
+ if (ownerIsAlive(owner) && !isQueueOwnerHeartbeatStale(owner)) return owner;
177
+ await terminateQueueOwnerForSession(sessionId, owner, true);
178
+ const current = await readQueueOwnerRecord(sessionId);
179
+ return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && ownerIsAlive(current) && !isQueueOwnerHeartbeatStale(current) ? current : void 0;
180
+ }
187
181
  async function readQueueOwnerStatus(sessionId) {
188
- const owner = await readQueueOwnerRecord(sessionId);
182
+ const observed = await readQueueOwnerRecord(sessionId);
183
+ if (!observed) return;
184
+ const owner = await resolveUsableQueueOwner(sessionId, observed);
189
185
  if (!owner) return;
190
- const alive = await ensureOwnerIsUsable(sessionId, owner);
191
- if (!alive) return;
192
186
  return {
193
187
  pid: owner.pid,
194
188
  socketPath: owner.socketPath,
195
189
  heartbeatAt: owner.heartbeatAt,
196
190
  ownerGeneration: owner.ownerGeneration,
197
191
  queueDepth: owner.queueDepth,
198
- alive,
192
+ alive: true,
199
193
  stale: isQueueOwnerHeartbeatStale(owner)
200
194
  };
201
195
  }
@@ -207,30 +201,32 @@ async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, no
207
201
  const socketPath = queueSocketPath(sessionId);
208
202
  const createdAt = clock();
209
203
  const ownerGeneration = createOwnerGeneration();
210
- const payload = JSON.stringify({
204
+ const lease = {
211
205
  pid: process.pid,
212
206
  sessionId,
207
+ lockPath,
213
208
  socketPath,
214
209
  createdAt,
215
- heartbeatAt: createdAt,
216
210
  ownerGeneration,
217
- queueDepth: 0,
218
- ...mcpConfigMetadata
219
- }, null, 2);
211
+ ...mcpConfigMetadata,
212
+ updates: Promise.resolve(),
213
+ released: false
214
+ };
220
215
  try {
221
- await fs$1.writeFile(lockPath, `${payload}\n`, {
222
- encoding: "utf8",
223
- flag: "wx"
216
+ await stageQueueOwnerRecord(lease, 0, () => createdAt, async (tempPath, payload) => {
217
+ try {
218
+ await fs$1.link(tempPath, lockPath);
219
+ } catch (error) {
220
+ if (!isHardlinkFallbackError(error)) throw error;
221
+ await fs$1.writeFile(lockPath, payload, {
222
+ encoding: "utf8",
223
+ flag: "wx",
224
+ mode: 384
225
+ });
226
+ }
224
227
  });
225
228
  await removeSocketFile(socketPath).catch(() => {});
226
- return {
227
- sessionId,
228
- lockPath,
229
- socketPath,
230
- createdAt,
231
- ownerGeneration,
232
- ...mcpConfigMetadata
233
- };
229
+ return lease;
234
230
  } catch (error) {
235
231
  return await handleLeaseCollision(sessionId, error);
236
232
  }
@@ -248,10 +244,25 @@ async function handleLeaseCollision(sessionId, error) {
248
244
  if (error.code !== "EEXIST") throw error;
249
245
  const owner = await readQueueOwnerRecord(sessionId);
250
246
  if (!owner) {
251
- await cleanupStaleQueueOwner(sessionId, owner);
247
+ await cleanupAbandonedReservation(sessionId);
252
248
  return;
253
249
  }
254
- if (!isProcessAlive(owner.pid) || isQueueOwnerHeartbeatStale(owner)) await retireStaleQueueOwner(sessionId, owner);
250
+ await resolveUsableQueueOwner(sessionId, owner);
251
+ }
252
+ async function cleanupAbandonedReservation(sessionId) {
253
+ const lockPath = queueLockFilePath(sessionId);
254
+ try {
255
+ const observed = await fs$1.lstat(lockPath, { bigint: true });
256
+ if (!observed.isFile() || Date.now() - Number(observed.mtimeMs) <= QUEUE_OWNER_STALE_HEARTBEAT_MS) return;
257
+ const raw = await fs$1.readFile(lockPath, "utf8");
258
+ if (await readQueueOwnerRecord(sessionId)) return;
259
+ await cleanupQueueOwnerFiles(sessionId, queueSocketPath(sessionId), async () => {
260
+ const current = await fs$1.lstat(lockPath, { bigint: true });
261
+ return current.isFile() && current.dev === observed.dev && current.ino === observed.ino && current.mtimeNs === observed.mtimeNs && await fs$1.readFile(lockPath, "utf8") === raw;
262
+ });
263
+ } catch (error) {
264
+ if (error.code !== "ENOENT") throw error;
265
+ }
255
266
  }
256
267
  function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
257
268
  if (typeof mcpConfigOrNowIsoFactory === "string") return {
@@ -271,31 +282,52 @@ function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
271
282
  clock: nowIsoFactory
272
283
  };
273
284
  }
274
- async function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
285
+ function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
286
+ if (lease.released) return Promise.resolve();
287
+ const update = lease.updates.then(async () => {
288
+ await stageQueueOwnerRecord(lease, options.queueDepth, nowIsoFactory, async (tempPath) => {
289
+ if (await ownsQueueLease(lease)) await fs$1.rename(tempPath, lease.lockPath);
290
+ });
291
+ });
292
+ lease.updates = update.catch(() => {});
293
+ return update;
294
+ }
295
+ async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
275
296
  const payload = JSON.stringify({
276
297
  pid: process.pid,
277
298
  sessionId: lease.sessionId,
278
299
  socketPath: lease.socketPath,
279
300
  createdAt: lease.createdAt,
280
- heartbeatAt: nowIsoFactory(),
301
+ heartbeatAt: clock(),
281
302
  ownerGeneration: lease.ownerGeneration,
282
- queueDepth: Math.max(0, Math.round(options.queueDepth)),
303
+ queueDepth: Math.max(0, Math.round(queueDepth)),
283
304
  ...lease.mcpConfigPath ? { mcpConfigPath: lease.mcpConfigPath } : {},
284
305
  ...lease.mcpConfigFingerprint ? { mcpConfigFingerprint: lease.mcpConfigFingerprint } : {}
285
306
  }, null, 2);
286
- await fs$1.writeFile(lease.lockPath, `${payload}\n`, { encoding: "utf8" });
307
+ await withTempFile({
308
+ rootDir: path.dirname(lease.lockPath),
309
+ prefix: "owner",
310
+ fileName: "lease"
311
+ }, async (tempPath) => {
312
+ await fs$1.writeFile(tempPath, `${payload}\n`, {
313
+ encoding: "utf8",
314
+ flag: "wx",
315
+ mode: 384
316
+ });
317
+ await publish(tempPath, `${payload}\n`);
318
+ });
287
319
  }
288
320
  async function releaseQueueOwnerLease(lease) {
289
- await removeSocketFile(lease.socketPath).catch(() => {});
290
- await fs$1.unlink(lease.lockPath).catch((error) => {
291
- if (error.code !== "ENOENT") throw error;
292
- });
321
+ lease.released = true;
322
+ await lease.updates;
323
+ await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease));
293
324
  }
294
- async function terminateQueueOwnerForSession(sessionId) {
295
- const owner = await readQueueOwnerRecord(sessionId);
296
- if (!owner) return;
297
- if (isProcessAlive(owner.pid)) await terminateProcess(owner.pid);
298
- await cleanupStaleQueueOwner(sessionId, owner);
325
+ async function terminateQueueOwnerForSession(sessionId, expectedOwner, requireStale = false) {
326
+ const owner = expectedOwner ?? await readQueueOwnerRecord(sessionId);
327
+ if (!owner || owner.sessionId !== sessionId) return;
328
+ if (ownerIsAlive(owner)) await terminateProcess(owner.pid, (signal) => ownsQueueLease(owner, requireStale && signal === "SIGTERM"));
329
+ if (ownerIsAlive(owner)) return;
330
+ await cleanupQueueOwnerFiles(sessionId, owner.socketPath, () => ownsQueueLease(owner));
299
331
  }
300
332
  async function waitMs(ms) {
301
333
  await new Promise((resolve) => {
@@ -339,16 +371,13 @@ async function connectToSocket(socketPath, timeoutMs = SOCKET_CONNECTION_TIMEOUT
339
371
  });
340
372
  }
341
373
  async function connectToQueueOwner(owner, maxAttempts = QUEUE_CONNECT_ATTEMPTS) {
342
- let lastError;
343
374
  const attempts = Math.max(1, Math.trunc(maxAttempts));
344
375
  for (let attempt = 0; attempt < attempts; attempt += 1) try {
345
376
  return await measurePerf("queue.connect", async () => await connectToSocket(owner.socketPath));
346
377
  } catch (error) {
347
- lastError = error;
348
378
  if (!shouldRetryQueueConnect(error)) throw error;
349
379
  await waitMs(50);
350
380
  }
351
- if (lastError && !shouldRetryQueueConnect(lastError)) throw lastError;
352
381
  }
353
382
  //#endregion
354
383
  //#region src/cli/queue/ipc-health.ts
@@ -1151,7 +1180,7 @@ async function maybeRecoverStaleOwnerAfterProtocolMismatch(params) {
1151
1180
  if (!(params.error instanceof QueueProtocolError)) return false;
1152
1181
  const detailCode = params.error.detailCode;
1153
1182
  if (!detailCode || !STALE_OWNER_PROTOCOL_DETAIL_CODES.has(detailCode)) return false;
1154
- await terminateQueueOwnerForSession(params.sessionId).catch(() => {});
1183
+ await terminateQueueOwnerForSession(params.sessionId, params.owner).catch(() => {});
1155
1184
  incrementPerfCounter("queue.owner.stale_recovered");
1156
1185
  if (params.verbose) process.stderr.write(`[acpx] dropped stale queue owner metadata after protocol mismatch for session ${params.sessionId} (${detailCode})\n`);
1157
1186
  return true;
@@ -1390,29 +1419,15 @@ async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
1390
1419
  request,
1391
1420
  onMessage: (message, { state, resolve, reject }) => {
1392
1421
  if (message.type === "error") {
1393
- reject(new QueueConnectionError(message.message, {
1394
- outputCode: message.code,
1395
- detailCode: message.detailCode,
1396
- origin: message.origin ?? "queue",
1397
- retryable: message.retryable,
1398
- acp: message.acp
1399
- }));
1422
+ reject(queueConnectionErrorFromOwner(message, false));
1400
1423
  return;
1401
1424
  }
1402
1425
  if (!state.acknowledged) {
1403
- reject(new QueueConnectionError("Queue owner did not acknowledge request", {
1404
- detailCode: "QUEUE_ACK_MISSING",
1405
- origin: "queue",
1406
- retryable: true
1407
- }));
1426
+ reject(missingQueueAckError());
1408
1427
  return;
1409
1428
  }
1410
1429
  if (!isExpectedResponse(message)) {
1411
- reject(new QueueProtocolError("Queue owner returned unexpected response", {
1412
- detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
1413
- origin: "queue",
1414
- retryable: true
1415
- }));
1430
+ reject(unexpectedQueueResponseError());
1416
1431
  return;
1417
1432
  }
1418
1433
  resolve(message);
@@ -1435,87 +1450,47 @@ async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
1435
1450
  });
1436
1451
  }
1437
1452
  async function submitCancelToQueueOwner(owner) {
1438
- const request = {
1453
+ return (await submitControlToQueueOwner(owner, {
1439
1454
  type: "cancel_prompt",
1440
1455
  requestId: randomUUID(),
1441
1456
  ownerGeneration: owner.ownerGeneration
1442
- };
1443
- const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "cancel_result");
1444
- if (!response) return;
1445
- if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched cancel response", {
1446
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
1447
- origin: "queue",
1448
- retryable: true
1449
- });
1450
- return response.cancelled;
1457
+ }, (message) => message.type === "cancel_result"))?.cancelled;
1451
1458
  }
1452
1459
  async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
1453
- const request = {
1460
+ return await submitControlToQueueOwner(owner, {
1454
1461
  type: "set_mode",
1455
1462
  requestId: randomUUID(),
1456
1463
  ownerGeneration: owner.ownerGeneration,
1457
1464
  modeId,
1458
1465
  timeoutMs
1459
- };
1460
- const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_mode_result");
1461
- if (!response) return;
1462
- if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_mode response", {
1463
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
1464
- origin: "queue",
1465
- retryable: true
1466
- });
1467
- return true;
1466
+ }, (message) => message.type === "set_mode_result") ? true : void 0;
1468
1467
  }
1469
1468
  async function submitSetModelToQueueOwner(owner, modelId, timeoutMs) {
1470
- const request = {
1469
+ return await submitControlToQueueOwner(owner, {
1471
1470
  type: "set_model",
1472
1471
  requestId: randomUUID(),
1473
1472
  ownerGeneration: owner.ownerGeneration,
1474
1473
  modelId,
1475
1474
  timeoutMs
1476
- };
1477
- const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_model_result");
1478
- if (!response) return;
1479
- if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_model response", {
1480
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
1481
- origin: "queue",
1482
- retryable: true
1483
- });
1484
- return response;
1475
+ }, (message) => message.type === "set_model_result");
1485
1476
  }
1486
1477
  async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
1487
- const request = {
1478
+ return (await submitControlToQueueOwner(owner, {
1488
1479
  type: "set_config_option",
1489
1480
  requestId: randomUUID(),
1490
1481
  ownerGeneration: owner.ownerGeneration,
1491
1482
  configId,
1492
1483
  value,
1493
1484
  timeoutMs
1494
- };
1495
- const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "set_config_option_result");
1496
- if (!response) return;
1497
- if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched set_config_option response", {
1498
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
1499
- origin: "queue",
1500
- retryable: true
1501
- });
1502
- return response.response;
1485
+ }, (message) => message.type === "set_config_option_result"))?.response;
1503
1486
  }
1504
1487
  async function submitCloseSessionToQueueOwner(owner, timeoutMs) {
1505
- const request = {
1488
+ return (await submitControlToQueueOwner(owner, {
1506
1489
  type: "close_session",
1507
1490
  requestId: randomUUID(),
1508
1491
  ownerGeneration: owner.ownerGeneration,
1509
1492
  timeoutMs
1510
- };
1511
- const response = await submitControlToQueueOwner(owner, request, (message) => message.type === "close_session_result");
1512
- if (!response) return;
1513
- if (response.requestId !== request.requestId) throw new QueueProtocolError("Queue owner returned mismatched close_session response", {
1514
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
1515
- origin: "queue",
1516
- retryable: true
1517
- });
1518
- return response.closed;
1493
+ }, (message) => message.type === "close_session_result"))?.closed;
1519
1494
  }
1520
1495
  function queueOwnerMcpConfigMatches(owner, options) {
1521
1496
  return owner.mcpConfigPath === options.mcpConfigPath && owner.mcpConfigFingerprint === options.mcpConfigFingerprint;
@@ -1529,9 +1504,10 @@ function assertQueueOwnerMcpConfigMatches(owner, options) {
1529
1504
  });
1530
1505
  }
1531
1506
  async function trySubmitToRunningOwner(options) {
1532
- const owner = await readQueueOwnerRecord(options.sessionId);
1507
+ const observed = await readQueueOwnerRecord(options.sessionId);
1508
+ if (!observed) return;
1509
+ const owner = await resolveUsableQueueOwner(options.sessionId, observed);
1533
1510
  if (!owner) return;
1534
- if (!await ensureOwnerIsUsable(options.sessionId, owner)) return;
1535
1511
  assertQueueOwnerMcpConfigMatches(owner, options);
1536
1512
  let submitted;
1537
1513
  try {
@@ -1556,78 +1532,64 @@ async function trySubmitToRunningOwner(options) {
1556
1532
  retryable: true
1557
1533
  });
1558
1534
  }
1559
- async function tryCloseSessionOnRunningOwner(options) {
1535
+ async function tryControlOnRunningOwner(options) {
1560
1536
  const owner = await readQueueOwnerRecord(options.sessionId);
1561
1537
  if (!owner) return;
1562
- const closed = await submitCloseSessionToQueueOwner(owner, options.timeoutMs);
1563
- if (closed !== void 0) {
1564
- if (options.verbose) process.stderr.write(`[acpx] requested session/close on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
1565
- return closed;
1538
+ const response = await options.submit(owner);
1539
+ if (response !== void 0) {
1540
+ if (options.verbose) process.stderr.write(`${options.logPrefix} ${owner.pid} for session ${options.sessionId}\n`);
1541
+ return response;
1566
1542
  }
1567
1543
  if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
1568
- throw new QueueConnectionError("Session queue owner is running but not accepting close_session requests", {
1544
+ throw new QueueConnectionError(`Session queue owner is running but not accepting ${options.requestName} requests`, {
1569
1545
  detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
1570
1546
  origin: "queue",
1571
1547
  retryable: true
1572
1548
  });
1573
1549
  }
1550
+ async function tryCloseSessionOnRunningOwner(options) {
1551
+ return await tryControlOnRunningOwner({
1552
+ sessionId: options.sessionId,
1553
+ verbose: options.verbose,
1554
+ requestName: "close_session",
1555
+ logPrefix: "[acpx] requested session/close on active owner pid",
1556
+ submit: (owner) => submitCloseSessionToQueueOwner(owner, options.timeoutMs)
1557
+ });
1558
+ }
1574
1559
  async function tryCancelOnRunningOwner(options) {
1575
- const owner = await readQueueOwnerRecord(options.sessionId);
1576
- if (!owner) return;
1577
- const cancelled = await submitCancelToQueueOwner(owner);
1578
- if (cancelled !== void 0) {
1579
- if (options.verbose) process.stderr.write(`[acpx] requested cancel on active owner pid ${owner.pid} for session ${options.sessionId}\n`);
1580
- return cancelled;
1581
- }
1582
- if (!(await probeQueueOwnerHealth(options.sessionId)).hasLease) return;
1583
- throw new QueueConnectionError("Session queue owner is running but not accepting cancel requests", {
1584
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
1585
- origin: "queue",
1586
- retryable: true
1560
+ return await tryControlOnRunningOwner({
1561
+ sessionId: options.sessionId,
1562
+ verbose: options.verbose,
1563
+ requestName: "cancel",
1564
+ logPrefix: "[acpx] requested cancel on active owner pid",
1565
+ submit: submitCancelToQueueOwner
1587
1566
  });
1588
1567
  }
1589
1568
  async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
1590
- const owner = await readQueueOwnerRecord(sessionId);
1591
- if (!owner) return;
1592
- if (await submitSetModeToQueueOwner(owner, modeId, timeoutMs)) {
1593
- if (verbose) process.stderr.write(`[acpx] requested session/set_mode on owner pid ${owner.pid} for session ${sessionId}\n`);
1594
- return true;
1595
- }
1596
- if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
1597
- throw new QueueConnectionError("Session queue owner is running but not accepting set_mode requests", {
1598
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
1599
- origin: "queue",
1600
- retryable: true
1569
+ return await tryControlOnRunningOwner({
1570
+ sessionId,
1571
+ verbose,
1572
+ requestName: "set_mode",
1573
+ logPrefix: "[acpx] requested session/set_mode on owner pid",
1574
+ submit: (owner) => submitSetModeToQueueOwner(owner, modeId, timeoutMs)
1601
1575
  });
1602
1576
  }
1603
1577
  async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose) {
1604
- const owner = await readQueueOwnerRecord(sessionId);
1605
- if (!owner) return;
1606
- const submitted = await submitSetModelToQueueOwner(owner, modelId, timeoutMs);
1607
- if (submitted) {
1608
- if (verbose) process.stderr.write(`[acpx] requested a model config update on owner pid ${owner.pid} for session ${sessionId}\n`);
1609
- return submitted;
1610
- }
1611
- if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
1612
- throw new QueueConnectionError("Session queue owner is running but not accepting set_model requests", {
1613
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
1614
- origin: "queue",
1615
- retryable: true
1578
+ return await tryControlOnRunningOwner({
1579
+ sessionId,
1580
+ verbose,
1581
+ requestName: "set_model",
1582
+ logPrefix: "[acpx] requested a model config update on owner pid",
1583
+ submit: (owner) => submitSetModelToQueueOwner(owner, modelId, timeoutMs)
1616
1584
  });
1617
1585
  }
1618
1586
  async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
1619
- const owner = await readQueueOwnerRecord(sessionId);
1620
- if (!owner) return;
1621
- const response = await submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs);
1622
- if (response) {
1623
- if (verbose) process.stderr.write(`[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}\n`);
1624
- return response;
1625
- }
1626
- if (!(await probeQueueOwnerHealth(sessionId)).hasLease) return;
1627
- throw new QueueConnectionError("Session queue owner is running but not accepting set_config_option requests", {
1628
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
1629
- origin: "queue",
1630
- retryable: true
1587
+ return await tryControlOnRunningOwner({
1588
+ sessionId,
1589
+ verbose,
1590
+ requestName: "set_config_option",
1591
+ logPrefix: "[acpx] requested session/set_config_option on owner pid",
1592
+ submit: (owner) => submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs)
1631
1593
  });
1632
1594
  }
1633
1595
  //#endregion
@@ -1864,41 +1826,28 @@ async function createSessionRecordWithClient(client, options) {
1864
1826
  const createdState = options.resumeSessionId ? await resumeSessionRecordWithClient(client, options, cwd) : await createFreshSessionState(client, options, cwd);
1865
1827
  const { sessionId, agentSessionId } = createdState;
1866
1828
  const lifecycle = client.getAgentLifecycleSnapshot();
1867
- const now = isoNow();
1868
1829
  const record = {
1869
- schema: "acpx.session.v1",
1870
- acpxRecordId: sessionId,
1871
- acpSessionId: sessionId,
1872
- agentSessionId,
1873
- agentCommand: options.agentCommand,
1874
- agentArgv: options.agentArgv,
1875
- cwd,
1876
- name: normalizeName(options.name),
1877
- createdAt: now,
1878
- lastUsedAt: now,
1879
- lastSeq: 0,
1830
+ ...createInitialSessionRecord({
1831
+ recordId: sessionId,
1832
+ sessionId,
1833
+ agentSessionId,
1834
+ agentCommand: options.agentCommand,
1835
+ agentArgv: options.agentArgv,
1836
+ cwd,
1837
+ name: normalizeName(options.name)
1838
+ }),
1880
1839
  lastRequestId: void 0,
1881
- eventLog: defaultSessionEventLog(sessionId),
1882
- closed: false,
1883
- closedAt: void 0,
1884
1840
  pid: lifecycle.running ? lifecycle.pid : void 0,
1885
1841
  agentStartedAt: lifecycle.startedAt,
1886
1842
  protocolVersion: client.initializeResult?.protocolVersion,
1887
- agentCapabilities: client.initializeResult?.agentCapabilities,
1888
- ...createSessionConversation(now),
1889
- acpx: {}
1843
+ agentCapabilities: client.initializeResult?.agentCapabilities
1890
1844
  };
1891
1845
  persistSessionOptions(record, options.sessionOptions);
1892
- applyCreatedSessionModelState(record, createdState, options.sessionOptions?.model);
1846
+ applyConfigOptionsToRecord(record, createdState.sessionResult);
1847
+ applyInitialModelSelection(record, createdState.sessionResult.models, options.sessionOptions?.model, createdState.modelApplication);
1893
1848
  await writeSessionRecord(record);
1894
1849
  return record;
1895
1850
  }
1896
- function applyCreatedSessionModelState(record, state, requestedModel) {
1897
- applyConfigOptionsToRecord(record, state.sessionResult);
1898
- applyConfigOptionsToRecord(record, state.requestedModelResponse);
1899
- syncAdvertisedModelState(record, state.requestedModelResponse ? modelStateFromConfigOptions(state.requestedModelResponse.configOptions) : state.sessionModels);
1900
- if (state.requestedModelApplied) setCurrentModelId(record, currentModelIdFromSetModelResponse(state.requestedModelResponse, requestedModel));
1901
- }
1902
1851
  async function createFreshSessionState(client, options, cwd) {
1903
1852
  const createdSession = await withTimeout(client.createSession(cwd), options.timeoutMs);
1904
1853
  const modelApplication = await applyRequestedModelIfAdvertised({
@@ -1912,11 +1861,9 @@ async function createFreshSessionState(client, options, cwd) {
1912
1861
  });
1913
1862
  return {
1914
1863
  sessionId: createdSession.sessionId,
1915
- agentSessionId: normalizeRuntimeSessionId(createdSession.agentSessionId),
1864
+ agentSessionId: normalizeAgentSessionId(createdSession.agentSessionId),
1916
1865
  sessionResult: createdSession,
1917
- sessionModels: createdSession.models,
1918
- requestedModelApplied: modelApplication.applied,
1919
- requestedModelResponse: modelApplication.response
1866
+ modelApplication
1920
1867
  };
1921
1868
  }
1922
1869
  async function resumeSessionRecordWithClient(client, options, cwd) {
@@ -1937,11 +1884,9 @@ async function resumeSessionRecordWithClient(client, options, cwd) {
1937
1884
  });
1938
1885
  return {
1939
1886
  sessionId: options.resumeSessionId,
1940
- agentSessionId: normalizeRuntimeSessionId(resumedSession.agentSessionId),
1887
+ agentSessionId: normalizeAgentSessionId(resumedSession.agentSessionId),
1941
1888
  sessionResult: resumedSession,
1942
- sessionModels,
1943
- requestedModelApplied: modelApplication.applied,
1944
- requestedModelResponse: modelApplication.response
1889
+ modelApplication
1945
1890
  };
1946
1891
  } catch (error) {
1947
1892
  throw new Error(`Failed to resume ACP session ${options.resumeSessionId}: ${formatErrorMessage(error)}`, { cause: error });
@@ -2113,7 +2058,11 @@ function payloadHasMetrics(payload) {
2113
2058
  }
2114
2059
  function appendPerfMetricsPayload(payload) {
2115
2060
  fs.mkdirSync(path.dirname(captureFilePath), { recursive: true });
2116
- fs.appendFileSync(captureFilePath, `${JSON.stringify(payload)}\n`, "utf8");
2061
+ appendRegularFileSync({
2062
+ filePath: captureFilePath,
2063
+ content: `${JSON.stringify(payload)}\n`,
2064
+ mode: 384
2065
+ });
2117
2066
  captureSequence += 1;
2118
2067
  }
2119
2068
  function writePerfMetricsCapture(reason, resetAfterWrite) {
@@ -2166,6 +2115,7 @@ var QueueOwnerTurnController = class {
2166
2115
  state = "idle";
2167
2116
  pendingCancel = false;
2168
2117
  activeController;
2118
+ waitingTurn;
2169
2119
  constructor(options) {
2170
2120
  this.options = options;
2171
2121
  }
@@ -2178,6 +2128,8 @@ var QueueOwnerTurnController = class {
2178
2128
  beginTurn() {
2179
2129
  this.state = "starting";
2180
2130
  this.pendingCancel = false;
2131
+ this.waitingTurn = new AbortController();
2132
+ return this.waitingTurn.signal;
2181
2133
  }
2182
2134
  markPromptActive() {
2183
2135
  if (this.state === "starting" || this.state === "active") this.state = "active";
@@ -2185,6 +2137,7 @@ var QueueOwnerTurnController = class {
2185
2137
  endTurn() {
2186
2138
  this.state = "idle";
2187
2139
  this.pendingCancel = false;
2140
+ this.waitingTurn = void 0;
2188
2141
  }
2189
2142
  beginClosing() {
2190
2143
  this.state = "closing";
@@ -2213,6 +2166,7 @@ var QueueOwnerTurnController = class {
2213
2166
  }
2214
2167
  if (this.state === "starting" || this.state === "active") {
2215
2168
  this.pendingCancel = true;
2169
+ this.waitingTurn?.abort();
2216
2170
  return true;
2217
2171
  }
2218
2172
  return false;
@@ -2248,8 +2202,6 @@ var QueueOwnerTurnController = class {
2248
2202
  };
2249
2203
  //#endregion
2250
2204
  //#region src/cli/session/queue-owner-process.ts
2251
- const QUEUE_OWNER_PAYLOAD_FILE_ENV = "ACPX_QUEUE_OWNER_PAYLOAD_FILE";
2252
- const QUEUE_OWNER_PAYLOAD_ENV = "ACPX_QUEUE_OWNER_PAYLOAD";
2253
2205
  function isNonEmptyStringArray(value) {
2254
2206
  return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string" && entry.length > 0);
2255
2207
  }
@@ -2341,35 +2293,8 @@ function queueOwnerRuntimeOptionsFromSend(options) {
2341
2293
  sessionOptions: options.sessionOptions
2342
2294
  };
2343
2295
  }
2344
- function writeQueueOwnerPayloadFile(payload) {
2345
- const dir = mkdtempSync(path.join(os.tmpdir(), "acpx-queue-owner-"));
2346
- const payloadPath = path.join(dir, "payload.json");
2347
- writeFileSync(payloadPath, payload, {
2348
- encoding: "utf8",
2349
- flag: "wx",
2350
- mode: 384
2351
- });
2352
- return payloadPath;
2353
- }
2354
2296
  /** Max stderr bytes retained for cold-start diagnostics. */
2355
2297
  const QUEUE_OWNER_STARTUP_STDERR_MAX_BYTES = 4e3;
2356
- function buildQueueOwnerSpawnOptions(payloadFilePath, opts) {
2357
- const env = {
2358
- ...process.env,
2359
- [QUEUE_OWNER_PAYLOAD_FILE_ENV]: payloadFilePath
2360
- };
2361
- delete env[QUEUE_OWNER_PAYLOAD_ENV];
2362
- return {
2363
- detached: true,
2364
- stdio: opts?.captureStderr === true ? [
2365
- "ignore",
2366
- "ignore",
2367
- "pipe"
2368
- ] : "ignore",
2369
- env,
2370
- windowsHide: true
2371
- };
2372
- }
2373
2298
  function formatQueueOwnerStartupFailure(params) {
2374
2299
  const parts = [`Session queue owner failed to start for session ${params.sessionId}`];
2375
2300
  if (params.exit.spawnError) parts.push(`spawn error: ${params.exit.spawnError.message}`);
@@ -2378,19 +2303,33 @@ function formatQueueOwnerStartupFailure(params) {
2378
2303
  const signalPart = params.exit.signal ? `, signal ${params.exit.signal}` : "";
2379
2304
  parts.push(`exited with code ${codePart}${signalPart} before binding its socket`);
2380
2305
  }
2306
+ if (params.exit.inputError) parts.push(`startup input failed: ${params.exit.inputError.message}`);
2381
2307
  const tail = params.logTail.trim();
2382
2308
  if (tail.length > 0) parts.push(`stderr:\n${tail}`);
2383
2309
  return parts.join(": ");
2384
2310
  }
2385
2311
  function spawnQueueOwnerProcess(options) {
2386
- const payloadPath = writeQueueOwnerPayloadFile(JSON.stringify(options));
2312
+ const payload = JSON.stringify(options);
2387
2313
  let exited = false;
2388
2314
  let code = null;
2389
2315
  let signal = null;
2390
2316
  let spawnError;
2317
+ let inputError;
2391
2318
  let capturing = true;
2392
2319
  let stderrTail = Buffer.alloc(0);
2393
- const child = spawn(process.execPath, resolveQueueOwnerSpawnArgs(), buildQueueOwnerSpawnOptions(payloadPath, { captureStderr: true }));
2320
+ const child = spawn(process.execPath, resolveQueueOwnerSpawnArgs(), {
2321
+ detached: true,
2322
+ stdio: [
2323
+ "pipe",
2324
+ "ignore",
2325
+ "pipe"
2326
+ ],
2327
+ windowsHide: true
2328
+ });
2329
+ child.stdin.on("error", (error) => {
2330
+ inputError = error;
2331
+ });
2332
+ child.stdin.end(payload);
2394
2333
  const stopStartupCapture = () => {
2395
2334
  if (!capturing) return;
2396
2335
  capturing = false;
@@ -2424,7 +2363,8 @@ function spawnQueueOwnerProcess(options) {
2424
2363
  exited,
2425
2364
  code,
2426
2365
  signal,
2427
- ...spawnError ? { spawnError } : {}
2366
+ ...spawnError ? { spawnError } : {},
2367
+ ...inputError ? { inputError } : {}
2428
2368
  }),
2429
2369
  readLogTail: (maxBytes = QUEUE_OWNER_STARTUP_STDERR_MAX_BYTES) => {
2430
2370
  const start = Math.max(0, stderrTail.length - maxBytes);
@@ -2435,10 +2375,11 @@ function spawnQueueOwnerProcess(options) {
2435
2375
  }
2436
2376
  //#endregion
2437
2377
  //#region src/session/events.ts
2438
- const LOCK_RETRY_MS = 15;
2439
- const EVENT_LOCK_STALE_MS = 15e3;
2440
2378
  async function ensureSessionDir() {
2441
- await fs$1.mkdir(sessionBaseDir(), { recursive: true });
2379
+ await fs$1.mkdir(sessionBaseDir(), {
2380
+ recursive: true,
2381
+ mode: 448
2382
+ });
2442
2383
  }
2443
2384
  async function pathExists(filePath) {
2444
2385
  try {
@@ -2479,71 +2420,16 @@ async function rotateSegments(sessionId, maxSegments) {
2479
2420
  }
2480
2421
  if (await pathExists(active)) await fs$1.rename(active, sessionEventSegmentPath(sessionId, 1));
2481
2422
  }
2482
- function parseEventLockPayload(raw) {
2483
- try {
2484
- const parsed = JSON.parse(raw);
2485
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
2486
- const record = parsed;
2487
- return {
2488
- pid: typeof record.pid === "number" ? record.pid : void 0,
2489
- created_at: typeof record.created_at === "string" ? record.created_at : void 0
2490
- };
2491
- } catch {
2492
- return {};
2493
- }
2494
- }
2495
- async function removeStaleEventLock(lockPath) {
2496
- try {
2497
- const parsed = parseEventLockPayload(await fs$1.readFile(lockPath, "utf8"));
2498
- const createdAtMs = parsed.created_at ? Date.parse(parsed.created_at) : NaN;
2499
- const lockAgeMs = Number.isFinite(createdAtMs) ? Date.now() - createdAtMs : Number.POSITIVE_INFINITY;
2500
- if (isProcessAlive(parsed.pid) && lockAgeMs <= EVENT_LOCK_STALE_MS) return false;
2501
- await fs$1.unlink(lockPath);
2502
- incrementPerfCounter("session.events.stale_lock_recovered");
2503
- return true;
2504
- } catch (error) {
2505
- if (error.code === "ENOENT") return true;
2506
- return false;
2507
- }
2508
- }
2509
- async function acquireEventsLock(sessionId) {
2510
- await ensureSessionDir();
2511
- const lockPath = sessionEventLockPath(sessionId);
2512
- const payload = JSON.stringify({
2513
- pid: process.pid,
2514
- created_at: (/* @__PURE__ */ new Date()).toISOString()
2515
- }, null, 2);
2516
- for (;;) try {
2517
- await fs$1.writeFile(lockPath, `${payload}\n`, {
2518
- encoding: "utf8",
2519
- flag: "wx"
2520
- });
2521
- return { filePath: lockPath };
2522
- } catch (error) {
2523
- if (error.code !== "EEXIST") throw error;
2524
- if (await removeStaleEventLock(lockPath)) continue;
2525
- await new Promise((resolve) => {
2526
- setTimeout(resolve, LOCK_RETRY_MS);
2527
- });
2528
- }
2529
- }
2530
- async function releaseEventsLock(lock) {
2531
- await fs$1.unlink(lock.filePath).catch((error) => {
2532
- if (error.code !== "ENOENT") throw error;
2533
- });
2534
- }
2535
2423
  var SessionEventWriter = class SessionEventWriter {
2536
2424
  record;
2537
- lock;
2538
2425
  maxSegmentBytes;
2539
2426
  maxSegments;
2540
2427
  activePath;
2541
2428
  activeSizeBytes;
2542
2429
  segmentCount;
2543
2430
  closed = false;
2544
- constructor(record, lock, options, state) {
2431
+ constructor(record, options, state) {
2545
2432
  this.record = record;
2546
- this.lock = lock;
2547
2433
  this.maxSegmentBytes = options.maxSegmentBytes;
2548
2434
  this.maxSegments = options.maxSegments;
2549
2435
  this.activePath = state.activePath;
@@ -2551,13 +2437,12 @@ var SessionEventWriter = class SessionEventWriter {
2551
2437
  this.segmentCount = state.segmentCount;
2552
2438
  }
2553
2439
  static async open(record, options = {}) {
2554
- const lock = await acquireEventsLock(record.acpxRecordId);
2555
2440
  const maxSegmentBytes = options.maxSegmentBytes ?? record.eventLog.max_segment_bytes ?? 67108864;
2556
2441
  const maxSegments = options.maxSegments ?? record.eventLog.max_segments ?? 5;
2557
2442
  const activePath = sessionEventActivePath(record.acpxRecordId);
2558
2443
  const activeSizeBytes = await statSize(activePath);
2559
2444
  const segmentCount = await resolveInitialSegmentCount(record, maxSegments);
2560
- return new SessionEventWriter(record, lock, {
2445
+ return new SessionEventWriter(record, {
2561
2446
  maxSegmentBytes,
2562
2447
  maxSegments
2563
2448
  }, {
@@ -2588,7 +2473,11 @@ var SessionEventWriter = class SessionEventWriter {
2588
2473
  this.segmentCount = Math.min(this.segmentCount + 1, this.maxSegments);
2589
2474
  incrementPerfCounter("session.events.rotate");
2590
2475
  }
2591
- await fs$1.appendFile(this.activePath, line, "utf8");
2476
+ await appendRegularFile({
2477
+ filePath: this.activePath,
2478
+ content: line,
2479
+ mode: 384
2480
+ });
2592
2481
  this.activeSizeBytes += lineBytes;
2593
2482
  this.record.lastSeq += 1;
2594
2483
  if (Object.hasOwn(message, "id")) {
@@ -2619,11 +2508,132 @@ var SessionEventWriter = class SessionEventWriter {
2619
2508
  if (options.checkpoint !== false) await writeSessionRecord(this.record);
2620
2509
  } finally {
2621
2510
  this.closed = true;
2622
- await releaseEventsLock(this.lock);
2623
2511
  }
2624
2512
  }
2625
2513
  };
2626
2514
  //#endregion
2515
+ //#region src/session/turn-ownership.ts
2516
+ const LOCK_RETRY_MS = 15;
2517
+ const INCOMPLETE_RESERVATION_GRACE_MS = 15e3;
2518
+ let lastCreatedAt = 0;
2519
+ async function readLock(filePath) {
2520
+ try {
2521
+ const stat = await fs$1.lstat(filePath, { bigint: true });
2522
+ if (!stat.isFile()) throw new Error(`Session turn lock is not a regular file: ${filePath}`);
2523
+ return {
2524
+ stat,
2525
+ payload: await fs$1.readFile(filePath, "utf8")
2526
+ };
2527
+ } catch (error) {
2528
+ if (error.code === "ENOENT") return;
2529
+ throw error;
2530
+ }
2531
+ }
2532
+ function lockPid(payload) {
2533
+ try {
2534
+ const pid = JSON.parse(payload)?.pid;
2535
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
2536
+ } catch {
2537
+ return;
2538
+ }
2539
+ }
2540
+ async function removeObservedLock(filePath, observed) {
2541
+ const current = await readLock(filePath);
2542
+ if (!current) return true;
2543
+ if (current.payload !== observed.payload || current.stat.mtimeNs !== observed.stat.mtimeNs || current.stat.size !== observed.stat.size) return false;
2544
+ await fs$1.unlink(filePath).catch((error) => {
2545
+ if (error.code !== "ENOENT") throw error;
2546
+ });
2547
+ return true;
2548
+ }
2549
+ async function recoverAbandonedLock(filePath) {
2550
+ const observed = await readLock(filePath);
2551
+ if (!observed) return;
2552
+ const pid = lockPid(observed.payload);
2553
+ if (pid === process.pid || isProcessAlive(pid)) return;
2554
+ if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return;
2555
+ if (await removeObservedLock(filePath, observed)) incrementPerfCounter("session.events.stale_lock_recovered");
2556
+ }
2557
+ async function publishLock(filePath, payload, signal) {
2558
+ try {
2559
+ await withTempFile({
2560
+ rootDir: path.dirname(filePath),
2561
+ prefix: "session-turn",
2562
+ fileName: "lock"
2563
+ }, async (temporaryPath) => {
2564
+ await fs$1.writeFile(temporaryPath, payload, {
2565
+ encoding: "utf8",
2566
+ flag: "wx",
2567
+ mode: 384
2568
+ });
2569
+ signal?.throwIfAborted();
2570
+ try {
2571
+ await fs$1.link(temporaryPath, filePath);
2572
+ } catch (error) {
2573
+ if (!isHardlinkFallbackError(error)) throw error;
2574
+ await fs$1.writeFile(filePath, payload, {
2575
+ encoding: "utf8",
2576
+ flag: "wx",
2577
+ mode: 384
2578
+ });
2579
+ }
2580
+ });
2581
+ } catch (error) {
2582
+ await removePublishedLock(filePath, payload).catch(() => {});
2583
+ throw error;
2584
+ }
2585
+ }
2586
+ async function removePublishedLock(filePath, payload) {
2587
+ const current = await readLock(filePath);
2588
+ if (current?.payload === payload) await removeObservedLock(filePath, current);
2589
+ }
2590
+ async function observePublishedLock(filePath, payload, signal) {
2591
+ try {
2592
+ const observed = await readLock(filePath);
2593
+ if (observed?.payload !== payload) throw new Error(`Session turn ownership changed before admission: ${filePath}`);
2594
+ signal?.throwIfAborted();
2595
+ return { [Symbol.asyncDispose]: async () => {
2596
+ await removeObservedLock(filePath, observed);
2597
+ } };
2598
+ } catch (error) {
2599
+ await removePublishedLock(filePath, payload).catch(() => {});
2600
+ throw error;
2601
+ }
2602
+ }
2603
+ async function acquireSessionTurn(recordId, signal) {
2604
+ const filePath = sessionEventLockPath(recordId);
2605
+ await fs$1.mkdir(path.dirname(filePath), {
2606
+ recursive: true,
2607
+ mode: 448
2608
+ });
2609
+ lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);
2610
+ const payload = `${JSON.stringify({
2611
+ pid: process.pid,
2612
+ created_at: new Date(lastCreatedAt).toISOString()
2613
+ }, null, 2)}\n`;
2614
+ for (;;) {
2615
+ signal?.throwIfAborted();
2616
+ try {
2617
+ await publishLock(filePath, payload, signal);
2618
+ break;
2619
+ } catch (error) {
2620
+ if (error.code !== "EEXIST") throw error;
2621
+ await recoverAbandonedLock(filePath);
2622
+ await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
2623
+ }
2624
+ }
2625
+ return await observePublishedLock(filePath, payload, signal);
2626
+ }
2627
+ //#endregion
2628
+ //#region src/cli/output/discard.ts
2629
+ const DISCARD_OUTPUT_FORMATTER = {
2630
+ setContext() {},
2631
+ onAcpMessage() {},
2632
+ onError() {},
2633
+ onPermissionEscalation() {},
2634
+ flush() {}
2635
+ };
2636
+ //#endregion
2627
2637
  //#region src/cli/session/runtime.ts
2628
2638
  const INTERRUPT_CANCEL_WAIT_MS = 2500;
2629
2639
  var QueueTaskOutputFormatter = class {
@@ -2662,13 +2672,6 @@ var QueueTaskOutputFormatter = class {
2662
2672
  }
2663
2673
  flush() {}
2664
2674
  };
2665
- const DISCARD_OUTPUT_FORMATTER = {
2666
- setContext() {},
2667
- onAcpMessage() {},
2668
- onError() {},
2669
- onPermissionEscalation() {},
2670
- flush() {}
2671
- };
2672
2675
  function markOutputAlreadyEmitted(error, outputAlreadyEmitted) {
2673
2676
  if (!outputAlreadyEmitted || !error || typeof error !== "object") return;
2674
2677
  error.outputAlreadyEmitted = true;
@@ -2725,29 +2728,6 @@ function toPromptResult(stopReason, sessionId, client, meta) {
2725
2728
  function requestedModelId(value) {
2726
2729
  return typeof value === "string" ? value.trim() : "";
2727
2730
  }
2728
- function mergeConnectedModelState(state, connectedState) {
2729
- if (!connectedState) return state;
2730
- const nextState = cloneSessionAcpxState(state) ?? {};
2731
- mergeConnectedAdvertisedModelState(nextState, connectedState);
2732
- mergeConnectedModelPreferences(nextState, connectedState);
2733
- return nextState;
2734
- }
2735
- function mergeConnectedAdvertisedModelState(nextState, connectedState) {
2736
- if (connectedState.config_options !== void 0) nextState.config_options = structuredClone(connectedState.config_options);
2737
- else delete nextState.config_options;
2738
- if (connectedState.current_model_id !== void 0) nextState.current_model_id = connectedState.current_model_id;
2739
- else delete nextState.current_model_id;
2740
- if (connectedState.available_models) nextState.available_models = [...connectedState.available_models];
2741
- else delete nextState.available_models;
2742
- if (connectedState.model_control) nextState.model_control = connectedState.model_control;
2743
- else delete nextState.model_control;
2744
- }
2745
- function mergeConnectedModelPreferences(nextState, connectedState) {
2746
- if (connectedState.session_options) nextState.session_options = cloneSessionAcpxState(connectedState)?.session_options;
2747
- if (connectedState.desired_mode_id !== void 0) nextState.desired_mode_id = connectedState.desired_mode_id;
2748
- if (connectedState.desired_config_options) nextState.desired_config_options = { ...connectedState.desired_config_options };
2749
- else delete nextState.desired_config_options;
2750
- }
2751
2731
  async function applyPromptModelIfAdvertised(params) {
2752
2732
  const requestedModel = requestedModelId(params.requestedModel);
2753
2733
  if (!requestedModel) return;
@@ -2838,23 +2818,20 @@ function emitPromptDisconnectNotice(snapshot, verbose) {
2838
2818
  if (!lastExit?.unexpectedDuringPrompt || !verbose) return;
2839
2819
  process.stderr.write("[acpx] agent disconnected during prompt (" + lastExit.reason + ", exit=" + lastExit.exitCode + ", signal=" + (lastExit.signal ?? "none") + ")\n");
2840
2820
  }
2841
- function shouldRetryRuntimePrompt(error, attempt, maxRetries, snapshot, hasSideEffects) {
2842
- if (!shouldRetryPromptAttempt(error, attempt, maxRetries, hasSideEffects)) return false;
2843
- return snapshot.lastExit?.unexpectedDuringPrompt !== true;
2844
- }
2845
- function shouldRetryPromptAttempt(error, attempt, maxRetries, hasSideEffects) {
2846
- return attempt < maxRetries && !hasSideEffects() && isRetryablePromptError(error);
2847
- }
2848
- async function waitBeforePromptRetry(error, attempt, maxRetries, suppressSdkConsoleErrors) {
2849
- const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
2850
- emitPromptRetryNotice({
2851
- error,
2852
- delayMs,
2853
- attempt: attempt + 1,
2854
- maxRetries,
2855
- suppressSdkConsoleErrors
2856
- });
2857
- await waitMs(delayMs);
2821
+ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors) {
2822
+ if (attempt < maxRetries && !hasSideEffects() && isRetryablePromptError(error)) {
2823
+ const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
2824
+ emitPromptRetryNotice({
2825
+ error,
2826
+ delayMs,
2827
+ attempt: attempt + 1,
2828
+ maxRetries,
2829
+ suppressSdkConsoleErrors
2830
+ });
2831
+ await waitMs(delayMs);
2832
+ return !hasSideEffects();
2833
+ }
2834
+ return false;
2858
2835
  }
2859
2836
  function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatter) {
2860
2837
  return {
@@ -2878,6 +2855,7 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
2878
2855
  onClientClosed: options.onClientClosed,
2879
2856
  onPromptActive: options.onPromptActive,
2880
2857
  handleProcessInterrupts: options.handleProcessInterrupts,
2858
+ waitSignal: options.waitSignal,
2881
2859
  client: options.sharedClient
2882
2860
  };
2883
2861
  }
@@ -2919,7 +2897,64 @@ async function runQueuedTask(sessionRecordId, task, options) {
2919
2897
  task.close();
2920
2898
  }
2921
2899
  }
2900
+ async function waitForSessionTurn(options) {
2901
+ const waiting = new AbortController();
2902
+ const onInterrupt = () => waiting.abort(new InterruptedError());
2903
+ const signals = [
2904
+ "SIGINT",
2905
+ "SIGTERM",
2906
+ "SIGHUP"
2907
+ ];
2908
+ for (const signal of signals) process.once(signal, onInterrupt);
2909
+ const timeoutMs = options.timeoutMs;
2910
+ const timeout = timeoutMs != null && timeoutMs > 0 ? setTimeout(() => waiting.abort(new TimeoutError(timeoutMs)), timeoutMs) : void 0;
2911
+ const signal = options.waitSignal ? AbortSignal.any([waiting.signal, options.waitSignal]) : waiting.signal;
2912
+ try {
2913
+ const { acpxRecordId } = await resolveSessionRecord(options.sessionRecordId);
2914
+ return {
2915
+ recordId: acpxRecordId,
2916
+ ownership: await acquireSessionTurn(acpxRecordId, signal)
2917
+ };
2918
+ } catch (error) {
2919
+ signal.throwIfAborted();
2920
+ throw error;
2921
+ } finally {
2922
+ clearTimeout(timeout);
2923
+ for (const signal of signals) process.off(signal, onInterrupt);
2924
+ }
2925
+ }
2922
2926
  async function runSessionPrompt(options) {
2927
+ let turn;
2928
+ try {
2929
+ turn = await waitForSessionTurn(options);
2930
+ } catch (error) {
2931
+ if (options.waitSignal?.aborted && error === options.waitSignal.reason) {
2932
+ const record = await resolveSessionRecord(options.sessionRecordId);
2933
+ return {
2934
+ sessionId: record.acpxRecordId,
2935
+ stopReason: "cancelled",
2936
+ permissionStats: {
2937
+ requested: 0,
2938
+ approved: 0,
2939
+ denied: 0,
2940
+ cancelled: 0
2941
+ },
2942
+ record,
2943
+ resumed: false
2944
+ };
2945
+ }
2946
+ throw error;
2947
+ }
2948
+ try {
2949
+ return await runOwnedSessionPrompt({
2950
+ ...options,
2951
+ sessionRecordId: turn.recordId
2952
+ });
2953
+ } finally {
2954
+ await turn.ownership[Symbol.asyncDispose]();
2955
+ }
2956
+ }
2957
+ async function runOwnedSessionPrompt(options) {
2923
2958
  const stopTotalTimer = startPerfTimer("runtime.prompt.total");
2924
2959
  const output = options.outputFormatter;
2925
2960
  const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
@@ -2927,13 +2962,12 @@ async function runSessionPrompt(options) {
2927
2962
  return await resolveSessionRecord(options.sessionRecordId);
2928
2963
  });
2929
2964
  const conversation = cloneSessionConversation(record);
2930
- let acpxState = cloneSessionAcpxState(record.acpx);
2965
+ record.acpx = cloneSessionAcpxState(record.acpx);
2931
2966
  const promptStartedAt = isoNow();
2932
2967
  const promptMessageId = recordPromptSubmission(conversation, options.prompt, promptStartedAt);
2933
2968
  record.lastPromptAt = promptStartedAt;
2934
2969
  record.lastUsedAt = promptStartedAt;
2935
2970
  applyConversation(record, conversation);
2936
- record.acpx = acpxState;
2937
2971
  await writeSessionRecord(record);
2938
2972
  output.setContext({ sessionId: record.acpxRecordId });
2939
2973
  const eventWriter = await measurePerf("session.events.open", async () => {
@@ -2975,7 +3009,6 @@ async function runSessionPrompt(options) {
2975
3009
  await flushPendingMessages(false);
2976
3010
  record.lastUsedAt = isoNow();
2977
3011
  applyConversation(record, conversation);
2978
- record.acpx = acpxState;
2979
3012
  await preserveClosedState();
2980
3013
  await eventWriter.checkpoint();
2981
3014
  },
@@ -3026,14 +3059,14 @@ async function runSessionPrompt(options) {
3026
3059
  },
3027
3060
  onSessionUpdate: (notification) => {
3028
3061
  if (promptTurnActive) promptTurnHadSideEffects = true;
3029
- acpxState = recordSessionUpdate(conversation, acpxState, notification);
3062
+ record.acpx = recordSessionUpdate(conversation, record.acpx, notification);
3030
3063
  trimConversationForRuntime(conversation);
3031
3064
  liveCheckpoint.request();
3032
3065
  options.onSessionUpdate?.(notification);
3033
3066
  },
3034
3067
  onClientOperation: (operation) => {
3035
3068
  if (promptTurnActive) promptTurnHadSideEffects = true;
3036
- acpxState = recordClientOperation(conversation, acpxState, operation);
3069
+ record.acpx = recordClientOperation(conversation, record.acpx, operation);
3037
3070
  trimConversationForRuntime(conversation);
3038
3071
  liveCheckpoint.request();
3039
3072
  options.onClientOperation?.(operation);
@@ -3052,15 +3085,15 @@ async function runSessionPrompt(options) {
3052
3085
  await client.setSessionMode(activeSessionIdForControl, modeId);
3053
3086
  },
3054
3087
  setSessionModel: async (modelId) => {
3055
- const models = advertisedModelState(acpxState);
3088
+ const models = advertisedModelState(record.acpx);
3056
3089
  const response = await client.setSessionModel(activeSessionIdForControl, modelId, models);
3057
- acpxState = applyModelSelection(acpxState, modelId, response);
3090
+ record.acpx = applyModelSelection(record.acpx, modelId, response);
3058
3091
  return response;
3059
3092
  },
3060
3093
  setSessionConfigOption: async (configId, value) => {
3061
- const modelConfigId = advertisedModelState(acpxState)?.configId;
3094
+ const modelConfigId = advertisedModelState(record.acpx)?.configId;
3062
3095
  const response = await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
3063
- acpxState = applyConfigOptionSelection(acpxState, configId, value, response, modelConfigId);
3096
+ record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response, modelConfigId);
3064
3097
  return response;
3065
3098
  }
3066
3099
  };
@@ -3095,7 +3128,6 @@ async function runSessionPrompt(options) {
3095
3128
  }
3096
3129
  });
3097
3130
  });
3098
- acpxState = mergeConnectedModelState(acpxState, record.acpx);
3099
3131
  flushConnectOutput(connected.loadError);
3100
3132
  emitConnectPerfMetric(connectStartedAt, options.verbose);
3101
3133
  return connected;
@@ -3134,11 +3166,8 @@ async function runSessionPrompt(options) {
3134
3166
  };
3135
3167
  const handlePromptFailure = async (error, attempt) => {
3136
3168
  const snapshot = client.getAgentLifecycleSnapshot();
3137
- if (shouldRetryRuntimePrompt(error, attempt, options.promptRetries ?? 0, snapshot, () => promptTurnHadSideEffects)) {
3138
- await waitBeforePromptRetry(error, attempt, options.promptRetries ?? 0, options.suppressSdkConsoleErrors);
3139
- return promptTurnHadSideEffects ? await failRuntimePrompt(error, snapshot) : "retry";
3140
- }
3141
- return await failRuntimePrompt(error, snapshot);
3169
+ if (snapshot.lastExit?.unexpectedDuringPrompt !== true && await preparePromptRetry(error, attempt, options.promptRetries ?? 0, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors)) return;
3170
+ await failRuntimePrompt(error, snapshot);
3142
3171
  };
3143
3172
  const failRuntimePrompt = async (error, snapshot) => {
3144
3173
  promptTurnActive = false;
@@ -3153,7 +3182,6 @@ async function runSessionPrompt(options) {
3153
3182
  output.flush();
3154
3183
  record.lastUsedAt = isoNow();
3155
3184
  applyConversation(record, conversation);
3156
- record.acpx = acpxState;
3157
3185
  const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
3158
3186
  attachAcpErrorPayload(propagated, normalizedError.acp);
3159
3187
  propagated.outputAlreadyEmitted = matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted;
@@ -3165,7 +3193,7 @@ async function runSessionPrompt(options) {
3165
3193
  for (let attempt = 0;; attempt++) try {
3166
3194
  return await runPromptAttempt(sessionId, attempt);
3167
3195
  } catch (error) {
3168
- if (await handlePromptFailure(error, attempt) === "retry") continue;
3196
+ await handlePromptFailure(error, attempt);
3169
3197
  }
3170
3198
  };
3171
3199
  const savePromptSuccess = async (response) => {
@@ -3178,7 +3206,6 @@ async function runSessionPrompt(options) {
3178
3206
  record.protocolVersion = client.initializeResult?.protocolVersion;
3179
3207
  record.agentCapabilities = client.initializeResult?.agentCapabilities;
3180
3208
  applyConversation(record, conversation);
3181
- record.acpx = acpxState;
3182
3209
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3183
3210
  stopTotalTimer();
3184
3211
  return response;
@@ -3193,7 +3220,6 @@ async function runSessionPrompt(options) {
3193
3220
  timeoutMs: options.timeoutMs,
3194
3221
  suppressWarnings: options.suppressSdkConsoleErrors
3195
3222
  });
3196
- acpxState = cloneSessionAcpxState(record.acpx);
3197
3223
  output.setContext({ sessionId: record.acpxRecordId });
3198
3224
  await liveCheckpoint.checkpoint();
3199
3225
  const response = await savePromptSuccess(await runPromptWithRetries(activeSessionId));
@@ -3210,7 +3236,6 @@ async function runSessionPrompt(options) {
3210
3236
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3211
3237
  record.lastUsedAt = isoNow();
3212
3238
  applyConversation(record, conversation);
3213
- record.acpx = acpxState;
3214
3239
  await flushPendingMessages(false).catch(() => {});
3215
3240
  if (closeClientOnExit) await client.close();
3216
3241
  };
@@ -3233,7 +3258,6 @@ async function runSessionPrompt(options) {
3233
3258
  if (closeClientOnExit) await client.close();
3234
3259
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3235
3260
  applyConversation(record, conversation);
3236
- record.acpx = acpxState;
3237
3261
  await liveCheckpoint.flush().catch(() => {});
3238
3262
  await flushPendingMessages(false).catch(() => {});
3239
3263
  await preserveClosedState().catch(() => {});
@@ -3290,10 +3314,7 @@ async function runOnce(options) {
3290
3314
  for (let attempt = 0;; attempt++) try {
3291
3315
  return await runExecPromptAttempt(sessionId);
3292
3316
  } catch (error) {
3293
- if (shouldRetryPromptAttempt(error, attempt, maxRetries, () => promptTurnHadSideEffects)) {
3294
- await waitBeforePromptRetry(error, attempt, maxRetries, options.suppressSdkConsoleErrors);
3295
- if (!promptTurnHadSideEffects) continue;
3296
- }
3317
+ if (await preparePromptRetry(error, attempt, maxRetries, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors)) continue;
3297
3318
  promptTurnActive = false;
3298
3319
  throw error;
3299
3320
  }
@@ -3456,19 +3477,17 @@ function logDeferredCancelFailure(error, verbose) {
3456
3477
  process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
3457
3478
  }
3458
3479
  function queueOwnerExitIsFatal(exit) {
3459
- return exit.exited && (exit.spawnError !== void 0 || exit.code !== 0 || exit.signal !== null);
3480
+ return exit.exited && (exit.spawnError !== void 0 || exit.inputError !== void 0 || exit.code !== 0 || exit.signal !== null);
3460
3481
  }
3461
3482
  function logQueueOwnerReady(params) {
3462
3483
  if (!params.verbose) return;
3463
3484
  process.stderr.write(`[acpx] queue owner ready for session ${params.sessionId} (ttlMs=${params.ttlMs}, maxQueueDepth=${params.maxQueueDepth})\n`);
3464
3485
  }
3465
3486
  async function closeQueueOwnerRuntime(params) {
3466
- if (params.heartbeatTimer) clearInterval(params.heartbeatTimer);
3467
3487
  params.turnController.beginClosing();
3468
3488
  await params.sharedClient.close().catch(() => {});
3469
3489
  await params.owner?.close();
3470
3490
  await writeQueueOwnerLifecycleSnapshot(params.sessionId, params.sharedClient);
3471
- await releaseQueueOwnerLease(params.lease);
3472
3491
  if (params.verbose) process.stderr.write(`[acpx] queue owner stopped for session ${params.sessionId}\n`);
3473
3492
  }
3474
3493
  async function writeQueueOwnerLifecycleSnapshot(sessionId, sharedClient) {
@@ -3525,9 +3544,7 @@ function createQueueOwnerShutdownController(params) {
3525
3544
  shutdownPromise ??= (async () => {
3526
3545
  await activeTurnShutdown;
3527
3546
  await closeQueueOwnerRuntime({
3528
- lease: params.lease,
3529
3547
  owner: params.getOwner(),
3530
- heartbeatTimer: void 0,
3531
3548
  turnController: params.turnController,
3532
3549
  sharedClient: params.sharedClient,
3533
3550
  sessionId: params.sessionId,
@@ -3553,6 +3570,13 @@ async function runSessionQueueOwner(options) {
3553
3570
  fingerprint: options.mcpConfigFingerprint
3554
3571
  });
3555
3572
  if (!lease) return;
3573
+ try {
3574
+ await runQueueOwnerRuntime(options, lease);
3575
+ } finally {
3576
+ await releaseQueueOwnerLease(lease);
3577
+ }
3578
+ }
3579
+ async function runQueueOwnerRuntime(options, lease) {
3556
3580
  const sessionRecord = await resolveSessionRecord(options.sessionId);
3557
3581
  let owner;
3558
3582
  let heartbeatTimer;
@@ -3584,15 +3608,14 @@ async function runSessionQueueOwner(options) {
3584
3608
  return true;
3585
3609
  };
3586
3610
  const runPromptTurn = async (run) => {
3587
- turnController.beginTurn();
3611
+ const waitSignal = turnController.beginTurn();
3588
3612
  try {
3589
- return await run();
3613
+ return await run(waitSignal);
3590
3614
  } finally {
3591
3615
  turnController.endTurn();
3592
3616
  }
3593
3617
  };
3594
3618
  const shutdown = createQueueOwnerShutdownController({
3595
- lease,
3596
3619
  getOwner: () => owner,
3597
3620
  stopHeartbeat: () => {
3598
3621
  if (heartbeatTimer) {
@@ -3652,7 +3675,7 @@ async function runSessionQueueOwner(options) {
3652
3675
  const task = await owner.nextTask(pollTimeoutMs);
3653
3676
  if (!task) break;
3654
3677
  isFirstTask = false;
3655
- const turnPromise = runPromptTurn(async () => {
3678
+ const turnPromise = runPromptTurn(async (waitSignal) => {
3656
3679
  try {
3657
3680
  await runQueuedTask(options.sessionId, task, {
3658
3681
  sharedClient,
@@ -3670,7 +3693,8 @@ async function runSessionQueueOwner(options) {
3670
3693
  turnController.markPromptActive();
3671
3694
  await applyPendingCancel();
3672
3695
  },
3673
- handleProcessInterrupts: false
3696
+ handleProcessInterrupts: false,
3697
+ waitSignal
3674
3698
  });
3675
3699
  } finally {
3676
3700
  checkpointPerfMetricsCapture();
@@ -3749,7 +3773,6 @@ var session_exports = /* @__PURE__ */ __exportAll({
3749
3773
  listAgentSessions: () => listAgentSessions,
3750
3774
  listSessions: () => listSessions,
3751
3775
  listSessionsForAgent: () => listSessionsForAgent,
3752
- mergeConnectedModelState: () => mergeConnectedModelState,
3753
3776
  normalizeQueueOwnerTtlMs: () => normalizeQueueOwnerTtlMs,
3754
3777
  pruneSessions: () => pruneSessions,
3755
3778
  queueOwnerRuntimeTestInternals: () => queueOwnerRuntimeTestInternals,
@@ -4019,13 +4042,7 @@ function isFinalStatus(status) {
4019
4042
  return status === "completed" || status === "failed";
4020
4043
  }
4021
4044
  function toStatusLabel(status) {
4022
- switch (status) {
4023
- case "in_progress": return "running";
4024
- case "pending": return "pending";
4025
- case "completed": return "completed";
4026
- case "failed": return "failed";
4027
- default: return "running";
4028
- }
4045
+ return isFinalStatus(status) || status === "pending" ? status : "running";
4029
4046
  }
4030
4047
  function asRecord(value) {
4031
4048
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
@@ -4058,14 +4075,7 @@ function indentBlock(value, prefix) {
4058
4075
  return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
4059
4076
  }
4060
4077
  function dedupeStrings(values) {
4061
- const seen = /* @__PURE__ */ new Set();
4062
- const result = [];
4063
- for (const value of values) {
4064
- if (seen.has(value)) continue;
4065
- seen.add(value);
4066
- result.push(value);
4067
- }
4068
- return result;
4078
+ return [...new Set(values)];
4069
4079
  }
4070
4080
  function safeJson(value, spacing) {
4071
4081
  const seen = /* @__PURE__ */ new WeakSet();
@@ -4374,7 +4384,6 @@ var TextOutputFormatter = class {
4374
4384
  thoughtBuffer = "";
4375
4385
  wroteAny = false;
4376
4386
  atLineStart = true;
4377
- section = null;
4378
4387
  constructor(stdout, suppressReads) {
4379
4388
  this.stdout = stdout;
4380
4389
  this.useColor = Boolean(stdout.isTTY);
@@ -4382,6 +4391,12 @@ var TextOutputFormatter = class {
4382
4391
  }
4383
4392
  setContext(_context) {}
4384
4393
  onAcpMessage(message) {
4394
+ const notice = parsePermissionNotice(message);
4395
+ if (notice) {
4396
+ this.beginSection();
4397
+ this.writeLine(`${this.bold("[permission]")} ${notice}`);
4398
+ return;
4399
+ }
4385
4400
  const notification = extractSessionUpdateNotification(message);
4386
4401
  if (notification) {
4387
4402
  this.renderSessionUpdate(notification);
@@ -4417,7 +4432,7 @@ var TextOutputFormatter = class {
4417
4432
  renderSessionUpdateBody(update) {
4418
4433
  switch (update.sessionUpdate) {
4419
4434
  case "agent_message_chunk":
4420
- if (update.content.type === "text") this.writeAssistantChunk(update.content.text);
4435
+ if (update.content.type === "text") this.write(update.content.text);
4421
4436
  return;
4422
4437
  case "agent_thought_chunk":
4423
4438
  if (update.content.type === "text") this.thoughtBuffer += update.content.text;
@@ -4435,24 +4450,24 @@ var TextOutputFormatter = class {
4435
4450
  }
4436
4451
  }
4437
4452
  renderPlanUpdate(entries) {
4438
- this.beginSection("plan");
4453
+ this.beginSection();
4439
4454
  this.writeLine(this.bold("[plan]"));
4440
4455
  for (const entry of entries) this.writeLine(` - [${entry.status}] ${entry.content}`);
4441
4456
  }
4442
4457
  renderDone(stopReason) {
4443
4458
  this.flushThoughtBuffer();
4444
- this.beginSection("done");
4459
+ this.beginSection();
4445
4460
  this.writeLine(this.dim(`[done] ${stopReason}`));
4446
4461
  }
4447
4462
  onError(params) {
4448
4463
  this.flushThoughtBuffer();
4449
- this.beginSection("done");
4464
+ this.beginSection();
4450
4465
  this.writeLine(this.formatAnsi(`[error] ${params.code}: ${params.message}`, "31"));
4451
4466
  for (const hint of getTextErrorRemediationHints(params)) this.writeLine(this.dim(hint));
4452
4467
  }
4453
4468
  onClientOperation(operation) {
4454
4469
  this.flushThoughtBuffer();
4455
- this.beginSection("client");
4470
+ this.beginSection();
4456
4471
  const normalizedStatus = operation.status === "completed" ? "completed" : operation.status === "failed" ? "failed" : "in_progress";
4457
4472
  const statusText = this.colorStatus(operation.status, normalizedStatus);
4458
4473
  this.writeLine(`${this.bold("[client]")} ${operation.summary} (${statusText})`);
@@ -4463,7 +4478,7 @@ var TextOutputFormatter = class {
4463
4478
  }
4464
4479
  onPermissionEscalation(event) {
4465
4480
  this.flushThoughtBuffer();
4466
- this.beginSection("client");
4481
+ this.beginSection();
4467
4482
  this.writeLine(`${this.bold("[permission]")} ${event.message}`);
4468
4483
  const details = [
4469
4484
  `sessionId: ${event.sessionId}`,
@@ -4489,21 +4504,15 @@ var TextOutputFormatter = class {
4489
4504
  writeLine(line) {
4490
4505
  this.write(`${line}\n`);
4491
4506
  }
4492
- beginSection(next) {
4507
+ beginSection() {
4493
4508
  if (!this.atLineStart) this.write("\n");
4494
4509
  if (this.wroteAny) this.write("\n");
4495
- this.section = next;
4496
- }
4497
- writeAssistantChunk(text) {
4498
- if (!text) return;
4499
- this.section = "assistant";
4500
- this.write(text);
4501
4510
  }
4502
4511
  flushThoughtBuffer() {
4503
4512
  const thought = truncate(normalizeLineEndings(this.thoughtBuffer).trim(), MAX_THOUGHT_CHARS);
4504
4513
  this.thoughtBuffer = "";
4505
4514
  if (!thought) return;
4506
- this.beginSection("thought");
4515
+ this.beginSection();
4507
4516
  const [firstLine, ...restLines] = thought.split("\n");
4508
4517
  this.writeLine(this.dim(`[thinking] ${firstLine}`));
4509
4518
  for (const line of restLines) this.writeLine(this.dim(` ${line}`));
@@ -4512,17 +4521,25 @@ var TextOutputFormatter = class {
4512
4521
  const state = this.getOrCreateToolState(update.toolCallId);
4513
4522
  this.mergeToolState(state, update);
4514
4523
  const status = asStatus(state.status);
4515
- if (isFinalStatus(status)) {
4516
- const signature = this.toolSignature(state);
4517
- if (signature !== state.finalSignature) {
4518
- state.finalSignature = signature;
4519
- this.renderFinalToolState(state, status);
4520
- }
4521
- return;
4522
- }
4523
- if (state.startedPrinted) return;
4524
- state.startedPrinted = true;
4525
- this.renderStartingToolState(state, status);
4524
+ const final = isFinalStatus(status);
4525
+ if (!final && state.startedPrinted) return;
4526
+ const display = {
4527
+ input: summarizeToolInput(state.rawInput),
4528
+ files: formatLocations(state.locations),
4529
+ output: final ? renderToolOutput(state, this.suppressReads) : void 0
4530
+ };
4531
+ if (final) {
4532
+ const payload = {
4533
+ title: state.title,
4534
+ status: state.status,
4535
+ kind: state.kind,
4536
+ ...display
4537
+ };
4538
+ const signature = safeJson(payload, 0) ?? JSON.stringify(payload);
4539
+ if (signature === state.finalSignature) return;
4540
+ state.finalSignature = signature;
4541
+ } else state.startedPrinted = true;
4542
+ this.renderToolState(state, status, display);
4526
4543
  }
4527
4544
  getOrCreateToolState(toolCallId) {
4528
4545
  const existing = this.toolStates.get(toolCallId);
@@ -4549,42 +4566,17 @@ var TextOutputFormatter = class {
4549
4566
  if (update.rawOutput !== void 0) state.rawOutput = update.rawOutput;
4550
4567
  if (update.content !== void 0) state.content = update.content;
4551
4568
  }
4552
- toolSignature(state) {
4553
- const signaturePayload = {
4554
- title: state.title,
4555
- status: state.status,
4556
- kind: state.kind,
4557
- input: summarizeToolInput(state.rawInput),
4558
- files: formatLocations(state.locations),
4559
- output: renderToolOutput(state, this.suppressReads)
4560
- };
4561
- return safeJson(signaturePayload, 0) ?? JSON.stringify(signaturePayload);
4562
- }
4563
- renderStartingToolState(state, status) {
4564
- this.beginSection("tool");
4565
- const title = state.title ?? state.id;
4566
- const label = status === "pending" ? "pending" : "running";
4567
- const statusText = this.colorStatus(label, status);
4568
- this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
4569
- const input = summarizeToolInput(state.rawInput);
4570
- if (input) this.writeLine(` input: ${input}`);
4571
- const files = formatLocations(state.locations);
4572
- if (files) this.writeLine(` files: ${files}`);
4573
- }
4574
- renderFinalToolState(state, status) {
4575
- this.beginSection("tool");
4569
+ renderToolState(state, status, display) {
4570
+ this.beginSection();
4576
4571
  const title = state.title ?? state.id;
4577
4572
  const statusText = this.colorStatus(toStatusLabel(status), status);
4578
4573
  this.writeLine(`${this.bold("[tool]")} ${title} (${statusText})`);
4579
- if (state.kind) this.writeLine(` kind: ${state.kind}`);
4580
- const input = summarizeToolInput(state.rawInput);
4581
- if (input) this.writeLine(` input: ${input}`);
4582
- const files = formatLocations(state.locations);
4583
- if (files) this.writeLine(` files: ${files}`);
4584
- const output = renderToolOutput(state, this.suppressReads);
4585
- if (output) {
4574
+ if (isFinalStatus(status) && state.kind) this.writeLine(` kind: ${state.kind}`);
4575
+ if (display.input) this.writeLine(` input: ${display.input}`);
4576
+ if (display.files) this.writeLine(` files: ${display.files}`);
4577
+ if (display.output) {
4586
4578
  this.writeLine(" output:");
4587
- this.writeLine(indentBlock(limitOutputBlock(output), " "));
4579
+ this.writeLine(indentBlock(limitOutputBlock(display.output), " "));
4588
4580
  }
4589
4581
  }
4590
4582
  formatAnsi(text, code) {
@@ -4618,6 +4610,11 @@ var QuietOutputFormatter = class {
4618
4610
  }
4619
4611
  setContext(_context) {}
4620
4612
  onAcpMessage(message) {
4613
+ const notice = parsePermissionNotice(message);
4614
+ if (notice) {
4615
+ this.stderr.write(`[acpx] permission: ${notice.replace(/\r\n?|\n/g, " ")}\n`);
4616
+ return;
4617
+ }
4621
4618
  const update = extractSessionUpdateNotification(message);
4622
4619
  if (update?.update.sessionUpdate === "agent_message_chunk" && update.update.content.type === "text") {
4623
4620
  this.chunks.push(update.update.content.text);
@@ -4709,6 +4706,6 @@ function createOutputFormatter(format, options = {}) {
4709
4706
  }
4710
4707
  }
4711
4708
  //#endregion
4712
- export { runSessionQueueOwner as a, buildQueueOwnerArgOverride as c, createSessionWithClient as d, cancelSessionPrompt as f, __exportAll as g, isProcessAlive 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 };
4709
+ export { runSessionQueueOwner as a, DISCARD_OUTPUT_FORMATTER as c, installPerfMetricsCapture as d, createSessionWithClient as f, __exportAll as g, DEFAULT_QUEUE_OWNER_TTL_MS as h, session_exports as i, buildQueueOwnerArgOverride as l, probeQueueOwnerHealth as m, getTextErrorRemediationHints as n, runOnce as o, cancelSessionPrompt as p, output_exports as r, sendSessionDirect as s, createOutputFormatter as t, flushPerfMetricsCapture as u };
4713
4710
 
4714
- //# sourceMappingURL=output-CIVecnHU.js.map
4711
+ //# sourceMappingURL=output-uo5a27KB.js.map