acpx 0.19.2 → 0.19.3

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-DWlx5BQ4.js → agent-registry-DWnqUICY.js} +20 -17
  2. package/dist/agent-registry-DWnqUICY.js.map +1 -0
  3. package/dist/agent-registry.js +1 -1
  4. package/dist/{cli-D-s_eTUy.js → cli-C1uofM8S.js} +5 -5
  5. package/dist/{cli-D-s_eTUy.js.map → cli-C1uofM8S.js.map} +1 -1
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +56 -40
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{client-BchvQRpz.d.ts → client-CdG6nVDG.d.ts} +4 -3
  10. package/dist/client-CdG6nVDG.d.ts.map +1 -0
  11. package/dist/{flows-DdxEaeZo.js → flows-hz_Pfsp-.js} +9 -5
  12. package/dist/flows-hz_Pfsp-.js.map +1 -0
  13. package/dist/flows.d.ts +2 -2
  14. package/dist/flows.js +1 -1
  15. package/dist/{invocation-options-HjxSBsK0.js → invocation-options-DbZzMvKM.js} +5 -5
  16. package/dist/{invocation-options-HjxSBsK0.js.map → invocation-options-DbZzMvKM.js.map} +1 -1
  17. package/dist/{ipc-D0ZMQ0q9.js → ipc-BW06Pbb8.js} +338 -289
  18. package/dist/ipc-BW06Pbb8.js.map +1 -0
  19. package/dist/{output-GQnhkJMU.js → output-BjeeG-cj.js} +3 -3
  20. package/dist/{output-GQnhkJMU.js.map → output-BjeeG-cj.js.map} +1 -1
  21. package/dist/{queue-owner-runtime-W0lly6ap.js → queue-owner-runtime-Dhr4gxNE.js} +427 -368
  22. package/dist/queue-owner-runtime-Dhr4gxNE.js.map +1 -0
  23. package/dist/runtime.d.ts +7 -2
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +24 -17
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/{session-options-Dp6mzNSy.d.ts → session-options-X7LhX9BF.d.ts} +2 -1
  28. package/dist/{session-options-Dp6mzNSy.d.ts.map → session-options-X7LhX9BF.d.ts.map} +1 -1
  29. package/dist/{session-watch-_8yxQ6ba.js → session-watch-CgfkUKLA.js} +4 -4
  30. package/dist/{session-watch-_8yxQ6ba.js.map → session-watch-CgfkUKLA.js.map} +1 -1
  31. package/dist/{watch-C0mwpJcN.js → watch-BGdqBiNX.js} +2 -2
  32. package/dist/{watch-C0mwpJcN.js.map → watch-BGdqBiNX.js.map} +1 -1
  33. package/package.json +11 -11
  34. package/skills/acpx/SKILL.md +7 -2
  35. package/dist/agent-registry-DWlx5BQ4.js.map +0 -1
  36. package/dist/client-BchvQRpz.d.ts.map +0 -1
  37. package/dist/flows-DdxEaeZo.js.map +0 -1
  38. package/dist/ipc-D0ZMQ0q9.js.map +0 -1
  39. package/dist/queue-owner-runtime-W0lly6ap.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { $ as resetPerfMetrics, Bt as PermissionDeniedError, Ct as extractAcpError, E as textPrompt, F as normalizeName, Ft as AgentStartupError, G as SESSION_JOURNAL_SCHEMA, Gt as SessionModelReplayError, Ht as QueueConnectionError, It as AuthPolicyError, Jt as UnsupportedPromptContentError, K as SessionJournalReader, Lt as ClaudeAcpSessionCreateTimeoutError, M as isoNow$1, Mt as AcpxOperationalError, Nt as AgentDisconnectedError, O as absolutePath, Pt as AgentSpawnError, Q as measurePerf, R as resolveSessionRecord, Rt as CopilotAcpUnsupportedError, S as getUnsupportedPromptContentMessage, St as normalizeOutputError, T as promptToDisplayText, U as extractAgentSessionId, Ut as SessionConfigOptionReplayError, Vt as PermissionPromptUnavailableError, W as normalizeAgentSessionId, Wt as SessionModeReplayError, X as getPerfMetricsSnapshot, Y as formatPerfMetric, Z as incrementPerfCounter, _ as createLockOwner, _t as isProcessAlive, a as trySetModelOnRunningOwner, at as isSessionUpdateNotification, bt as isAcpQueryClosedBeforeResponseError, d as releaseQueueOwnerLease, dt as sessionBaseDir, et as setPerfGauge, f as terminateProcess, ft as sessionEventActivePath, g as QueueLeaseGuardSettlementError, h as waitMs$1, i as trySetModeOnRunningOwner, it as isAcpMessageObject, j as findSessionByDirectoryWalk, k as findGitRepositoryRoot, m as tryAcquireQueueOwnerLease, mt as sessionEventSegmentPath, n as tryCloseSessionOnRunningOwner, o as trySubmitToRunningOwner, p as terminateQueueOwnerForSession, pt as sessionEventLockPath, qt as SessionResumeRequiredError, r as trySetConfigOptionOnRunningOwner, rt as isAcpJsonRpcMessage, s as SessionQueueOwner, t as tryCancelOnRunningOwner, tt as startPerfTimer, u as refreshQueueOwnerLease, ut as defaultSessionEventLog, v as lockOwnerPid, wt as isAcpResourceNotFoundError, xt as isRetryablePromptError, y as ProcessDescendants, yt as formatErrorMessage, z as writeSessionRecord, zt as GeminiAcpStartupTimeoutError } from "./ipc-D0ZMQ0q9.js";
2
- import { A as requireAgentStdio, C as PROCESS_HELPER_TIMEOUT_MS, D as isoNow$2, E as isChildProcessRunning, F as waitForChildExit, I as waitForSpawn, M as resolveAgentSessionCwd, N as runTimedExecFile, P as splitCommandLine, T as basenameToken, _ as buildSpawnCommandOptions, b as readWindowsEnvValue, f as resolveBuiltInAgentLaunch, g as buildAgentSpawnCommand, j as resolveAgentCommandParts, v as buildTerminalShellSpawnCommand, w as asAbsoluteCwd, x as resolveWindowsExecutablePath, y as buildTerminalSpawnCommand } from "./agent-registry-DWlx5BQ4.js";
1
+ import { $ as resetPerfMetrics, Bt as GeminiAcpStartupTimeoutError, C as textPrompt, Ct as normalizeOutputError, F as normalizeName, Ft as AgentSpawnError, G as SESSION_JOURNAL_SCHEMA, Gt as SessionModeReplayError, Ht as PermissionPromptUnavailableError, It as AgentStartupError, Jt as SessionResumeRequiredError, K as SessionJournalReader, Kt as SessionModelReplayError, L as readSessionRecord, Lt as AuthPolicyError, M as isoNow$1, Nt as AcpxOperationalError, O as absolutePath, Pt as AgentDisconnectedError, Q as measurePerf, R as resolveSessionRecord, Rt as ClaudeAcpSessionCreateTimeoutError, S as promptToDisplayText, St as isRetryablePromptError, T as lockOwnerPid, Tt as isAcpResourceNotFoundError, U as extractAgentSessionId, Ut as QueueConnectionError, Vt as PermissionDeniedError, W as normalizeAgentSessionId, Wt as SessionConfigOptionReplayError, X as getPerfMetricsSnapshot, Y as formatPerfMetric, Yt as UnsupportedPromptContentError, Z as incrementPerfCounter, _ as ProcessDescendants, a as trySetModelOnRunningOwner, at as isSessionUpdateNotification, bt as formatErrorMessage, d as releaseQueueOwnerLease, dt as sessionBaseDir, et as setPerfGauge, f as terminateProcess, ft as sessionEventActivePath, g as QueueLeaseGuardSettlementError, h as waitMs$1, i as trySetModeOnRunningOwner, it as isAcpMessageObject, j as findSessionByDirectoryWalk, k as findGitRepositoryRoot, m as tryAcquireQueueOwnerLease, mt as sessionEventSegmentPath, n as tryCloseSessionOnRunningOwner, o as trySubmitToRunningOwner, p as terminateQueueOwnerForSession, pt as sessionEventLockPath, r as trySetConfigOptionOnRunningOwner, rt as isAcpJsonRpcMessage, s as SessionQueueOwner, t as tryCancelOnRunningOwner, tt as startPerfTimer, u as refreshQueueOwnerLease, ut as defaultSessionEventLog, vt as isProcessAlive, w as createLockOwner, wt as extractAcpError, xt as isAcpQueryClosedBeforeResponseError, y as getUnsupportedPromptContentMessage, z as writeSessionRecord, zt as CopilotAcpUnsupportedError } from "./ipc-BW06Pbb8.js";
2
+ import { A as requireAgentStdio, C as PROCESS_HELPER_TIMEOUT_MS, D as isoNow$2, E as isChildProcessRunning, F as waitForChildExit, I as waitForSpawn, M as resolveAgentSessionCwd, N as runTimedExecFile, P as splitCommandLine, T as basenameToken, _ as buildSpawnCommandOptions, b as readWindowsEnvValue, f as resolveBuiltInAgentLaunch, g as buildAgentSpawnCommand, j as resolveAgentCommandParts, v as buildTerminalShellSpawnCommand, w as asAbsoluteCwd, x as resolveWindowsExecutablePath, y as buildTerminalSpawnCommand } from "./agent-registry-DWnqUICY.js";
3
3
  import fs, { readFileSync, realpathSync } from "node:fs";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import path from "node:path";
@@ -8,14 +8,14 @@ import fs$1 from "node:fs/promises";
8
8
  import { appendRegularFile, appendRegularFileSync, withTempFile } from "@openclaw/fs-safe/advanced";
9
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
+ import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
12
+ import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
11
13
  import { Readable, Writable } from "node:stream";
12
14
  import { PROTOCOL_VERSION, RequestError, client, methods } from "@agentclientprotocol/sdk";
13
15
  import { FsSafeError } from "@openclaw/fs-safe/errors";
14
16
  import { isPathInside } from "@openclaw/fs-safe/path";
15
17
  import { root } from "@openclaw/fs-safe/root";
16
18
  import readline from "node:readline/promises";
17
- import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
18
- import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
19
19
  //#region src/async-control.ts
20
20
  var TimeoutError = class extends Error {
21
21
  constructor(timeoutMs) {
@@ -346,6 +346,246 @@ async function listSessionEvents(sessionId, maxSegments) {
346
346
  }).readAcpMessages();
347
347
  }
348
348
  //#endregion
349
+ //#region src/session/turn-ownership.ts
350
+ const LOCK_RETRY_MS = 15;
351
+ const INCOMPLETE_RESERVATION_GRACE_MS = 15e3;
352
+ let lastCreatedAt = 0;
353
+ const localTurns = /* @__PURE__ */ new Map();
354
+ async function readLock(filePath) {
355
+ try {
356
+ const stat = await fs$1.lstat(filePath, { bigint: true });
357
+ if (!stat.isFile()) throw new Error(`Session turn lock is not a regular file: ${filePath}`);
358
+ return {
359
+ stat,
360
+ payload: await fs$1.readFile(filePath, "utf8")
361
+ };
362
+ } catch (error) {
363
+ if (error.code === "ENOENT") return;
364
+ throw error;
365
+ }
366
+ }
367
+ function parseLock(payload) {
368
+ try {
369
+ return JSON.parse(payload);
370
+ } catch {
371
+ return;
372
+ }
373
+ }
374
+ async function tryAcquireGuard(filePath, owner, signal) {
375
+ try {
376
+ return await acquireFileLock(filePath, {
377
+ managerKey: "acpx.session-turn",
378
+ lockPath: `${filePath}.guard`,
379
+ staleMs: Infinity,
380
+ timeoutMs: LOCK_RETRY_MS,
381
+ retry: {
382
+ retries: 8,
383
+ minTimeout: 1,
384
+ maxTimeout: 2,
385
+ factor: 1,
386
+ randomize: false
387
+ },
388
+ staleRecovery: "remove-if-unchanged",
389
+ payload: () => owner.payload,
390
+ shouldReclaim: ({ payload }) => owner.hasExited(payload, false, signal),
391
+ shouldRemoveStaleLock: ({ payload }) => owner.hasExited(payload, true, signal)
392
+ });
393
+ } catch (error) {
394
+ if (error.code === "file_lock_timeout") return;
395
+ throw error;
396
+ }
397
+ }
398
+ async function removeObservedLock(filePath, observed) {
399
+ const current = await readLock(filePath);
400
+ if (!current) return true;
401
+ if (current.payload !== observed.payload || current.stat.mtimeNs !== observed.stat.mtimeNs || current.stat.size !== observed.stat.size) return false;
402
+ await fs$1.unlink(filePath).catch((error) => {
403
+ if (error.code !== "ENOENT") throw error;
404
+ });
405
+ return true;
406
+ }
407
+ async function recoverAbandonedLock(filePath, owner, signal) {
408
+ const observed = await readLock(filePath);
409
+ if (!observed) return true;
410
+ const payload = parseLock(observed.payload);
411
+ const pid = lockOwnerPid(payload);
412
+ if (pid && (!await owner.hasExited(payload, false, signal) || !await owner.hasExited(payload, true, signal))) return false;
413
+ if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return false;
414
+ if (await removeObservedLock(filePath, observed)) {
415
+ incrementPerfCounter("session.events.stale_lock_recovered");
416
+ return true;
417
+ }
418
+ return false;
419
+ }
420
+ async function publishLock(filePath, payload, signal) {
421
+ await withTempFile({
422
+ rootDir: path.dirname(filePath),
423
+ prefix: "session-turn",
424
+ fileName: "lock"
425
+ }, async (temporaryPath) => {
426
+ await fs$1.writeFile(temporaryPath, payload, {
427
+ encoding: "utf8",
428
+ flag: "wx",
429
+ mode: 384
430
+ });
431
+ signal?.throwIfAborted();
432
+ try {
433
+ await fs$1.link(temporaryPath, filePath);
434
+ } catch (error) {
435
+ if (!isHardlinkFallbackError(error)) throw error;
436
+ await fs$1.writeFile(filePath, payload, {
437
+ encoding: "utf8",
438
+ flag: "wx",
439
+ mode: 384
440
+ });
441
+ }
442
+ });
443
+ }
444
+ async function removePublishedLock(filePath, payload) {
445
+ const current = await readLock(filePath);
446
+ if (current?.payload === payload) await removeObservedLock(filePath, current);
447
+ }
448
+ async function rejectAcquisition(filePath, receipt, error) {
449
+ const failures = [error];
450
+ await disposeTurn(filePath, receipt).catch((cleanupError) => {
451
+ failures.push(...cleanupError instanceof AggregateError ? cleanupError.errors : [cleanupError]);
452
+ });
453
+ if (failures.length > 1) throw new AggregateError(failures, "Session turn acquisition and cleanup failed");
454
+ throw error;
455
+ }
456
+ async function releaseTurnGuard(receipt) {
457
+ await receipt.guard?.release();
458
+ receipt.guard = void 0;
459
+ }
460
+ async function removeTurnMarker(filePath, receipt) {
461
+ if (!receipt.removeMarker) return true;
462
+ if (receipt.guard && !await receipt.guard.verifyStillHeld()) await releaseTurnGuard(receipt);
463
+ receipt.guard ??= await tryAcquireGuard(filePath, receipt.owner);
464
+ if (!receipt.guard) return false;
465
+ await receipt.removeMarker();
466
+ receipt.removeMarker = void 0;
467
+ return true;
468
+ }
469
+ async function settleTurn(filePath, receipt) {
470
+ if (receipt.settled) return true;
471
+ const pending = receipt.pending ??= (async () => {
472
+ receipt.failed = false;
473
+ const failures = [];
474
+ try {
475
+ if (!await removeTurnMarker(filePath, receipt)) return false;
476
+ } catch (error) {
477
+ failures.push(error);
478
+ }
479
+ if (failures.length === 0 || !receipt.admitted) try {
480
+ await releaseTurnGuard(receipt);
481
+ } catch (error) {
482
+ failures.push(error);
483
+ }
484
+ if (failures.length > 1) throw new AggregateError(failures, "Session turn cleanup failed");
485
+ if (failures.length === 1) throw failures[0];
486
+ receipt.settled = true;
487
+ localTurns.delete(filePath);
488
+ return true;
489
+ })();
490
+ try {
491
+ return await pending;
492
+ } finally {
493
+ if (receipt.pending === pending) {
494
+ receipt.pending = void 0;
495
+ receipt.failed = !receipt.settled;
496
+ }
497
+ }
498
+ }
499
+ async function disposeTurn(filePath, receipt) {
500
+ if (!await settleTurn(filePath, receipt)) throw new Error(`Session turn cleanup is waiting for its guard: ${filePath}`);
501
+ }
502
+ async function tryPublishLock(filePath, payload, owner, signal) {
503
+ for (;;) try {
504
+ await publishLock(filePath, payload, signal);
505
+ return true;
506
+ } catch (error) {
507
+ if (error.code !== "EEXIST") throw error;
508
+ if (!await recoverAbandonedLock(filePath, owner, signal)) return false;
509
+ }
510
+ }
511
+ async function tryAcquireTurnCleanup(filePath, payload, owner, signal) {
512
+ const previous = localTurns.get(filePath);
513
+ if (previous) {
514
+ if (previous.failed) await settleTurn(filePath, previous);
515
+ return;
516
+ }
517
+ const receipt = {
518
+ owner,
519
+ removeMarker: async () => await removePublishedLock(filePath, payload),
520
+ admitted: false,
521
+ failed: false,
522
+ settled: false
523
+ };
524
+ localTurns.set(filePath, receipt);
525
+ try {
526
+ receipt.guard = await tryAcquireGuard(filePath, owner, signal);
527
+ } catch (error) {
528
+ localTurns.delete(filePath);
529
+ throw error;
530
+ }
531
+ if (!receipt.guard) {
532
+ localTurns.delete(filePath);
533
+ return;
534
+ }
535
+ return receipt;
536
+ }
537
+ async function tryAcquireTurn(filePath, payload, owner, signal) {
538
+ const receipt = await tryAcquireTurnCleanup(filePath, payload, owner, signal);
539
+ if (!receipt) return;
540
+ try {
541
+ signal?.throwIfAborted();
542
+ if (!await tryPublishLock(filePath, payload, owner, signal)) {
543
+ await disposeTurn(filePath, receipt);
544
+ return;
545
+ }
546
+ const observed = await readLock(filePath);
547
+ if (observed?.payload !== payload) throw new Error(`Session turn ownership changed before admission: ${filePath}`);
548
+ signal?.throwIfAborted();
549
+ receipt.admitted = true;
550
+ receipt.removeMarker = async () => await removeObservedLock(filePath, observed);
551
+ return { [Symbol.asyncDispose]: async () => await disposeTurn(filePath, receipt) };
552
+ } catch (error) {
553
+ return await rejectAcquisition(filePath, receipt, error);
554
+ }
555
+ }
556
+ async function acquireSessionTurn(recordId, signal) {
557
+ return await acquireSessionOwnership(sessionEventLockPath(recordId), signal);
558
+ }
559
+ async function acquireSessionScope(scope, signal) {
560
+ return await acquireSessionTurn(`ensure:${createHash("sha256").update(JSON.stringify([
561
+ scope.agentCommand,
562
+ scope.cwd,
563
+ scope.name
564
+ ])).digest("hex")}`, signal);
565
+ }
566
+ async function acquireSessionImport(signal) {
567
+ return await acquireSessionOwnership(path.join(sessionBaseDir(), ".import-admission.lock"), signal);
568
+ }
569
+ async function acquireSessionOwnership(requestedPath, signal) {
570
+ await fs$1.mkdir(path.dirname(requestedPath), {
571
+ recursive: true,
572
+ mode: 448
573
+ });
574
+ const filePath = path.join(await fs$1.realpath(path.dirname(requestedPath)), path.basename(requestedPath));
575
+ const owner = await createLockOwner({ signal });
576
+ lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);
577
+ const payload = `${JSON.stringify({
578
+ ...owner.payload,
579
+ created_at: new Date(lastCreatedAt).toISOString()
580
+ }, null, 2)}\n`;
581
+ for (;;) {
582
+ signal?.throwIfAborted();
583
+ const turn = await tryAcquireTurn(filePath, payload, owner, signal);
584
+ if (turn) return turn;
585
+ await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
586
+ }
587
+ }
588
+ //#endregion
349
589
  //#region src/session/execution/contracts.ts
350
590
  const DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
351
591
  function normalizeQueueOwnerTtlMs(ttlMs) {
@@ -1016,10 +1256,10 @@ function assignClaudeCodeSystemPrompt(target, systemPrompt) {
1016
1256
  function isAppendSystemPrompt(value) {
1017
1257
  return !!value && typeof value === "object" && typeof value.append === "string" && value.append.length > 0;
1018
1258
  }
1019
- function resolveClaudeCodeExecutable(platform = process.platform, env = process.env) {
1259
+ function resolveClaudeCodeExecutable(platform = process.platform, env = process.env, cwd) {
1020
1260
  if (platform !== "win32") return;
1021
1261
  if (readWindowsEnvValue(env, "CLAUDE_CODE_EXECUTABLE")) return;
1022
- return resolveWindowsExecutablePath("claude", env);
1262
+ return resolveWindowsExecutablePath("claude", env, cwd);
1023
1263
  }
1024
1264
  //#endregion
1025
1265
  //#region src/acp/auth-env.ts
@@ -1066,7 +1306,7 @@ function promotePrefixedAuthEnvironment(env) {
1066
1306
  if (!normalized) continue;
1067
1307
  protectEnvKey(protectedKeys, key);
1068
1308
  protectEnvKey(protectedKeys, normalized);
1069
- if (env[normalized] == null) env[normalized] = value;
1309
+ assignIfMissing(env, normalized, value);
1070
1310
  }
1071
1311
  return protectedKeys;
1072
1312
  }
@@ -1103,7 +1343,7 @@ function addAuthCredentialEnvKeys(protectedKeys, methodId, credential) {
1103
1343
  }
1104
1344
  function assignAuthCredentialEnv(env, methodId, credential) {
1105
1345
  if (typeof credential !== "string" || credential.trim().length === 0) return;
1106
- if (!methodId.includes("=") && !methodId.includes("\0") && env[methodId] == null) env[methodId] = credential;
1346
+ if (!methodId.includes("=") && !methodId.includes("\0")) assignIfMissing(env, methodId, credential);
1107
1347
  const normalized = toEnvToken(methodId);
1108
1348
  if (normalized) {
1109
1349
  assignIfMissing(env, `${AUTH_ENV_PREFIX}${normalized}`, credential);
@@ -1111,7 +1351,10 @@ function assignAuthCredentialEnv(env, methodId, credential) {
1111
1351
  }
1112
1352
  }
1113
1353
  function assignIfMissing(env, key, value) {
1114
- if (env[key] == null) env[key] = value;
1354
+ if (env[key] != null) return;
1355
+ const normalizedKey = protectedEnvKey(key);
1356
+ if (process.platform === "win32" && Object.entries(env).some(([existingKey, existingValue]) => existingValue != null && protectedEnvKey(existingKey) === normalizedKey)) return;
1357
+ env[key] = value;
1115
1358
  }
1116
1359
  function resolveConfiguredAuthCredential(methodId, authCredentials) {
1117
1360
  const configCredentials = authCredentials ?? {};
@@ -1622,7 +1865,7 @@ function buildTerminalSpawnOptions(command, cwd, env, platform = process.platfor
1622
1865
  "pipe"
1623
1866
  ],
1624
1867
  windowsHide: true
1625
- }, platform, resolvedEnv ?? process.env);
1868
+ }, platform, resolvedEnv ?? process.env, cwd);
1626
1869
  }
1627
1870
  function readTerminalOutputCeiling() {
1628
1871
  const raw = process.env.ACPX_TERMINAL_MAX_OUTPUT_BYTES?.trim();
@@ -1673,6 +1916,7 @@ var TerminalManager = class {
1673
1916
  processHelperTimeoutMs;
1674
1917
  outputCeilingBytes;
1675
1918
  terminals = /* @__PURE__ */ new Map();
1919
+ creations = /* @__PURE__ */ new Map();
1676
1920
  constructor(options) {
1677
1921
  this.outputCeilingBytes = readTerminalOutputCeiling();
1678
1922
  this.cwd = options.cwd;
@@ -1689,7 +1933,15 @@ var TerminalManager = class {
1689
1933
  this.permissionMode = permissionMode;
1690
1934
  this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
1691
1935
  }
1692
- async createTerminal(params, authority) {
1936
+ createTerminal(params, authority) {
1937
+ const creation = {};
1938
+ const pending = Promise.resolve().then(() => this.create(params, authority, creation));
1939
+ this.creations.set(pending, creation);
1940
+ const remove = () => this.creations.delete(pending);
1941
+ pending.then(remove, remove);
1942
+ return pending;
1943
+ }
1944
+ async create(params, authority, creation) {
1693
1945
  assertControlAuthority(authority);
1694
1946
  const commandLine = toCommandLine(params.command, params.args);
1695
1947
  const summary = `terminal/create: ${commandLine}`;
@@ -1753,10 +2005,15 @@ var TerminalManager = class {
1753
2005
  await terminal.descendants?.capture(terminal.processHelperTimeoutMs);
1754
2006
  assertControlAuthority(authority);
1755
2007
  } catch (error) {
1756
- await this.releaseTerminal({
1757
- terminalId,
1758
- sessionId: params.sessionId
1759
- });
2008
+ try {
2009
+ await this.releaseTerminal({
2010
+ terminalId,
2011
+ sessionId: params.sessionId
2012
+ });
2013
+ } catch (cleanupError) {
2014
+ creation.cleanupFailure = { error: cleanupError };
2015
+ throw cleanupError;
2016
+ }
1760
2017
  throw error;
1761
2018
  }
1762
2019
  this.emitOperation({
@@ -1889,11 +2146,13 @@ var TerminalManager = class {
1889
2146
  }
1890
2147
  }
1891
2148
  async shutdown() {
2149
+ const creations = [...this.creations];
2150
+ await Promise.allSettled(creations.map(([pending]) => pending));
1892
2151
  const results = await Promise.allSettled(Array.from(this.terminals.keys(), (terminalId) => this.releaseTerminal({
1893
2152
  terminalId,
1894
2153
  sessionId: "shutdown"
1895
2154
  })));
1896
- const failures = [];
2155
+ const failures = creations.flatMap(([, creation]) => creation.cleanupFailure ? [creation.cleanupFailure.error] : []);
1897
2156
  for (const result of results) if (result.status === "rejected") failures.push(result.reason);
1898
2157
  if (failures.length > 0) throw new AggregateError(failures, "Terminal shutdown failed", { cause: failures[0] });
1899
2158
  }
@@ -2337,8 +2596,9 @@ var AcpClient = class {
2337
2596
  }
2338
2597
  fallback?.abort();
2339
2598
  }
2340
- async start(authority) {
2599
+ async start(authority, options) {
2341
2600
  assertControlAuthority(authority);
2601
+ if (options) this.options.sessionOptions = structuredClone(options.sessionOptions);
2342
2602
  if (this.hasLiveConnection()) return;
2343
2603
  const epoch = await this.waitForPreviousClose();
2344
2604
  const maxMessageBytes = readMaxAcpMessageBytes();
@@ -2457,14 +2717,14 @@ var AcpClient = class {
2457
2717
  async ensureLaunchSupport(plan) {
2458
2718
  if (plan.copilotAcp) await ensureCopilotAcpSupport(plan.spawnCommand, plan.commandContext);
2459
2719
  if (!plan.claudeAcp) return;
2460
- const claudeExe = resolveClaudeCodeExecutable(process.platform, plan.spawnOptions.env);
2720
+ const claudeExe = resolveClaudeCodeExecutable(process.platform, plan.spawnOptions.env, plan.spawnOptions.cwd);
2461
2721
  if (claudeExe) {
2462
2722
  plan.spawnOptions.env.CLAUDE_CODE_EXECUTABLE = claudeExe;
2463
2723
  this.log(`resolved system Claude Code executable: ${claudeExe}`);
2464
2724
  }
2465
2725
  }
2466
2726
  prepareProcessLaunch(command, args, options) {
2467
- const resolved = buildAgentSpawnCommand(command, args, process.platform, options.env);
2727
+ const resolved = buildAgentSpawnCommand(command, args, process.platform, options.env, options.cwd);
2468
2728
  return {
2469
2729
  launch: Object.freeze({
2470
2730
  launchId: randomUUID(),
@@ -2674,6 +2934,10 @@ var AcpClient = class {
2674
2934
  onPromptRequestWritten: (active, owner) => this.onPromptRequestWritten(active, owner)
2675
2935
  });
2676
2936
  }
2937
+ sessionMeta() {
2938
+ const { command, args } = resolveAgentCommandParts(this.options.agentCommand, this.options.agentArgv);
2939
+ return buildClaudeCodeOptionsMeta(this.options.sessionOptions, isClaudeAcpCommand(command, args));
2940
+ }
2677
2941
  async createSession(cwd = this.options.cwd, authority) {
2678
2942
  assertControlAuthority(authority);
2679
2943
  const connection = this.getConnection();
@@ -2685,7 +2949,7 @@ var AcpClient = class {
2685
2949
  const createPromise = this.runConnectionRequest(() => connection.agent.request(methods.agent.session.new, {
2686
2950
  cwd: sessionCwd,
2687
2951
  mcpServers: this.options.mcpServers ?? [],
2688
- _meta: buildClaudeCodeOptionsMeta(this.options.sessionOptions, claudeAcp)
2952
+ _meta: this.sessionMeta()
2689
2953
  }), authority);
2690
2954
  result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise;
2691
2955
  } catch (error) {
@@ -2725,7 +2989,8 @@ var AcpClient = class {
2725
2989
  response = await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.load, {
2726
2990
  sessionId,
2727
2991
  cwd: sessionCwd,
2728
- mcpServers: this.options.mcpServers ?? []
2992
+ mcpServers: this.options.mcpServers ?? [],
2993
+ _meta: this.sessionMeta()
2729
2994
  }), options.authority);
2730
2995
  await this.waitForSessionUpdateDrain(options.replayIdleMs ?? REPLAY_IDLE_MS, options.replayDrainTimeoutMs ?? REPLAY_DRAIN_TIMEOUT_MS);
2731
2996
  } finally {
@@ -2743,7 +3008,8 @@ var AcpClient = class {
2743
3008
  const response = await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.resume, {
2744
3009
  sessionId,
2745
3010
  cwd: sessionCwd,
2746
- mcpServers: this.options.mcpServers ?? []
3011
+ mcpServers: this.options.mcpServers ?? [],
3012
+ _meta: this.sessionMeta()
2747
3013
  }), authority);
2748
3014
  this.loadedSessionId = sessionId;
2749
3015
  const result = toReconnectedSessionResult(response);
@@ -3731,6 +3997,7 @@ function contentToUserContent(content) {
3731
3997
  if (content.type === "resource") return resourceToUserContent(content);
3732
3998
  if (content.type === "image") return { Image: {
3733
3999
  source: content.data,
4000
+ mime_type: content.mimeType,
3734
4001
  size: null
3735
4002
  } };
3736
4003
  if (content.type === "audio") return { Audio: {
@@ -3799,7 +4066,10 @@ function appendAgentThinking(agent, text) {
3799
4066
  }
3800
4067
  function trimRuntimeText(value, maxChars) {
3801
4068
  if (value.length <= maxChars) return value;
3802
- return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
4069
+ let end = Math.max(0, maxChars - 3);
4070
+ const last = value.charCodeAt(end - 1);
4071
+ if (last >= 55296 && last <= 56319) end -= 1;
4072
+ return `${value.slice(0, end)}...`;
3803
4073
  }
3804
4074
  function statusIndicatesComplete(status) {
3805
4075
  if (typeof status !== "string") return false;
@@ -4346,13 +4616,6 @@ function clearDesiredConfigOption(state, configId) {
4346
4616
  function getDesiredModelId(state) {
4347
4617
  return normalizeModelId(state?.session_options?.model);
4348
4618
  }
4349
- function setCurrentModelId(record, modelId) {
4350
- const acpx = ensureAcpxState(record.acpx);
4351
- const normalized = normalizeModelId(modelId);
4352
- if (normalized) acpx.current_model_id = normalized;
4353
- else delete acpx.current_model_id;
4354
- record.acpx = acpx;
4355
- }
4356
4619
  function syncAdvertisedModelState(record, models) {
4357
4620
  if (!models) return;
4358
4621
  const acpx = ensureAcpxState(record.acpx);
@@ -4375,9 +4638,13 @@ async function applyRequestedModelIfAdvertised(params) {
4375
4638
  });
4376
4639
  if (warning) params.onWarning?.(warning);
4377
4640
  if (!params.models) return { applied: false };
4378
- if (params.models.currentModelId === requestedModel) return { applied: true };
4641
+ if (params.models.currentModelId === requestedModel) return {
4642
+ applied: true,
4643
+ modelId: requestedModel
4644
+ };
4379
4645
  return {
4380
4646
  applied: true,
4647
+ modelId: requestedModel,
4381
4648
  response: await withTimeout(params.client.setSessionModel(params.sessionId, requestedModel, params.models, params.authority), params.timeoutMs)
4382
4649
  };
4383
4650
  }
@@ -4393,25 +4660,35 @@ function applyConfigOptionsToRecord(record, result) {
4393
4660
  if (!configOptions) return;
4394
4661
  record.acpx = applyConfigOptionsToState(record.acpx, configOptions);
4395
4662
  }
4396
- function applyInitialModelSelection(record, originalModels, requestedModel, modelApplication) {
4663
+ function applyInitialModelSelection(record, originalModels, modelApplication) {
4397
4664
  applyConfigOptionsToRecord(record, modelApplication.response);
4398
- syncAdvertisedModelState(record, modelApplication.response ? modelStateFromConfigOptions(modelApplication.response.configOptions) : originalModels);
4399
- if (modelApplication.applied) setCurrentModelId(record, currentModelIdFromSetModelResponse(modelApplication.response, requestedModel));
4665
+ syncAdvertisedModelState(record, modelApplication.response?.configOptions !== void 0 ? modelStateFromConfigOptions(modelApplication.response.configOptions) : originalModels);
4666
+ if (modelApplication.applied) record.acpx = applyModelSelection(record.acpx, modelApplication.modelId, modelApplication.response);
4400
4667
  }
4401
- function applyAcceptedConfigOptions(state, response) {
4668
+ function applyAcceptedConfigOptions(state, response, selection) {
4402
4669
  const next = cloneSessionAcpxState(state) ?? {};
4403
- if (!response) return next;
4670
+ if (response?.configOptions === void 0) {
4671
+ const option = next.config_options?.find((entry) => entry.id === selection.configId);
4672
+ if (option) option.currentValue = selection.value;
4673
+ return next;
4674
+ }
4404
4675
  applyConfigOptionsModelState(next, response.configOptions);
4405
- if (!next.desired_config_options) return next;
4406
- const desiredEntries = [];
4407
- for (const option of response.configOptions) if (typeof option.currentValue === "string" && Object.hasOwn(next.desired_config_options, option.id)) desiredEntries.push([option.id, option.currentValue]);
4408
- if (desiredEntries.length > 0) next.desired_config_options = Object.fromEntries(desiredEntries);
4409
- else delete next.desired_config_options;
4676
+ reconcileDesiredConfigOptions(next, response.configOptions);
4410
4677
  return next;
4411
4678
  }
4679
+ function reconcileDesiredConfigOptions(state, configOptions) {
4680
+ if (!state.desired_config_options) return;
4681
+ const desiredEntries = [];
4682
+ for (const option of configOptions) if (typeof option.currentValue === "string" && Object.hasOwn(state.desired_config_options, option.id)) desiredEntries.push([option.id, option.currentValue]);
4683
+ if (desiredEntries.length > 0) state.desired_config_options = Object.fromEntries(desiredEntries);
4684
+ else delete state.desired_config_options;
4685
+ }
4412
4686
  function applyModelSelection(state, modelId, response) {
4413
4687
  const modelConfigId = advertisedModelState(state)?.configId;
4414
- const next = applyAcceptedConfigOptions(state, response);
4688
+ const next = applyAcceptedConfigOptions(state, response, {
4689
+ configId: modelConfigId,
4690
+ value: modelId
4691
+ });
4415
4692
  next.session_options = {
4416
4693
  ...next.session_options,
4417
4694
  model: modelId
@@ -4427,235 +4704,10 @@ function applyConfigOptionSelection(state, configId, value, response, modelConfi
4427
4704
  ...next.desired_config_options,
4428
4705
  [configId]: value
4429
4706
  };
4430
- return applyAcceptedConfigOptions(next, response);
4431
- }
4432
- //#endregion
4433
- //#region src/session/turn-ownership.ts
4434
- const LOCK_RETRY_MS = 15;
4435
- const INCOMPLETE_RESERVATION_GRACE_MS = 15e3;
4436
- let lastCreatedAt = 0;
4437
- const localTurns = /* @__PURE__ */ new Map();
4438
- async function readLock(filePath) {
4439
- try {
4440
- const stat = await fs$1.lstat(filePath, { bigint: true });
4441
- if (!stat.isFile()) throw new Error(`Session turn lock is not a regular file: ${filePath}`);
4442
- return {
4443
- stat,
4444
- payload: await fs$1.readFile(filePath, "utf8")
4445
- };
4446
- } catch (error) {
4447
- if (error.code === "ENOENT") return;
4448
- throw error;
4449
- }
4450
- }
4451
- function parseLock(payload) {
4452
- try {
4453
- return JSON.parse(payload);
4454
- } catch {
4455
- return;
4456
- }
4457
- }
4458
- async function tryAcquireGuard(filePath, owner, signal) {
4459
- try {
4460
- return await acquireFileLock(filePath, {
4461
- managerKey: "acpx.session-turn",
4462
- lockPath: `${filePath}.guard`,
4463
- staleMs: Infinity,
4464
- timeoutMs: LOCK_RETRY_MS,
4465
- retry: {
4466
- retries: 8,
4467
- minTimeout: 1,
4468
- maxTimeout: 2,
4469
- factor: 1,
4470
- randomize: false
4471
- },
4472
- staleRecovery: "remove-if-unchanged",
4473
- payload: () => owner.payload,
4474
- shouldReclaim: ({ payload }) => owner.hasExited(payload, false, signal),
4475
- shouldRemoveStaleLock: ({ payload }) => owner.hasExited(payload, true, signal)
4476
- });
4477
- } catch (error) {
4478
- if (error.code === "file_lock_timeout") return;
4479
- throw error;
4480
- }
4481
- }
4482
- async function removeObservedLock(filePath, observed) {
4483
- const current = await readLock(filePath);
4484
- if (!current) return true;
4485
- if (current.payload !== observed.payload || current.stat.mtimeNs !== observed.stat.mtimeNs || current.stat.size !== observed.stat.size) return false;
4486
- await fs$1.unlink(filePath).catch((error) => {
4487
- if (error.code !== "ENOENT") throw error;
4488
- });
4489
- return true;
4490
- }
4491
- async function recoverAbandonedLock(filePath, owner, signal) {
4492
- const observed = await readLock(filePath);
4493
- if (!observed) return true;
4494
- const payload = parseLock(observed.payload);
4495
- const pid = lockOwnerPid(payload);
4496
- if (pid && (!await owner.hasExited(payload, false, signal) || !await owner.hasExited(payload, true, signal))) return false;
4497
- if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return false;
4498
- if (await removeObservedLock(filePath, observed)) {
4499
- incrementPerfCounter("session.events.stale_lock_recovered");
4500
- return true;
4501
- }
4502
- return false;
4503
- }
4504
- async function publishLock(filePath, payload, signal) {
4505
- await withTempFile({
4506
- rootDir: path.dirname(filePath),
4507
- prefix: "session-turn",
4508
- fileName: "lock"
4509
- }, async (temporaryPath) => {
4510
- await fs$1.writeFile(temporaryPath, payload, {
4511
- encoding: "utf8",
4512
- flag: "wx",
4513
- mode: 384
4514
- });
4515
- signal?.throwIfAborted();
4516
- try {
4517
- await fs$1.link(temporaryPath, filePath);
4518
- } catch (error) {
4519
- if (!isHardlinkFallbackError(error)) throw error;
4520
- await fs$1.writeFile(filePath, payload, {
4521
- encoding: "utf8",
4522
- flag: "wx",
4523
- mode: 384
4524
- });
4525
- }
4526
- });
4527
- }
4528
- async function removePublishedLock(filePath, payload) {
4529
- const current = await readLock(filePath);
4530
- if (current?.payload === payload) await removeObservedLock(filePath, current);
4531
- }
4532
- async function rejectAcquisition(filePath, receipt, error) {
4533
- const failures = [error];
4534
- await disposeTurn(filePath, receipt).catch((cleanupError) => {
4535
- failures.push(...cleanupError instanceof AggregateError ? cleanupError.errors : [cleanupError]);
4536
- });
4537
- if (failures.length > 1) throw new AggregateError(failures, "Session turn acquisition and cleanup failed");
4538
- throw error;
4539
- }
4540
- async function releaseTurnGuard(receipt) {
4541
- await receipt.guard?.release();
4542
- receipt.guard = void 0;
4543
- }
4544
- async function removeTurnMarker(filePath, receipt) {
4545
- if (!receipt.removeMarker) return true;
4546
- if (receipt.guard && !await receipt.guard.verifyStillHeld()) await releaseTurnGuard(receipt);
4547
- receipt.guard ??= await tryAcquireGuard(filePath, receipt.owner);
4548
- if (!receipt.guard) return false;
4549
- await receipt.removeMarker();
4550
- receipt.removeMarker = void 0;
4551
- return true;
4552
- }
4553
- async function settleTurn(filePath, receipt) {
4554
- if (receipt.settled) return true;
4555
- const pending = receipt.pending ??= (async () => {
4556
- receipt.failed = false;
4557
- const failures = [];
4558
- try {
4559
- if (!await removeTurnMarker(filePath, receipt)) return false;
4560
- } catch (error) {
4561
- failures.push(error);
4562
- }
4563
- if (failures.length === 0 || !receipt.admitted) try {
4564
- await releaseTurnGuard(receipt);
4565
- } catch (error) {
4566
- failures.push(error);
4567
- }
4568
- if (failures.length > 1) throw new AggregateError(failures, "Session turn cleanup failed");
4569
- if (failures.length === 1) throw failures[0];
4570
- receipt.settled = true;
4571
- localTurns.delete(filePath);
4572
- return true;
4573
- })();
4574
- try {
4575
- return await pending;
4576
- } finally {
4577
- if (receipt.pending === pending) {
4578
- receipt.pending = void 0;
4579
- receipt.failed = !receipt.settled;
4580
- }
4581
- }
4582
- }
4583
- async function disposeTurn(filePath, receipt) {
4584
- if (!await settleTurn(filePath, receipt)) throw new Error(`Session turn cleanup is waiting for its guard: ${filePath}`);
4585
- }
4586
- async function tryPublishLock(filePath, payload, owner, signal) {
4587
- for (;;) try {
4588
- await publishLock(filePath, payload, signal);
4589
- return true;
4590
- } catch (error) {
4591
- if (error.code !== "EEXIST") throw error;
4592
- if (!await recoverAbandonedLock(filePath, owner, signal)) return false;
4593
- }
4594
- }
4595
- async function tryAcquireTurnCleanup(filePath, payload, owner, signal) {
4596
- const previous = localTurns.get(filePath);
4597
- if (previous) {
4598
- if (previous.failed) await settleTurn(filePath, previous);
4599
- return;
4600
- }
4601
- const receipt = {
4602
- owner,
4603
- removeMarker: async () => await removePublishedLock(filePath, payload),
4604
- admitted: false,
4605
- failed: false,
4606
- settled: false
4607
- };
4608
- localTurns.set(filePath, receipt);
4609
- try {
4610
- receipt.guard = await tryAcquireGuard(filePath, owner, signal);
4611
- } catch (error) {
4612
- localTurns.delete(filePath);
4613
- throw error;
4614
- }
4615
- if (!receipt.guard) {
4616
- localTurns.delete(filePath);
4617
- return;
4618
- }
4619
- return receipt;
4620
- }
4621
- async function tryAcquireTurn(filePath, payload, owner, signal) {
4622
- const receipt = await tryAcquireTurnCleanup(filePath, payload, owner, signal);
4623
- if (!receipt) return;
4624
- try {
4625
- signal?.throwIfAborted();
4626
- if (!await tryPublishLock(filePath, payload, owner, signal)) {
4627
- await disposeTurn(filePath, receipt);
4628
- return;
4629
- }
4630
- const observed = await readLock(filePath);
4631
- if (observed?.payload !== payload) throw new Error(`Session turn ownership changed before admission: ${filePath}`);
4632
- signal?.throwIfAborted();
4633
- receipt.admitted = true;
4634
- receipt.removeMarker = async () => await removeObservedLock(filePath, observed);
4635
- return { [Symbol.asyncDispose]: async () => await disposeTurn(filePath, receipt) };
4636
- } catch (error) {
4637
- return await rejectAcquisition(filePath, receipt, error);
4638
- }
4639
- }
4640
- async function acquireSessionTurn(recordId, signal) {
4641
- const requestedPath = sessionEventLockPath(recordId);
4642
- await fs$1.mkdir(path.dirname(requestedPath), {
4643
- recursive: true,
4644
- mode: 448
4707
+ return applyAcceptedConfigOptions(next, response, {
4708
+ configId,
4709
+ value
4645
4710
  });
4646
- const filePath = path.join(await fs$1.realpath(path.dirname(requestedPath)), path.basename(requestedPath));
4647
- const owner = await createLockOwner({ signal });
4648
- lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);
4649
- const payload = `${JSON.stringify({
4650
- ...owner.payload,
4651
- created_at: new Date(lastCreatedAt).toISOString()
4652
- }, null, 2)}\n`;
4653
- for (;;) {
4654
- signal?.throwIfAborted();
4655
- const turn = await tryAcquireTurn(filePath, payload, owner, signal);
4656
- if (turn) return turn;
4657
- await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4658
- }
4659
4711
  }
4660
4712
  //#endregion
4661
4713
  //#region src/session/execution/direct-lifetime.ts
@@ -4757,10 +4809,7 @@ async function replayDesiredModel(params) {
4757
4809
  const response = await withTimeout(params.client.setSessionModel(params.sessionId, params.desiredModelId, params.models, params.replay.authority), params.timeoutMs);
4758
4810
  params.replay.acknowledged = true;
4759
4811
  params.record.acpx = applyModelSelection(params.record.acpx, params.desiredModelId, response);
4760
- const models = response ? modelStateFromConfigOptions(response.configOptions) : {
4761
- ...params.models,
4762
- currentModelId: params.desiredModelId
4763
- };
4812
+ const models = advertisedModelState(params.record.acpx);
4764
4813
  if (params.verbose) process.stderr.write(`[acpx] replayed desired model ${params.desiredModelId} on ACP session ${params.sessionId} (previous ${params.previousSessionId})\n`);
4765
4814
  return {
4766
4815
  replayed: true,
@@ -4788,10 +4837,10 @@ async function replayDesiredConfigOptions(params) {
4788
4837
  const response = await withTimeout(params.client.setSessionConfigOption(params.sessionId, configId, value, advertisedModelState(params.record.acpx), params.replay.authority), params.timeoutMs);
4789
4838
  params.replay.acknowledged = true;
4790
4839
  params.record.acpx = applyConfigOptionSelection(params.record.acpx, configId, value, response);
4791
- acceptedConfigOptions = response.configOptions;
4840
+ acceptedConfigOptions = params.record.acpx.config_options;
4792
4841
  result = {
4793
4842
  replayed: true,
4794
- models: modelStateFromConfigOptions(response.configOptions)
4843
+ models: advertisedModelState(params.record.acpx)
4795
4844
  };
4796
4845
  if (params.verbose) process.stderr.write(`[acpx] replayed desired config option ${configId} on ACP session ${params.sessionId} (previous ${params.previousSessionId})\n`);
4797
4846
  } catch (error) {
@@ -4823,7 +4872,7 @@ async function connectAndLoadSession(options) {
4823
4872
  logReconnectAttempt(record, storedProcessAlive, Boolean(record.pid) && !storedProcessAlive, options.verbose);
4824
4873
  const reusingLoadedSession = client.hasReusableSession(record.acpSessionId);
4825
4874
  if (reusingLoadedSession) incrementPerfCounter("runtime.connect_and_load.reused_session");
4826
- else await withTimeout(client.start(options.authority), options.timeoutMs);
4875
+ else await withTimeout(client.start(options.authority, { sessionOptions: mergeSessionOptions(options.sessionOptions, sessionOptionsFromRecord(record)) }), options.timeoutMs);
4827
4876
  assertControlAuthority(options.authority);
4828
4877
  options.onClientAvailable?.(options.activeController);
4829
4878
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
@@ -5577,7 +5626,7 @@ async function createSessionRecordWithClient(client, options) {
5577
5626
  };
5578
5627
  persistSessionOptions(record, options.sessionOptions);
5579
5628
  applyConfigOptionsToRecord(record, createdState.sessionResult);
5580
- applyInitialModelSelection(record, createdState.sessionResult.models, options.sessionOptions?.model, createdState.modelApplication);
5629
+ applyInitialModelSelection(record, createdState.sessionResult.models, createdState.modelApplication);
5581
5630
  await writeSessionRecord(record);
5582
5631
  assertControlAuthority({ signal: options.signal });
5583
5632
  return record;
@@ -5632,6 +5681,14 @@ async function resumeSessionRecordWithClient(client, options, cwd) {
5632
5681
  }
5633
5682
  async function createSessionWithClient(options) {
5634
5683
  assertControlAuthority({ signal: options.signal });
5684
+ if (options.resumeSessionId) {
5685
+ const resumedRecord = await readSessionRecord(options.resumeSessionId);
5686
+ assertControlAuthority({ signal: options.signal });
5687
+ if (resumedRecord && !resumedRecord.closed && resumedRecord.agentCommand === options.agentCommand) {
5688
+ await closeSession(resumedRecord.acpxRecordId);
5689
+ assertControlAuthority({ signal: options.signal });
5690
+ }
5691
+ }
5635
5692
  const client = new AcpClient({
5636
5693
  agentCommand: options.agentCommand,
5637
5694
  agentArgv: options.agentArgv,
@@ -5719,11 +5776,11 @@ async function listAgentSessions(options) {
5719
5776
  }
5720
5777
  async function ensureSession(options) {
5721
5778
  const cwd = absolutePath(options.cwd);
5722
- const ownership = await acquireSessionTurn(`ensure:${createHash("sha256").update(JSON.stringify([
5723
- options.agentCommand,
5779
+ const ownership = await acquireSessionScope({
5780
+ agentCommand: options.agentCommand,
5724
5781
  cwd,
5725
- normalizeName(options.name)
5726
- ])).digest("hex")}`, options.signal);
5782
+ name: normalizeName(options.name)
5783
+ }, options.signal);
5727
5784
  try {
5728
5785
  return await ensureSessionWithOwnership(options, cwd);
5729
5786
  } finally {
@@ -6503,7 +6560,12 @@ var LiveSessionCheckpoint = class {
6503
6560
  async flushDirty() {
6504
6561
  while (this.dirty) {
6505
6562
  this.dirty = false;
6506
- await this.save();
6563
+ try {
6564
+ await this.save();
6565
+ } catch (error) {
6566
+ this.dirty = true;
6567
+ throw error;
6568
+ }
6507
6569
  }
6508
6570
  }
6509
6571
  };
@@ -6557,8 +6619,8 @@ var QueueTaskOutputFormatter = class {
6557
6619
  flush() {}
6558
6620
  };
6559
6621
  function markOutputAlreadyEmitted(error, outputAlreadyEmitted) {
6560
- if (!outputAlreadyEmitted || !error || typeof error !== "object") return;
6561
- error.outputAlreadyEmitted = true;
6622
+ if (!error || typeof error !== "object") return;
6623
+ error.outputAlreadyEmitted = outputAlreadyEmitted;
6562
6624
  }
6563
6625
  async function runWithOptionalInterrupt(options) {
6564
6626
  if (options.handleProcessInterrupts === false) return await options.run();
@@ -6601,11 +6663,17 @@ var AcpErrorTracker = class {
6601
6663
  return this.latestInbound ?? this.outbound.findLast((acp) => outboundAcpErrorMatches(error, acp));
6602
6664
  }
6603
6665
  };
6604
- function toPromptResult(stopReason, sessionId, client, meta) {
6666
+ function toPromptResult(stopReason, sessionId, client, before, meta) {
6667
+ const after = client.getPermissionStats();
6605
6668
  return {
6606
6669
  stopReason,
6607
6670
  sessionId,
6608
- permissionStats: client.getPermissionStats(),
6671
+ permissionStats: {
6672
+ requested: after.requested - before.requested,
6673
+ approved: after.approved - before.approved,
6674
+ denied: after.denied - before.denied,
6675
+ cancelled: after.cancelled - before.cancelled
6676
+ },
6609
6677
  ...meta === void 0 ? {} : { _meta: meta }
6610
6678
  };
6611
6679
  }
@@ -6627,60 +6695,37 @@ async function applyPromptModelIfAdvertised(params) {
6627
6695
  });
6628
6696
  if (result.applied) params.record.acpx = applyModelSelection(params.record.acpx, requestedModel, result.response);
6629
6697
  }
6630
- function jsonRpcIdKey(value) {
6698
+ function jsonRpcIdKey(message) {
6699
+ if (!("id" in message)) return;
6700
+ const value = message.id;
6631
6701
  if (typeof value === "string") return `s:${value}`;
6632
6702
  if (typeof value === "number" && Number.isFinite(value)) return `n:${value}`;
6633
6703
  }
6634
- function extractJsonRpcRequestInfo(message) {
6635
- const candidate = message;
6636
- if (typeof candidate.method !== "string") return;
6637
- const idKey = jsonRpcIdKey(candidate.id);
6638
- if (!idKey) return;
6639
- return {
6640
- idKey,
6641
- method: candidate.method
6642
- };
6643
- }
6644
- function extractJsonRpcResponseInfo(message) {
6645
- const candidate = message;
6646
- const idKey = jsonRpcIdKey(candidate.id);
6647
- if (!idKey) return;
6648
- const hasError = Object.hasOwn(candidate, "error");
6649
- const hasResult = Object.hasOwn(candidate, "result");
6650
- if (!hasError && !hasResult) return;
6651
- return {
6652
- idKey,
6653
- hasError
6654
- };
6655
- }
6656
6704
  const SESSION_RECONNECT_METHODS = /* @__PURE__ */ new Set(["session/load", "session/resume"]);
6657
- function filterRecoverableLoadFallbackOutput(messages) {
6658
- const requestMethodById = /* @__PURE__ */ new Map();
6659
- const failedLoadRequestIds = /* @__PURE__ */ new Set();
6660
- for (const message of messages) {
6661
- const request = extractJsonRpcRequestInfo(message);
6662
- if (request) {
6663
- requestMethodById.set(request.idKey, request.method);
6664
- continue;
6665
- }
6666
- const response = extractJsonRpcResponseInfo(message);
6667
- if (!response || !response.hasError) continue;
6668
- const requestMethod = requestMethodById.get(response.idKey);
6669
- if (requestMethod && SESSION_RECONNECT_METHODS.has(requestMethod)) failedLoadRequestIds.add(response.idKey);
6670
- }
6671
- if (failedLoadRequestIds.size === 0) return messages;
6672
- return messages.filter((message) => {
6673
- const request = extractJsonRpcRequestInfo(message);
6674
- if (request && SESSION_RECONNECT_METHODS.has(request.method) && failedLoadRequestIds.has(request.idKey)) return false;
6675
- const response = extractJsonRpcResponseInfo(message);
6676
- if (response && failedLoadRequestIds.has(response.idKey)) return false;
6677
- return true;
6678
- });
6705
+ function isReconnectRequest(direction, message) {
6706
+ return direction === "outbound" && "method" in message && typeof message.method === "string" && SESSION_RECONNECT_METHODS.has(message.method);
6707
+ }
6708
+ function isInboundResponse(direction, message) {
6709
+ return direction === "inbound" && ("result" in message || "error" in message);
6679
6710
  }
6680
6711
  function filterBufferedConnectOutput(messages, loadError) {
6681
6712
  if (loadError == null) return messages;
6682
- const filteredMessages = new Set(filterRecoverableLoadFallbackOutput(messages.map(({ message }) => message)));
6683
- return messages.filter(({ message }) => filteredMessages.has(message));
6713
+ const pending = /* @__PURE__ */ new Map();
6714
+ const suppressed = /* @__PURE__ */ new Set();
6715
+ for (const [index, { direction, message }] of messages.entries()) {
6716
+ const id = jsonRpcIdKey(message);
6717
+ if (!id) continue;
6718
+ if (isReconnectRequest(direction, message)) pending.set(id, index);
6719
+ else if (isInboundResponse(direction, message)) {
6720
+ const requestIndex = pending.get(id);
6721
+ if (requestIndex !== void 0 && "error" in message) {
6722
+ suppressed.add(requestIndex);
6723
+ suppressed.add(index);
6724
+ }
6725
+ pending.delete(id);
6726
+ }
6727
+ }
6728
+ return messages.filter((_message, index) => !suppressed.has(index));
6684
6729
  }
6685
6730
  function emitPromptRetryNotice(params) {
6686
6731
  if (params.suppressSdkConsoleErrors) return;
@@ -6744,7 +6789,7 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
6744
6789
  suppressSdkConsoleErrors: task.suppressSdkConsoleErrors ?? options.suppressSdkConsoleErrors,
6745
6790
  verbose: options.verbose,
6746
6791
  promptRetries: task.promptRetries ?? options.promptRetries ?? 0,
6747
- sessionOptions: mergeSessionOptions(task.sessionOptions, options.sessionOptions),
6792
+ sessionOptions: task.sessionOptions,
6748
6793
  onClientAvailable: options.onClientAvailable,
6749
6794
  onClientClosed: options.onClientClosed,
6750
6795
  onClientCloseFailure: options.onClientCloseFailure,
@@ -7016,6 +7061,7 @@ async function runOwnedSessionPrompt(options) {
7016
7061
  verbose: options.verbose,
7017
7062
  sessionOptions
7018
7063
  });
7064
+ const permissionStatsBefore = client.getPermissionStats();
7019
7065
  client.updateRuntimeOptions({
7020
7066
  permissionMode: options.permissionMode,
7021
7067
  nonInteractivePermissions: options.nonInteractivePermissions,
@@ -7094,6 +7140,7 @@ async function runOwnedSessionPrompt(options) {
7094
7140
  client,
7095
7141
  record,
7096
7142
  resumePolicy: options.resumePolicy,
7143
+ sessionOptions: options.sessionOptions,
7097
7144
  replacingConfigOption: requestedModelId(options.sessionOptions?.model) ? { key: "model" } : void 0,
7098
7145
  timeoutMs: options.timeoutMs,
7099
7146
  verbose: options.verbose,
@@ -7171,7 +7218,7 @@ async function runOwnedSessionPrompt(options) {
7171
7218
  applyConversation(record, conversation);
7172
7219
  const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
7173
7220
  attachAcpErrorPayload(propagated, normalizedError.acp);
7174
- propagated.outputAlreadyEmitted = matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted;
7221
+ markOutputAlreadyEmitted(propagated, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
7175
7222
  propagated.normalizedOutputError = normalizedError;
7176
7223
  throw propagated;
7177
7224
  };
@@ -7224,7 +7271,7 @@ async function runOwnedSessionPrompt(options) {
7224
7271
  const response = await savePromptSuccess(await runPromptWithRetries(activeSessionId));
7225
7272
  promptTurnActive = false;
7226
7273
  return {
7227
- ...toPromptResult(response.stopReason, record.acpxRecordId, client, response._meta),
7274
+ ...toPromptResult(response.stopReason, record.acpxRecordId, client, permissionStatsBefore, response._meta),
7228
7275
  record,
7229
7276
  resumed,
7230
7277
  loadError
@@ -7344,6 +7391,7 @@ async function runOnce(options, control) {
7344
7391
  },
7345
7392
  sessionOptions: options.sessionOptions
7346
7393
  });
7394
+ const permissionStatsBefore = client.getPermissionStats();
7347
7395
  const closeOwnedClient = ownDirectClient(client, control?.signal);
7348
7396
  const runExecPromptAttempt = async (sessionId) => {
7349
7397
  assertControlAuthority(authority);
@@ -7389,17 +7437,17 @@ async function runOnce(options, control) {
7389
7437
  authority,
7390
7438
  onWarning: options.suppressSdkConsoleErrors ? void 0 : (message) => process.stderr.write(`[acpx] warning: ${message}\n`)
7391
7439
  });
7392
- if (modelApplication.response) controlState = applyConfigOptionsToState(controlState, modelApplication.response.configOptions);
7440
+ if (modelApplication.applied) controlState = applyModelSelection(controlState, modelApplication.modelId, modelApplication.response);
7393
7441
  for (const configOption of options.configOptions ?? []) {
7394
7442
  assertControlAuthority(authority);
7395
7443
  const response = await withTimeout(client.setSessionConfigOption(sessionId, configOption.configId, configOption.value, advertisedModelState(controlState), authority), options.timeoutMs);
7396
- controlState = applyConfigOptionsToState(controlState, response.configOptions);
7444
+ controlState = applyConfigOptionSelection(controlState, configOption.configId, configOption.value, response);
7397
7445
  }
7398
7446
  assertControlAuthority(authority);
7399
7447
  output.setContext({ sessionId });
7400
7448
  const response = await runExecPromptWithRetries(sessionId);
7401
7449
  promptTurnActive = false;
7402
- return toPromptResult(response.stopReason, sessionId, client, response._meta);
7450
+ return toPromptResult(response.stopReason, sessionId, client, permissionStatsBefore, response._meta);
7403
7451
  },
7404
7452
  handleInterrupt: async () => {
7405
7453
  await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
@@ -7626,31 +7674,50 @@ function createLeaseHeartbeat(lease, shutdown) {
7626
7674
  };
7627
7675
  }
7628
7676
  async function runSessionQueueOwner(options) {
7629
- const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
7630
- path: options.mcpConfigPath,
7631
- fingerprint: options.mcpConfigFingerprint
7632
- });
7633
- if (!lease) return;
7634
- let runtimeFailure;
7635
- try {
7636
- await runQueueOwnerRuntime(options, lease);
7637
- } catch (error) {
7638
- runtimeFailure = { error };
7639
- }
7677
+ const signals = [
7678
+ "SIGINT",
7679
+ "SIGTERM",
7680
+ "SIGHUP"
7681
+ ];
7682
+ let stopRequested = false;
7683
+ let requestRuntimeStop;
7684
+ const onSignal = () => {
7685
+ stopRequested = true;
7686
+ requestRuntimeStop?.();
7687
+ };
7640
7688
  try {
7641
- await releaseQueueOwnerLease(lease);
7642
- } catch (error) {
7643
- if (runtimeFailure) throw new AggregateError([runtimeFailure.error, error], "Queue owner shutdown failed", { cause: error });
7644
- throw error;
7689
+ for (const signal of signals) process.on(signal, onSignal);
7690
+ const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
7691
+ path: options.mcpConfigPath,
7692
+ fingerprint: options.mcpConfigFingerprint
7693
+ });
7694
+ if (!lease) return;
7695
+ let runtimeFailure;
7696
+ try {
7697
+ await runQueueOwnerRuntime(options, lease, (requestStop) => {
7698
+ requestRuntimeStop = requestStop;
7699
+ if (stopRequested) requestStop();
7700
+ });
7701
+ } catch (error) {
7702
+ runtimeFailure = { error };
7703
+ }
7704
+ try {
7705
+ await releaseQueueOwnerLease(lease);
7706
+ } catch (error) {
7707
+ if (runtimeFailure) throw new AggregateError([runtimeFailure.error, error], "Queue owner shutdown failed", { cause: error });
7708
+ throw error;
7709
+ }
7710
+ if (runtimeFailure) throw runtimeFailure.error;
7711
+ } finally {
7712
+ for (const signal of signals) process.off(signal, onSignal);
7645
7713
  }
7646
- if (runtimeFailure) throw runtimeFailure.error;
7647
7714
  }
7648
7715
  async function keepOwnerForPendingControls(controls, shutdown) {
7649
7716
  if (shutdown.requested || !controls.hasPending) return false;
7650
7717
  await controls.drainAdmitted();
7651
7718
  return !shutdown.requested;
7652
7719
  }
7653
- async function runQueueOwnerRuntime(options, lease) {
7720
+ async function runQueueOwnerRuntime(options, lease, bindStopRequest) {
7654
7721
  const sessionRecord = await resolveSessionRecord(options.sessionId);
7655
7722
  let owner;
7656
7723
  let heartbeatTimer;
@@ -7724,13 +7791,8 @@ async function runQueueOwnerRuntime(options, lease) {
7724
7791
  sessionId: options.sessionId,
7725
7792
  verbose: options.verbose
7726
7793
  });
7727
- const onSignal = () => {
7728
- shutdown.request();
7729
- };
7794
+ bindStopRequest(shutdown.request);
7730
7795
  const heartbeat = createLeaseHeartbeat(lease, shutdown);
7731
- process.once("SIGINT", onSignal);
7732
- process.once("SIGTERM", onSignal);
7733
- process.once("SIGHUP", onSignal);
7734
7796
  try {
7735
7797
  owner = await SessionQueueOwner.start(lease, {
7736
7798
  cancelPrompt: async () => {
@@ -7812,9 +7874,6 @@ async function runQueueOwnerRuntime(options, lease) {
7812
7874
  }
7813
7875
  }
7814
7876
  } finally {
7815
- process.off("SIGINT", onSignal);
7816
- process.off("SIGTERM", onSignal);
7817
- process.off("SIGHUP", onSignal);
7818
7877
  await shutdown.shutdown();
7819
7878
  await lease.updates;
7820
7879
  }
@@ -7858,6 +7917,6 @@ async function sendSession(options) {
7858
7917
  }
7859
7918
  const queueOwnerRuntimeTestInternals = { queueOwnerExitIsFatal };
7860
7919
  //#endregion
7861
- export { isRequestedModelUnsupportedError as $, applyInitialModelSelection as A, reconcileAgentSessionId as B, setSessionModeOnOwner as C, connectAndLoadSession as D, withConnectedSession as E, persistSessionOptions as F, recordPromptSubmission as G, cloneSessionConversation as H, sessionOptionsFromRecord as I, advertisedModelState as J, recordSessionUpdate as K, applyConversation as L, applyRequestedModelIfAdvertised as M, setDesiredModeId as N, applyConfigOptionSelection as O, normalizeSessionOptionShape as P, RequestedModelUnsupportedError as Q, applyLifecycleSnapshotToRecord as R, setSessionMode as S, setSessionModelOnOwner as T, createSessionConversation as U, cloneSessionAcpxState as V, recordClientOperation as W, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE as X, AcpClient as Y, REQUESTED_MODEL_UNSUPPORTED_REASONS as Z, cancelSessionPrompt as _, runQueuedTask as a, isLegacyZedCodexAcpInvocation as at, setSessionConfigOption as b, LiveSessionCheckpoint as c, assertControlAuthority as ct, flushPerfMetricsCapture as d, getAcpxVersion as et, installPerfMetricsCapture as f, listAgentSessions as g, ensureSession as h, runOnce as i, listSessionEvents as it, applyModelSelection as j, applyConfigOptionsToRecord as k, runPromptTurn as l, withInterrupt as lt, createSessionWithClient as m, runSessionQueueOwner as n, DEFAULT_QUEUE_OWNER_TTL_MS as nt, sendSessionDirect as o, InterruptedError as ot, createSession as p, trimConversationForRuntime as q, sendSession as r, normalizeQueueOwnerTtlMs as rt, DISCARD_OUTPUT_FORMATTER as s, TimeoutError as st, queueOwnerRuntimeTestInternals as t, permissionModeSatisfies as tt, buildQueueOwnerArgOverride as u, withTimeout as ut, closeSession as v, setSessionModel as w, setSessionConfigOptionOnOwner as x, sessionControlTestInternals as y, createInitialSessionRecord as z };
7920
+ export { isRequestedModelUnsupportedError as $, applyInitialModelSelection as A, reconcileAgentSessionId as B, setSessionModeOnOwner as C, connectAndLoadSession as D, withConnectedSession as E, persistSessionOptions as F, recordPromptSubmission as G, cloneSessionConversation as H, sessionOptionsFromRecord as I, advertisedModelState as J, recordSessionUpdate as K, applyConversation as L, applyRequestedModelIfAdvertised as M, setDesiredModeId as N, applyConfigOptionSelection as O, normalizeSessionOptionShape as P, RequestedModelUnsupportedError as Q, applyLifecycleSnapshotToRecord as R, setSessionMode as S, setSessionModelOnOwner as T, createSessionConversation as U, cloneSessionAcpxState as V, recordClientOperation as W, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE as X, AcpClient as Y, REQUESTED_MODEL_UNSUPPORTED_REASONS as Z, cancelSessionPrompt as _, runQueuedTask as a, acquireSessionScope as at, setSessionConfigOption as b, LiveSessionCheckpoint as c, InterruptedError as ct, flushPerfMetricsCapture as d, withInterrupt as dt, getAcpxVersion as et, installPerfMetricsCapture as f, withTimeout as ft, listAgentSessions as g, ensureSession as h, runOnce as i, acquireSessionImport as it, applyModelSelection as j, applyConfigOptionsToRecord as k, runPromptTurn as l, TimeoutError as lt, createSessionWithClient as m, runSessionQueueOwner as n, DEFAULT_QUEUE_OWNER_TTL_MS as nt, sendSessionDirect as o, listSessionEvents as ot, createSession as p, trimConversationForRuntime as q, sendSession as r, normalizeQueueOwnerTtlMs as rt, DISCARD_OUTPUT_FORMATTER as s, isLegacyZedCodexAcpInvocation as st, queueOwnerRuntimeTestInternals as t, permissionModeSatisfies as tt, buildQueueOwnerArgOverride as u, assertControlAuthority as ut, closeSession as v, setSessionModel as w, setSessionConfigOptionOnOwner as x, sessionControlTestInternals as y, createInitialSessionRecord as z };
7862
7921
 
7863
- //# sourceMappingURL=queue-owner-runtime-W0lly6ap.js.map
7922
+ //# sourceMappingURL=queue-owner-runtime-Dhr4gxNE.js.map