acpx 0.17.1 → 0.19.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 (42) hide show
  1. package/dist/{agent-registry-evUoJINz.js → agent-registry-Ct2yWPW7.js} +18 -18
  2. package/dist/agent-registry-Ct2yWPW7.js.map +1 -0
  3. package/dist/agent-registry.d.ts.map +1 -1
  4. package/dist/agent-registry.js +1 -1
  5. package/dist/{cli-B2ycKzaO.js → cli-Bmki9cj5.js} +5 -5
  6. package/dist/{cli-B2ycKzaO.js.map → cli-Bmki9cj5.js.map} +1 -1
  7. package/dist/cli.js +167 -131
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{client-CkbBoSGd.d.ts → client-DzaUKazo.d.ts} +23 -15
  10. package/dist/client-DzaUKazo.d.ts.map +1 -0
  11. package/dist/{flows-xGyhmoHO.js → flows--Ewxn_fQ.js} +665 -460
  12. package/dist/flows--Ewxn_fQ.js.map +1 -0
  13. package/dist/flows.d.ts +19 -7
  14. package/dist/flows.d.ts.map +1 -1
  15. package/dist/flows.js +1 -1
  16. package/dist/{invocation-options-eZEBh4vd.js → invocation-options-DRzxHZmr.js} +34 -12
  17. package/dist/invocation-options-DRzxHZmr.js.map +1 -0
  18. package/dist/{ipc-D2rstexT.js → ipc-fMgj5vrO.js} +818 -302
  19. package/dist/ipc-fMgj5vrO.js.map +1 -0
  20. package/dist/{output-D2xL6wdp.js → output-D6kDs8BN.js} +30 -14
  21. package/dist/output-D6kDs8BN.js.map +1 -0
  22. package/dist/{queue-owner-runtime-Dgv3e2Tp.js → queue-owner-runtime-CLwjMYNL.js} +1448 -717
  23. package/dist/queue-owner-runtime-CLwjMYNL.js.map +1 -0
  24. package/dist/runtime.d.ts +57 -3
  25. package/dist/runtime.d.ts.map +1 -1
  26. package/dist/runtime.js +303 -166
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/session-options-Dp6mzNSy.d.ts.map +1 -1
  29. package/dist/{session-watch-CTe9Bnbc.js → session-watch-BfdIo-n4.js} +5 -5
  30. package/dist/session-watch-BfdIo-n4.js.map +1 -0
  31. package/dist/{watch-DxuXEc8n.js → watch-DH-zXQH2.js} +2 -2
  32. package/dist/{watch-DxuXEc8n.js.map → watch-DH-zXQH2.js.map} +1 -1
  33. package/package.json +1 -1
  34. package/skills/acpx/SKILL.md +69 -30
  35. package/dist/agent-registry-evUoJINz.js.map +0 -1
  36. package/dist/client-CkbBoSGd.d.ts.map +0 -1
  37. package/dist/flows-xGyhmoHO.js.map +0 -1
  38. package/dist/invocation-options-eZEBh4vd.js.map +0 -1
  39. package/dist/ipc-D2rstexT.js.map +0 -1
  40. package/dist/output-D2xL6wdp.js.map +0 -1
  41. package/dist/queue-owner-runtime-Dgv3e2Tp.js.map +0 -1
  42. package/dist/session-watch-CTe9Bnbc.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { $ as startPerfTimer, At as AgentSpawnError, B as extractAgentSessionId, Bt as SessionModeReplayError, C as textPrompt, Ct as OUTPUT_ERROR_ORIGINS, E as findGitRepositoryRoot, F as resolveSessionRecord, Ft as GeminiAcpStartupTimeoutError, H as SESSION_JOURNAL_SCHEMA, I as writeSessionRecord, It as PermissionDeniedError, J as incrementPerfCounter, K as formatPerfMetric, Lt as PermissionPromptUnavailableError, M as normalizeName, Mt as AuthPolicyError, Nt as ClaudeAcpSessionCreateTimeoutError, O as findSessionByDirectoryWalk, Ot as AcpxOperationalError, Pt as CopilotAcpUnsupportedError, Q as setPerfGauge, Rt as QueueConnectionError, S as promptToDisplayText, St as OUTPUT_ERROR_CODES, T as absolutePath, U as SessionJournalReader, Ut as SessionResumeRequiredError, V as normalizeAgentSessionId, Vt as SessionModelReplayError, Wt as UnsupportedPromptContentError, X as recordPerfDuration, Y as measurePerf, Z as resetPerfMetrics, _ as waitMs$1, _t as formatUnknownErrorMessage, a as trySetModelOnRunningOwner, bt as EXIT_CODES, c as queueRequestExceedsLimit, ct as defaultSessionEventLog, dt as sessionEventLockPath, f as refreshQueueOwnerLease, ft as sessionEventSegmentPath, g as tryAcquireQueueOwnerLease, gt as extractAcpError, h as terminateQueueOwnerForSession, ht as isProcessAlive, i as trySetModeOnRunningOwner, jt as AgentStartupError, k as isoNow$1, kt as AgentDisconnectedError, l as parseQueueRequest, lt as sessionBaseDir, m as terminateProcess, n as tryCloseSessionOnRunningOwner, nt as isAcpMessageObject, o as trySubmitToRunningOwner, p as releaseQueueOwnerLease, q as getPerfMetricsSnapshot, r as trySetConfigOptionOnRunningOwner, rt as isSessionUpdateNotification, s as queueRequestByteLimit, t as tryCancelOnRunningOwner, tt as isAcpJsonRpcMessage, ut as sessionEventActivePath, vt as isAcpResourceNotFoundError, y as getUnsupportedPromptContentMessage, zt as SessionConfigOptionReplayError } from "./ipc-D2rstexT.js";
2
- import { A as resolveAgentCommandParts, C as asAbsoluteCwd, E as isoNow$2, F as waitForSpawn, M as runTimedExecFile, N as splitCommandLine, P as waitForChildExit, S as PROCESS_HELPER_TIMEOUT_MS, T as isChildProcessRunning, _ as buildSpawnCommandOptions, b as readWindowsEnvValue, f as resolveBuiltInAgentLaunch, g as buildAgentSpawnCommand, j as resolveAgentSessionCwd, k as requireAgentStdio, v as buildTerminalShellSpawnCommand, w as basenameToken, x as resolveWindowsExecutablePath, y as buildTerminalSpawnCommand } from "./agent-registry-evUoJINz.js";
1
+ import { $ as measurePerf, A as findGitRepositoryRoot, B as writeSessionRecord, Bt as PermissionDeniedError, C as getUnsupportedPromptContentMessage, D as textPrompt, Dt as OUTPUT_ERROR_ORIGINS, E as promptToDisplayText, Et as OUTPUT_ERROR_CODES, Ft as AgentStartupError, G as normalizeAgentSessionId, Gt as SessionModelReplayError, Ht as QueueConnectionError, I as normalizeName, It as AuthPolicyError, Jt as UnsupportedPromptContentError, K as SESSION_JOURNAL_SCHEMA, Lt as ClaudeAcpSessionCreateTimeoutError, M as findSessionByDirectoryWalk, Mt as AcpxOperationalError, N as isoNow$1, Nt as AgentDisconnectedError, Pt as AgentSpawnError, Q as incrementPerfCounter, Rt as CopilotAcpUnsupportedError, St as isAcpResourceNotFoundError, Ut as SessionConfigOptionReplayError, Vt as PermissionPromptUnavailableError, W as extractAgentSessionId, Wt as SessionModeReplayError, X as formatPerfMetric, Z as getPerfMetricsSnapshot, _ as waitMs$1, a as trySetModelOnRunningOwner, at as isAcpJsonRpcMessage, b as lockOwnerPid, bt as extractAcpError, c as queueRequestExceedsLimit, et as recordPerfDuration, f as refreshQueueOwnerLease, ft as defaultSessionEventLog, g as tryAcquireQueueOwnerLease, gt as sessionEventSegmentPath, h as terminateQueueOwnerForSession, ht as sessionEventLockPath, i as trySetModeOnRunningOwner, k as absolutePath, l as parseQueueRequest, m as terminateProcess, mt as sessionEventActivePath, n as tryCloseSessionOnRunningOwner, nt as setPerfGauge, o as trySubmitToRunningOwner, ot as isAcpMessageObject, p as releaseQueueOwnerLease, pt as sessionBaseDir, q as SessionJournalReader, qt as SessionResumeRequiredError, r as trySetConfigOptionOnRunningOwner, rt as startPerfTimer, s as queueRequestByteLimit, st as isSessionUpdateNotification, t as tryCancelOnRunningOwner, tt as resetPerfMetrics, v as QueueLeaseGuardSettlementError, wt as EXIT_CODES, x as readProcessTable, xt as formatUnknownErrorMessage, y as createLockOwner, yt as isProcessAlive, z as resolveSessionRecord, zt as GeminiAcpStartupTimeoutError } from "./ipc-fMgj5vrO.js";
2
+ import { A as resolveAgentCommandParts, C as asAbsoluteCwd, E as isoNow$2, F as waitForSpawn, M as runTimedExecFile, N as splitCommandLine, P as waitForChildExit, S as PROCESS_HELPER_TIMEOUT_MS, T as isChildProcessRunning, _ as buildSpawnCommandOptions, b as readWindowsEnvValue, f as resolveBuiltInAgentLaunch, g as buildAgentSpawnCommand, j as resolveAgentSessionCwd, k as requireAgentStdio, v as buildTerminalShellSpawnCommand, w as basenameToken, x as resolveWindowsExecutablePath, y as buildTerminalSpawnCommand } from "./agent-registry-Ct2yWPW7.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";
@@ -15,6 +15,7 @@ import { isPathInside } from "@openclaw/fs-safe/path";
15
15
  import { root } from "@openclaw/fs-safe/root";
16
16
  import readline from "node:readline/promises";
17
17
  import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
18
+ import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
18
19
  import net from "node:net";
19
20
  //#region src/acp/error-normalization.ts
20
21
  const AUTH_REQUIRED_ACP_CODES = /* @__PURE__ */ new Set([-32e3]);
@@ -484,22 +485,69 @@ function normalizeQueueOwnerTtlMs(ttlMs) {
484
485
  return Math.round(ttlMs);
485
486
  }
486
487
  //#endregion
488
+ //#region src/file-read-window.ts
489
+ function sliceReadWindow(content, line, limit) {
490
+ if (line == null && limit == null) return content;
491
+ const lines = content.split("\n");
492
+ const startIndex = Math.max(0, (line == null ? 1 : Math.max(1, Math.trunc(line))) - 1);
493
+ const maxLines = limit == null ? void 0 : Math.max(0, Math.trunc(limit));
494
+ if (maxLines === 0) return "";
495
+ const endIndex = maxLines == null ? lines.length : Math.min(lines.length, startIndex + maxLines);
496
+ return lines.slice(startIndex, endIndex).join("\n");
497
+ }
498
+ //#endregion
487
499
  //#region src/permission-prompt.ts
500
+ let promptQueue = Promise.resolve();
488
501
  async function promptForPermission(options) {
489
- if (!process.stdin.isTTY || !process.stderr.isTTY) return false;
490
- if (options.header) process.stderr.write(`\n${options.header}\n`);
491
- if (options.details && options.details.trim().length > 0) process.stderr.write(`${options.details}\n`);
502
+ options.signal?.throwIfAborted();
503
+ const result = promptQueue.then(() => askPermission(options));
504
+ promptQueue = result.then(() => {}, () => {});
505
+ const signal = options.signal;
506
+ if (!signal) return await result;
507
+ let onAbort = () => {};
508
+ const aborted = new Promise((_, reject) => {
509
+ onAbort = () => reject(signal.reason);
510
+ signal.addEventListener("abort", onAbort, { once: true });
511
+ });
512
+ try {
513
+ return await Promise.race([result, aborted]);
514
+ } finally {
515
+ signal.removeEventListener("abort", onAbort);
516
+ }
517
+ }
518
+ async function askPermission(options) {
519
+ options.signal?.throwIfAborted();
520
+ if (!canPrompt()) return false;
521
+ writePromptDetails(options);
492
522
  const rl = readline.createInterface({
493
523
  input: process.stdin,
494
524
  output: process.stderr
495
525
  });
526
+ const questionController = new AbortController();
527
+ const signal = options.signal ? AbortSignal.any([options.signal, questionController.signal]) : questionController.signal;
528
+ let onClose = () => {};
529
+ const closed = new Promise((resolve) => {
530
+ onClose = () => resolve(void 0);
531
+ rl.once("close", onClose);
532
+ });
496
533
  try {
497
- const normalized = (await rl.question(options.prompt)).trim().toLowerCase();
534
+ const answer = await Promise.race([rl.question(options.prompt, { signal }), closed]);
535
+ options.signal?.throwIfAborted();
536
+ const normalized = answer?.trim().toLowerCase();
498
537
  return normalized === "y" || normalized === "yes";
499
538
  } finally {
539
+ rl.removeListener("close", onClose);
540
+ questionController.abort();
500
541
  rl.close();
501
542
  }
502
543
  }
544
+ function canPrompt() {
545
+ return process.stdin.isTTY && process.stderr.isTTY && !process.stdin.readableEnded && !process.stdin.destroyed;
546
+ }
547
+ function writePromptDetails(options) {
548
+ if (options.header) process.stderr.write(`\n${options.header}\n`);
549
+ if (options.details && options.details.trim().length > 0) process.stderr.write(`${options.details}\n`);
550
+ }
503
551
  //#endregion
504
552
  //#region src/filesystem.ts
505
553
  const WRITE_PREVIEW_MAX_LINES = 16;
@@ -515,11 +563,12 @@ function toWritePreview(content) {
515
563
  if (preview.length > WRITE_PREVIEW_MAX_CHARS) preview = `${preview.slice(0, 1197)}...`;
516
564
  return preview;
517
565
  }
518
- async function defaultConfirmWrite(filePath, preview) {
566
+ async function defaultConfirmWrite(filePath, preview, signal) {
519
567
  return await promptForPermission({
520
568
  header: `[permission] Allow write to ${filePath}?`,
521
569
  details: preview,
522
- prompt: "Allow write? (y/N) "
570
+ prompt: "Allow write? (y/N) ",
571
+ signal
523
572
  });
524
573
  }
525
574
  function canPromptForPermission$2() {
@@ -545,7 +594,8 @@ var FileSystemHandlers = class {
545
594
  this.permissionMode = permissionMode;
546
595
  this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
547
596
  }
548
- async readTextFile(params) {
597
+ async readTextFile(params, authority) {
598
+ assertControlAuthority(authority);
549
599
  const filePath = this.resolvePathWithinRoot(params.path);
550
600
  const summary = `read_text_file: ${filePath}`;
551
601
  this.emitOperation({
@@ -557,8 +607,9 @@ var FileSystemHandlers = class {
557
607
  });
558
608
  try {
559
609
  if (this.permissionMode === "deny-all") throw new PermissionDeniedError("Permission denied for fs/read_text_file (--deny-all)");
560
- const content = await (await this.getWorkspace()).readText(`.${path.sep}${path.relative(this.rootDir, filePath)}`);
561
- const sliced = this.sliceContent(content, params.line, params.limit);
610
+ const content = await (await this.getWorkspace()).readText(filePath);
611
+ assertControlAuthority(authority);
612
+ const sliced = sliceReadWindow(content, params.line, params.limit);
562
613
  this.emitOperation({
563
614
  method: "fs/read_text_file",
564
615
  status: "completed",
@@ -579,7 +630,8 @@ var FileSystemHandlers = class {
579
630
  throw error;
580
631
  }
581
632
  }
582
- async writeTextFile(params) {
633
+ async writeTextFile(params, authority) {
634
+ assertControlAuthority(authority);
583
635
  const filePath = this.resolvePathWithinRoot(params.path);
584
636
  const preview = toWritePreview(params.content);
585
637
  const summary = `write_text_file: ${filePath}`;
@@ -591,9 +643,17 @@ var FileSystemHandlers = class {
591
643
  timestamp: nowIso$1()
592
644
  });
593
645
  try {
594
- if (!await this.isWriteApproved(filePath, preview)) throw new PermissionDeniedError("Permission denied for fs/write_text_file");
595
- const file = await (await this.getWorkspace()).openWritable(`.${path.sep}${path.relative(this.rootDir, filePath)}`, { mode: 438 });
646
+ const approved = await this.isWriteApproved(filePath, preview, authority?.signal);
647
+ assertControlAuthority(authority);
648
+ if (!approved) throw new PermissionDeniedError("Permission denied for fs/write_text_file");
649
+ const workspace = await this.getWorkspace();
650
+ const target = await workspace.resolve(filePath);
651
+ const file = await workspace.openWritable(target, {
652
+ mode: 438,
653
+ assertBeforeMutation: () => assertControlAuthority(authority)
654
+ });
596
655
  try {
656
+ assertControlAuthority(authority);
597
657
  await file.handle.writeFile(params.content, "utf8");
598
658
  } finally {
599
659
  await file.handle.close();
@@ -618,17 +678,17 @@ var FileSystemHandlers = class {
618
678
  throw error;
619
679
  }
620
680
  }
621
- async isWriteApproved(filePath, preview) {
681
+ async isWriteApproved(filePath, preview, signal) {
622
682
  if (this.permissionMode === "approve-all") return true;
623
683
  if (this.permissionMode === "deny-all") return false;
624
684
  if (this.usesDefaultConfirmWrite && this.nonInteractivePermissions === "fail" && !canPromptForPermission$2()) throw new PermissionPromptUnavailableError();
625
- return await this.confirmWrite(filePath, preview);
685
+ return await this.confirmWrite(filePath, preview, signal);
626
686
  }
627
687
  resolvePathWithinRoot(rawPath) {
628
688
  if (!path.isAbsolute(rawPath)) throw new Error(`Path must be absolute: ${rawPath}`);
629
689
  const resolved = path.resolve(rawPath);
630
690
  if (!isPathInside(this.rootDir, resolved)) throw new Error(`Path is outside allowed cwd subtree: ${resolved}`);
631
- return resolved;
691
+ return rawPath;
632
692
  }
633
693
  getWorkspace() {
634
694
  return this.workspace ??= root(this.rootDir, {
@@ -637,15 +697,6 @@ var FileSystemHandlers = class {
637
697
  maxBytes: Infinity
638
698
  });
639
699
  }
640
- sliceContent(content, line, limit) {
641
- if (line == null && limit == null) return content;
642
- const lines = content.split("\n");
643
- const startIndex = Math.max(0, (line == null ? 1 : Math.max(1, Math.trunc(line))) - 1);
644
- const maxLines = limit == null ? void 0 : Math.max(0, Math.trunc(limit));
645
- if (maxLines === 0) return "";
646
- const endIndex = maxLines == null ? lines.length : Math.min(lines.length, startIndex + maxLines);
647
- return lines.slice(startIndex, endIndex).join("\n");
648
- }
649
700
  readWindowDetails(line, limit) {
650
701
  if (line == null && limit == null) return;
651
702
  return `line=${line == null ? 1 : Math.max(1, Math.trunc(line))}, limit=${limit == null ? "all" : Math.max(0, Math.trunc(limit))}`;
@@ -691,11 +742,11 @@ function pickOption(options, kinds) {
691
742
  const TOOL_KIND_TITLE_MATCHERS = [
692
743
  {
693
744
  kind: "read",
694
- needles: ["read", "cat"]
745
+ names: ["read", "cat"]
695
746
  },
696
747
  {
697
748
  kind: "search",
698
- needles: [
749
+ names: [
699
750
  "search",
700
751
  "find",
701
752
  "grep"
@@ -703,7 +754,7 @@ const TOOL_KIND_TITLE_MATCHERS = [
703
754
  },
704
755
  {
705
756
  kind: "edit",
706
- needles: [
757
+ names: [
707
758
  "write",
708
759
  "edit",
709
760
  "patch"
@@ -711,15 +762,15 @@ const TOOL_KIND_TITLE_MATCHERS = [
711
762
  },
712
763
  {
713
764
  kind: "delete",
714
- needles: ["delete", "remove"]
765
+ names: ["delete", "remove"]
715
766
  },
716
767
  {
717
768
  kind: "move",
718
- needles: ["move", "rename"]
769
+ names: ["move", "rename"]
719
770
  },
720
771
  {
721
772
  kind: "execute",
722
- needles: [
773
+ names: [
723
774
  "run",
724
775
  "execute",
725
776
  "bash"
@@ -727,7 +778,7 @@ const TOOL_KIND_TITLE_MATCHERS = [
727
778
  },
728
779
  {
729
780
  kind: "fetch",
730
- needles: [
781
+ names: [
731
782
  "fetch",
732
783
  "http",
733
784
  "url"
@@ -735,25 +786,25 @@ const TOOL_KIND_TITLE_MATCHERS = [
735
786
  },
736
787
  {
737
788
  kind: "think",
738
- needles: ["think"]
789
+ names: ["think"]
739
790
  }
740
791
  ];
741
792
  function inferToolKind(params) {
742
793
  if (params.toolCall.kind) return params.toolCall.kind;
743
794
  const title = params.toolCall.title?.trim().toLowerCase();
744
795
  if (!title) return;
745
- const head = title.split(":", 1)[0]?.trim();
796
+ const head = title.split(/[:\s]/, 1)[0];
746
797
  if (!head) return;
747
- return titleHeadToolKind(head) ?? "other";
748
- }
749
- function titleHeadToolKind(head) {
750
- return TOOL_KIND_TITLE_MATCHERS.find(({ needles }) => needles.some((needle) => head.includes(needle)))?.kind;
798
+ return TOOL_KIND_TITLE_MATCHERS.find(({ names }) => names.includes(head))?.kind ?? "other";
751
799
  }
752
800
  function isAutoApprovedReadKind(kind) {
753
801
  return kind === "read" || kind === "search";
754
802
  }
755
- async function promptForToolPermission(params) {
756
- return await promptForPermission({ prompt: `\n[permission] Allow ${params.toolCall.title ?? "tool"} [${inferToolKind(params) ?? "other"}]? (y/N) ` });
803
+ async function promptForToolPermission(params, signal) {
804
+ return await promptForPermission({
805
+ prompt: `\n[permission] Allow ${params.toolCall.title ?? "tool"} [${inferToolKind(params) ?? "other"}]? (y/N) `,
806
+ signal
807
+ });
757
808
  }
758
809
  function canPromptForPermission$1() {
759
810
  return process.stdin.isTTY && process.stderr.isTTY;
@@ -848,24 +899,24 @@ function selectedOrFirst(options, allowOption) {
848
899
  function selectedOrCancelled(option) {
849
900
  return { response: option ? selected(option.optionId) : cancelled() };
850
901
  }
851
- async function resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption) {
852
- if (canPromptForPermission$1()) return resolveInteractivePromptResult(params, allowOption, rejectOption);
902
+ async function resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption, signal) {
903
+ if (canPromptForPermission$1()) return resolveInteractivePromptResult(params, allowOption, rejectOption, signal);
853
904
  const escalation = buildEscalationEvent(params, policyMatch.matchedRule);
854
905
  return {
855
906
  response: withPermissionMetadata(rejectOption ? selected(rejectOption.optionId) : cancelled(), { permissionEscalation: escalation }),
856
907
  escalation
857
908
  };
858
909
  }
859
- async function resolveInteractivePromptResult(params, allowOption, rejectOption) {
860
- const approved = await promptForToolPermission(params);
910
+ async function resolveInteractivePromptResult(params, allowOption, rejectOption, signal) {
911
+ const approved = await promptForToolPermission(params, signal);
861
912
  if (approved && allowOption) return { response: selected(allowOption.optionId) };
862
913
  if (!approved && rejectOption) return { response: selected(rejectOption.optionId) };
863
914
  return { response: cancelled() };
864
915
  }
865
- function resolvePolicyMatch(params, policyMatch, options, allowOption, rejectOption) {
916
+ function resolvePolicyMatch(params, policyMatch, options, allowOption, rejectOption, signal) {
866
917
  if (policyMatch?.action === "approve") return selectedOrFirst(options, allowOption);
867
918
  if (policyMatch?.action === "deny") return selectedOrCancelled(rejectOption);
868
- if (policyMatch?.action === "escalate") return resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption);
919
+ if (policyMatch?.action === "escalate") return resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption, signal);
869
920
  }
870
921
  function resolveModeMatch(options, mode, allowOption, rejectOption) {
871
922
  if (mode === "approve-all") return selectedOrFirst(options, allowOption);
@@ -875,24 +926,35 @@ function resolveNonInteractivePermission(nonInteractivePolicy, rejectOption) {
875
926
  if (nonInteractivePolicy === "fail") throw new PermissionPromptUnavailableError();
876
927
  return selectedOrCancelled(rejectOption);
877
928
  }
878
- async function resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption) {
929
+ async function resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption, signal) {
879
930
  if (isAutoApprovedReadKind(inferToolKind(params)) && allowOption) return { response: selected(allowOption.optionId) };
880
931
  if (!canPromptForPermission$1()) return resolveNonInteractivePermission(nonInteractivePolicy, rejectOption);
881
- return resolveInteractivePromptResult(params, allowOption, rejectOption);
932
+ return resolveInteractivePromptResult(params, allowOption, rejectOption, signal);
882
933
  }
883
934
  function permissionModeSatisfies(actual, required) {
884
935
  return PERMISSION_MODE_RANK[actual] >= PERMISSION_MODE_RANK[required];
885
936
  }
886
- async function resolvePermissionRequestWithDetails(params, mode, nonInteractivePolicy = "deny", policy) {
937
+ async function resolvePermissionRequestWithDetails(params, mode, nonInteractivePolicy = "deny", policy, signal) {
938
+ try {
939
+ signal?.throwIfAborted();
940
+ const result = await resolvePermissionByPolicy(params, mode, nonInteractivePolicy, policy, signal);
941
+ signal?.throwIfAborted();
942
+ return result;
943
+ } catch (error) {
944
+ if (signal?.aborted) return { response: cancelled() };
945
+ throw error;
946
+ }
947
+ }
948
+ async function resolvePermissionByPolicy(params, mode, nonInteractivePolicy, policy, signal) {
887
949
  const options = params.options ?? [];
888
950
  if (options.length === 0) return { response: cancelled() };
889
951
  const allowOption = pickOption(options, ["allow_once", "allow_always"]);
890
952
  const rejectOption = pickOption(options, ["reject_once", "reject_always"]);
891
- const resolvedByPolicy = await resolvePolicyMatch(params, matchPermissionPolicy(params, policy), options, allowOption, rejectOption);
953
+ const resolvedByPolicy = await resolvePolicyMatch(params, matchPermissionPolicy(params, policy), options, allowOption, rejectOption, signal);
892
954
  if (resolvedByPolicy) return resolvedByPolicy;
893
955
  const resolvedByMode = resolveModeMatch(options, mode, allowOption, rejectOption);
894
956
  if (resolvedByMode) return resolvedByMode;
895
- return resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption);
957
+ return resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption, signal);
896
958
  }
897
959
  const DECISION_FALLBACK_ORDER = {
898
960
  allow_once: ["allow_once", "allow_always"],
@@ -1321,10 +1383,12 @@ const REQUESTED_MODEL_UNSUPPORTED_REASONS = ["missing-capability", "unadvertised
1321
1383
  var RequestedModelUnsupportedError = class extends Error {
1322
1384
  code = REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE;
1323
1385
  reason;
1324
- constructor(message, reason) {
1386
+ ambiguous;
1387
+ constructor(message, reason, details) {
1325
1388
  super(message);
1326
1389
  this.name = "RequestedModelUnsupportedError";
1327
1390
  this.reason = reason;
1391
+ if (details?.ambiguous) this.ambiguous = true;
1328
1392
  }
1329
1393
  };
1330
1394
  function isRequestedModelUnsupportedReason(value) {
@@ -1427,6 +1491,7 @@ function resolveRequestedModelId(params) {
1427
1491
  if (!params.models || !isCursorAcpCommandForModelAlias(params.agentCommand)) return params.requestedModel;
1428
1492
  if (params.models.availableModels.some((model) => model.modelId === params.requestedModel)) return params.requestedModel;
1429
1493
  const candidates = params.models.availableModels.map((model) => model.modelId).filter((modelId) => modelId.startsWith(`${params.requestedModel}[`));
1494
+ if (candidates.length > 1) throw new RequestedModelUnsupportedError(`Cannot select model "${params.requestedModel}": multiple advertised Cursor models match (${candidates.join(", ")}). Use an exact advertised model ID.`, "unadvertised-model", { ambiguous: true });
1430
1495
  return candidates.length === 1 ? candidates[0] : params.requestedModel;
1431
1496
  }
1432
1497
  function isCursorAcpCommandForModelAlias(agentCommand) {
@@ -1493,15 +1558,22 @@ function enqueueNdJsonLine(agentCommand, line, controller) {
1493
1558
  console.error("Failed to parse JSON message:", trimmedLine, err);
1494
1559
  }
1495
1560
  }
1496
- function enqueueNdJsonLines(agentCommand, lines, controller) {
1561
+ function enqueueNdJsonChunk(agentCommand, chunk, fragments, controller) {
1562
+ const lines = chunk.split("\n");
1563
+ const suffix = lines.pop() || "";
1564
+ if (lines.length > 0 && fragments.length > 0) {
1565
+ lines[0] = fragments.join("") + lines[0];
1566
+ fragments.length = 0;
1567
+ }
1497
1568
  for (const line of lines) enqueueNdJsonLine(agentCommand, line, controller);
1569
+ if (suffix) fragments.push(suffix);
1498
1570
  }
1499
1571
  function createNdJsonMessageStream(agentCommand, output, input, maxMessageBytes, onReadError) {
1500
1572
  const textEncoder = new TextEncoder();
1501
1573
  const textDecoder = new TextDecoder();
1502
1574
  return {
1503
1575
  readable: new ReadableStream({ async start(controller) {
1504
- let content = "";
1576
+ const fragments = [];
1505
1577
  let retainedBytes = 0;
1506
1578
  const reader = input.getReader();
1507
1579
  try {
@@ -1509,10 +1581,7 @@ function createNdJsonMessageStream(agentCommand, output, input, maxMessageBytes,
1509
1581
  const { value, done } = await reader.read();
1510
1582
  if (done) break;
1511
1583
  if (maxMessageBytes !== void 0) retainedBytes = countLineBytes(value, retainedBytes, maxMessageBytes);
1512
- content += textDecoder.decode(value, { stream: true });
1513
- const lines = content.split("\n");
1514
- content = lines.pop() || "";
1515
- enqueueNdJsonLines(agentCommand, lines, controller);
1584
+ enqueueNdJsonChunk(agentCommand, textDecoder.decode(value, { stream: true }), fragments, controller);
1516
1585
  }
1517
1586
  controller.close();
1518
1587
  } catch (err) {
@@ -1604,65 +1673,88 @@ function promptRequestOwner(message) {
1604
1673
  }
1605
1674
  //#endregion
1606
1675
  //#region src/acp/process-descendants.ts
1607
- function parseProcessTable(output) {
1608
- const table = /* @__PURE__ */ new Map();
1609
- for (const line of output.split("\n")) {
1610
- const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
1611
- if (match && !match[3].startsWith("Z")) {
1612
- const pid = Number(match[1]);
1613
- if (Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid) table.set(pid, {
1614
- pid,
1615
- parentPid: Number(match[2]),
1616
- birth: match[4]
1617
- });
1618
- }
1619
- }
1620
- return table;
1621
- }
1622
1676
  function includeDescendants(table, owned) {
1623
1677
  let expanded;
1624
1678
  do {
1625
1679
  expanded = false;
1626
- for (const identity of table.values()) if (owned.has(identity.parentPid) && !owned.has(identity.pid)) {
1627
- owned.add(identity.pid);
1628
- expanded = true;
1680
+ for (const identity of table.values()) {
1681
+ const parent = table.get(identity.parentPid);
1682
+ if (process.platform === "win32" && (!parent || parent.birth > identity.birth)) continue;
1683
+ if (owned.has(identity.parentPid) && !owned.has(identity.pid)) {
1684
+ owned.add(identity.pid);
1685
+ expanded = true;
1686
+ }
1629
1687
  }
1630
1688
  } while (expanded);
1631
1689
  }
1632
- /** Best-effort POSIX cleanup for descendants witnessed during this child launch. */
1690
+ /** Best-effort cleanup for descendants witnessed during this child launch. */
1633
1691
  var ProcessDescendants = class {
1634
1692
  child;
1635
1693
  identities = /* @__PURE__ */ new Map();
1694
+ rootBirth;
1636
1695
  pending;
1637
1696
  retired = false;
1638
- constructor(child) {
1697
+ ownProcessGroup;
1698
+ captureGroupAfterExit;
1699
+ groupExitedAt;
1700
+ onRootExit = () => {
1701
+ this.groupExitedAt = Date.now();
1702
+ };
1703
+ constructor(child, options = {}) {
1639
1704
  this.child = child;
1705
+ this.ownProcessGroup = process.platform !== "win32" && Boolean(options.ownProcessGroup);
1706
+ this.captureGroupAfterExit = this.ownProcessGroup && isChildProcessRunning(child);
1707
+ if (this.ownProcessGroup) child.once("exit", this.onRootExit);
1640
1708
  }
1641
1709
  capture(timeoutMs = PROCESS_HELPER_TIMEOUT_MS) {
1642
- if (this.retired || process.platform === "win32") return Promise.resolve(true);
1710
+ if (this.retired) return Promise.resolve(true);
1643
1711
  this.pending ??= this.readSnapshot(timeoutMs).finally(() => {
1644
1712
  this.pending = void 0;
1645
1713
  });
1646
1714
  return this.pending;
1647
1715
  }
1648
1716
  async readSnapshot(timeoutMs) {
1717
+ const deadline = performance.now() + timeoutMs;
1718
+ const rootWasRunning = isChildProcessRunning(this.child);
1719
+ const captureRootGroup = rootWasRunning || this.captureGroupAfterExit;
1720
+ if (!rootWasRunning) this.captureGroupAfterExit = false;
1649
1721
  try {
1650
- const output = await runTimedExecFile("ps", ["-eo", "pid=,ppid=,stat=,lstart="], { timeoutMs });
1651
- if (!this.retired) this.refresh(parseProcessTable(output));
1722
+ const table = await readProcessTable(timeoutMs);
1723
+ if (!this.retired) {
1724
+ table.delete(1);
1725
+ table.delete(process.pid);
1726
+ this.refresh(table, captureRootGroup);
1727
+ if (this.captureGroupAfterExit && !isChildProcessRunning(this.child)) return await this.readSnapshot(Math.max(1, deadline - performance.now()));
1728
+ }
1652
1729
  return true;
1653
1730
  } catch {
1731
+ if (!isChildProcessRunning(this.child)) this.captureGroupAfterExit = false;
1654
1732
  return false;
1655
1733
  }
1656
1734
  }
1657
- refresh(table) {
1735
+ refresh(table, rootWasRunning) {
1658
1736
  const owned = /* @__PURE__ */ new Set();
1659
1737
  for (const [pid, identity] of this.identities) if (table.get(pid)?.birth === identity.birth) owned.add(pid);
1660
- const root = this.child.pid;
1661
- if (root && isChildProcessRunning(this.child)) owned.add(root);
1738
+ this.includeRoot(table, owned);
1739
+ if (this.ownProcessGroup) this.includeProcessGroup(table, owned, rootWasRunning);
1662
1740
  includeDescendants(table, owned);
1663
- owned.delete(root ?? 0);
1741
+ owned.delete(this.child.pid ?? 0);
1664
1742
  this.identities = new Map([...table].filter(([pid]) => owned.has(pid)));
1665
1743
  }
1744
+ includeRoot(table, owned) {
1745
+ const root = this.child.pid;
1746
+ const rootIdentity = root && table.get(root);
1747
+ if (rootIdentity && isChildProcessRunning(this.child)) {
1748
+ this.rootBirth ??= rootIdentity.birth;
1749
+ if (rootIdentity.birth === this.rootBirth) owned.add(rootIdentity.pid);
1750
+ }
1751
+ }
1752
+ includeProcessGroup(table, owned, rootWasRunning) {
1753
+ const root = this.child.pid;
1754
+ if (root && (rootWasRunning || [...owned].some((pid) => table.get(pid)?.groupPid === root))) {
1755
+ for (const identity of table.values()) if (identity.groupPid === root && (this.groupExitedAt === void 0 || Date.parse(identity.birth) <= this.groupExitedAt)) owned.add(identity.pid);
1756
+ }
1757
+ }
1666
1758
  async signal(signal, timeoutMs) {
1667
1759
  if (!await this.capture(timeoutMs) || this.retired) return;
1668
1760
  for (const pid of this.identities.keys()) try {
@@ -1671,6 +1763,9 @@ var ProcessDescendants = class {
1671
1763
  this.identities.delete(pid);
1672
1764
  }
1673
1765
  }
1766
+ hasTrackedProcesses() {
1767
+ return this.identities.size > 0;
1768
+ }
1674
1769
  async waitForExit(timeoutMs) {
1675
1770
  const deadline = Date.now() + timeoutMs;
1676
1771
  do {
@@ -1682,6 +1777,7 @@ var ProcessDescendants = class {
1682
1777
  }
1683
1778
  retire() {
1684
1779
  this.retired = true;
1780
+ this.child.off("exit", this.onRootExit);
1685
1781
  this.identities.clear();
1686
1782
  }
1687
1783
  };
@@ -1749,16 +1845,27 @@ function readTerminalOutputCeiling() {
1749
1845
  if (!/^\d+$/.test(raw) || !Number.isSafeInteger(bytes)) throw new Error("ACPX_TERMINAL_MAX_OUTPUT_BYTES must be a non-negative safe integer; zero disables the host ceiling");
1750
1846
  return bytes === 0 ? void 0 : bytes;
1751
1847
  }
1848
+ function resolveTerminalOutputLimit(requested, ceiling) {
1849
+ return Math.min(Math.max(0, Math.round(requested ?? DEFAULT_TERMINAL_OUTPUT_LIMIT_BYTES)), ceiling ?? Number.POSITIVE_INFINITY);
1850
+ }
1851
+ function tracksTerminalDescendants(terminal) {
1852
+ return terminal.killProcessGroup || terminal.descendants !== void 0;
1853
+ }
1854
+ function createTerminalDescendants(proc, killProcessGroup) {
1855
+ return process.platform === "win32" && killProcessGroup ? void 0 : new ProcessDescendants(proc, { ownProcessGroup: true });
1856
+ }
1752
1857
  function trimToUtf8Boundary(buffer, limit) {
1753
1858
  if (limit <= 0) return Buffer.alloc(0);
1754
- if (buffer.length <= limit) return buffer;
1755
- let start = buffer.length - limit;
1859
+ let start = Math.max(0, buffer.length - limit);
1756
1860
  while (start < buffer.length && (buffer[start] & 192) === 128) start += 1;
1757
- if (start >= buffer.length) start = buffer.length - limit;
1861
+ if (start >= buffer.length) return Buffer.alloc(0);
1758
1862
  return buffer.subarray(start);
1759
1863
  }
1760
- async function defaultConfirmExecute(commandLine) {
1761
- return await promptForPermission({ prompt: `\n[permission] Allow terminal command "${commandLine}"? (y/N) ` });
1864
+ async function defaultConfirmExecute(commandLine, signal) {
1865
+ return await promptForPermission({
1866
+ prompt: `\n[permission] Allow terminal command "${commandLine}"? (y/N) `,
1867
+ signal
1868
+ });
1762
1869
  }
1763
1870
  function canPromptForPermission() {
1764
1871
  return process.stdin.isTTY && process.stderr.isTTY;
@@ -1788,14 +1895,16 @@ var TerminalManager = class {
1788
1895
  this.onOperation = options.onOperation;
1789
1896
  this.usesDefaultConfirmExecute = options.confirmExecute == null;
1790
1897
  this.confirmExecute = options.confirmExecute ?? defaultConfirmExecute;
1791
- this.killGraceMs = Math.max(0, Math.round(options.killGraceMs ?? DEFAULT_KILL_GRACE_MS));
1898
+ const killGraceMs = Math.max(0, Math.round(options.killGraceMs ?? DEFAULT_KILL_GRACE_MS));
1899
+ this.killGraceMs = Number.isFinite(killGraceMs) && killGraceMs <= 2147483647 ? killGraceMs : 1;
1792
1900
  this.processHelperTimeoutMs = Math.max(1, Math.round(options.processHelperTimeoutMs ?? 8e3));
1793
1901
  }
1794
1902
  updatePermissionPolicy(permissionMode, nonInteractivePermissions) {
1795
1903
  this.permissionMode = permissionMode;
1796
1904
  this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
1797
1905
  }
1798
- async createTerminal(params) {
1906
+ async createTerminal(params, authority) {
1907
+ assertControlAuthority(authority);
1799
1908
  const commandLine = toCommandLine(params.command, params.args);
1800
1909
  const summary = `terminal/create: ${commandLine}`;
1801
1910
  this.emitOperation({
@@ -1805,10 +1914,11 @@ var TerminalManager = class {
1805
1914
  timestamp: nowIso()
1806
1915
  });
1807
1916
  try {
1808
- if (!await this.isExecuteApproved(commandLine)) throw new PermissionDeniedError("Permission denied for terminal/create");
1809
- const requestedLimit = Math.max(0, Math.round(params.outputByteLimit ?? DEFAULT_TERMINAL_OUTPUT_LIMIT_BYTES));
1810
- const outputByteLimit = Math.min(requestedLimit, this.outputCeilingBytes ?? Number.POSITIVE_INFINITY);
1811
- const { proc, spawnCommand } = await spawnTerminalProcess(params, this.cwd);
1917
+ const approved = await this.isExecuteApproved(commandLine, authority?.signal);
1918
+ assertControlAuthority(authority);
1919
+ if (!approved) throw new PermissionDeniedError("Permission denied for terminal/create");
1920
+ const outputByteLimit = resolveTerminalOutputLimit(params.outputByteLimit, this.outputCeilingBytes);
1921
+ const { proc, spawnCommand } = await spawnTerminalProcess(params, this.cwd, authority);
1812
1922
  let resolveExit = () => {};
1813
1923
  const exitPromise = new Promise((resolve) => {
1814
1924
  resolveExit = resolve;
@@ -1817,6 +1927,7 @@ var TerminalManager = class {
1817
1927
  process: proc,
1818
1928
  killProcessGroup: spawnCommand.killProcessGroup,
1819
1929
  descendantPids: /* @__PURE__ */ new Set(),
1930
+ descendants: createTerminalDescendants(proc, spawnCommand.killProcessGroup),
1820
1931
  processHelperTimeoutMs: this.processHelperTimeoutMs,
1821
1932
  output: Buffer.alloc(0),
1822
1933
  truncated: false,
@@ -1830,10 +1941,8 @@ var TerminalManager = class {
1830
1941
  const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1831
1942
  if (bytes.length === 0) return;
1832
1943
  terminal.output = Buffer.concat([terminal.output, bytes]);
1833
- if (terminal.output.length > terminal.outputByteLimit) {
1834
- terminal.output = trimToUtf8Boundary(terminal.output, terminal.outputByteLimit);
1835
- terminal.truncated = true;
1836
- }
1944
+ terminal.truncated ||= terminal.output.length > terminal.outputByteLimit;
1945
+ if (terminal.truncated) terminal.output = trimToUtf8Boundary(terminal.output, terminal.outputByteLimit);
1837
1946
  };
1838
1947
  proc.stdout.on("data", appendOutput);
1839
1948
  proc.stderr.on("data", appendOutput);
@@ -1842,9 +1951,10 @@ var TerminalManager = class {
1842
1951
  proc.once("exit", (exitCode, signal) => {
1843
1952
  terminal.exitCode = exitCode;
1844
1953
  terminal.signal = signal;
1845
- terminal.processGroupSnapshotPromise = rememberProcessGroupPids(terminal);
1954
+ terminal.processGroupSnapshotPromise = terminal.descendants ? terminal.descendants.capture(terminal.processHelperTimeoutMs).then(() => {}) : rememberProcessGroupPids(terminal);
1846
1955
  (async () => {
1847
1956
  await terminal.processGroupSnapshotPromise;
1957
+ terminal.processGroupSnapshotPromise = void 0;
1848
1958
  terminal.resolveExit({
1849
1959
  exitCode: exitCode ?? null,
1850
1960
  signal: signal ?? null
@@ -1853,6 +1963,16 @@ var TerminalManager = class {
1853
1963
  });
1854
1964
  const terminalId = randomUUID();
1855
1965
  this.terminals.set(terminalId, terminal);
1966
+ try {
1967
+ await terminal.descendants?.capture(terminal.processHelperTimeoutMs);
1968
+ assertControlAuthority(authority);
1969
+ } catch (error) {
1970
+ await this.releaseTerminal({
1971
+ terminalId,
1972
+ sessionId: params.sessionId
1973
+ });
1974
+ throw error;
1975
+ }
1856
1976
  this.emitOperation({
1857
1977
  method: "terminal/create",
1858
1978
  status: "completed",
@@ -1958,6 +2078,9 @@ var TerminalManager = class {
1958
2078
  try {
1959
2079
  await this.killProcess(terminal);
1960
2080
  await terminal.exitPromise.catch(() => {});
2081
+ terminal.descendants?.retire();
2082
+ terminal.process.stdout.destroy();
2083
+ terminal.process.stderr.destroy();
1961
2084
  terminal.output = Buffer.alloc(0);
1962
2085
  this.terminals.delete(params.terminalId);
1963
2086
  this.emitOperation({
@@ -1980,10 +2103,13 @@ var TerminalManager = class {
1980
2103
  }
1981
2104
  }
1982
2105
  async shutdown() {
1983
- for (const terminalId of Array.from(this.terminals.keys())) await this.releaseTerminal({
2106
+ const results = await Promise.allSettled(Array.from(this.terminals.keys(), (terminalId) => this.releaseTerminal({
1984
2107
  terminalId,
1985
2108
  sessionId: "shutdown"
1986
- });
2109
+ })));
2110
+ const failures = [];
2111
+ for (const result of results) if (result.status === "rejected") failures.push(result.reason);
2112
+ if (failures.length > 0) throw new AggregateError(failures, "Terminal shutdown failed", { cause: failures[0] });
1987
2113
  }
1988
2114
  getTerminal(terminalId) {
1989
2115
  return this.terminals.get(terminalId);
@@ -1991,23 +2117,23 @@ var TerminalManager = class {
1991
2117
  emitOperation(operation) {
1992
2118
  this.onOperation?.(operation);
1993
2119
  }
1994
- async isExecuteApproved(commandLine) {
2120
+ async isExecuteApproved(commandLine, signal) {
1995
2121
  if (this.permissionMode === "approve-all") return true;
1996
2122
  if (this.permissionMode === "deny-all") return false;
1997
2123
  if (this.usesDefaultConfirmExecute && this.nonInteractivePermissions === "fail" && !canPromptForPermission()) throw new PermissionPromptUnavailableError();
1998
- return await this.confirmExecute(commandLine);
2124
+ return await this.confirmExecute(commandLine, signal);
1999
2125
  }
2000
2126
  isRunning(terminal) {
2001
2127
  return terminal.exitCode === void 0 && terminal.signal === void 0;
2002
2128
  }
2003
2129
  async killProcess(terminal) {
2004
- if (!this.isRunning(terminal) && !terminal.killProcessGroup) return;
2130
+ if (!this.isRunning(terminal) && !tracksTerminalDescendants(terminal)) return;
2005
2131
  try {
2006
2132
  await this.signalProcess(terminal, "SIGTERM");
2007
2133
  } catch {
2008
2134
  return;
2009
2135
  }
2010
- if (await this.waitForCleanupAfterSignal(terminal) && !terminal.killProcessGroup) return;
2136
+ if (await this.waitForCleanupAfterSignal(terminal) && (!terminal.killProcessGroup || terminal.descendants)) return;
2011
2137
  try {
2012
2138
  await this.signalProcess(terminal, "SIGKILL");
2013
2139
  } catch {
@@ -2016,15 +2142,16 @@ var TerminalManager = class {
2016
2142
  await this.waitForFinalCleanup(terminal);
2017
2143
  }
2018
2144
  async signalProcess(terminal, signal) {
2145
+ if (terminal.descendants) {
2146
+ await terminal.descendants.signal(signal, terminal.processHelperTimeoutMs);
2147
+ if (this.isRunning(terminal)) terminal.process.kill(signal);
2148
+ return;
2149
+ }
2019
2150
  const pid = terminal.process.pid;
2020
2151
  if (terminal.killProcessGroup && pid && process.platform === "win32") {
2021
2152
  await this.signalWindowsProcessGroup(terminal, pid, signal);
2022
2153
  return;
2023
2154
  }
2024
- if (terminal.killProcessGroup && pid) {
2025
- await this.signalPosixProcessGroup(terminal, pid, signal);
2026
- return;
2027
- }
2028
2155
  terminal.process.kill(signal);
2029
2156
  }
2030
2157
  async signalWindowsProcessGroup(terminal, pid, signal) {
@@ -2035,14 +2162,6 @@ var TerminalManager = class {
2035
2162
  }
2036
2163
  for (const descendantPid of terminal.descendantPids) await killWindowsProcessTree(descendantPid, signal, terminal.processHelperTimeoutMs);
2037
2164
  }
2038
- async signalPosixProcessGroup(terminal, pid, signal) {
2039
- await this.captureDescendantPids(terminal, pid);
2040
- if (hasLiveProcessGroup(pid)) {
2041
- sendSignal(-pid, signal);
2042
- return;
2043
- }
2044
- for (const descendantPid of terminal.descendantPids) sendSignal(descendantPid, signal);
2045
- }
2046
2165
  async captureDescendantPids(terminal, pid) {
2047
2166
  if (!this.isRunning(terminal)) await terminal.processGroupSnapshotPromise?.catch(() => {});
2048
2167
  for (const descendantPid of await listDescendantPids(pid, terminal.processHelperTimeoutMs)) terminal.descendantPids.add(descendantPid);
@@ -2051,33 +2170,35 @@ var TerminalManager = class {
2051
2170
  if (!await this.waitForCleanupAfterSignal(terminal) && process.platform === "win32") throw new Error("Terminal process cleanup did not finish after SIGKILL");
2052
2171
  }
2053
2172
  async waitForCleanupAfterSignal(terminal) {
2054
- return await Promise.race([this.waitForTerminalAndTrackedDescendants(terminal).then(() => true), waitMs(this.killGraceMs).then(() => false)]);
2055
- }
2056
- async waitForTerminalAndTrackedDescendants(terminal) {
2057
- await terminal.exitPromise;
2058
- while (hasLiveTerminalProcessGroup(terminal)) await waitMs(25);
2059
- while (hasLivePid(terminal.descendantPids)) await waitMs(25);
2173
+ const deadline = performance.now() + this.killGraceMs;
2174
+ while (this.isRunning(terminal) || terminal.processGroupSnapshotPromise || await hasLiveTerminalDescendants(terminal, deadline - performance.now()) || hasLivePid(terminal.descendantPids)) {
2175
+ const remaining = deadline - performance.now();
2176
+ if (remaining <= 0) return false;
2177
+ await waitMs(Math.min(25, remaining));
2178
+ }
2179
+ return true;
2060
2180
  }
2061
2181
  };
2062
- async function spawnTerminalProcess(params, defaultCwd) {
2182
+ async function spawnTerminalProcess(params, defaultCwd, authority) {
2063
2183
  const directCommand = buildTerminalSpawnCommand(params.command, params.args);
2064
2184
  try {
2065
2185
  return {
2066
- proc: await spawnAndWait(directCommand, params, defaultCwd),
2186
+ proc: await spawnAndWait(directCommand, params, defaultCwd, authority),
2067
2187
  spawnCommand: directCommand
2068
2188
  };
2069
2189
  } catch (error) {
2070
2190
  const fallbackCommand = params.args === void 0 && isNotFoundSpawnError(error) ? buildTerminalFallbackSpawnCommand(params.command, params.cwd ?? defaultCwd) : void 0;
2071
2191
  if (!fallbackCommand) throw error;
2072
2192
  return {
2073
- proc: await spawnAndWait(fallbackCommand, params, defaultCwd),
2193
+ proc: await spawnAndWait(fallbackCommand, params, defaultCwd, authority),
2074
2194
  spawnCommand: fallbackCommand
2075
2195
  };
2076
2196
  }
2077
2197
  }
2078
- async function spawnAndWait(spawnCommand, params, defaultCwd) {
2198
+ async function spawnAndWait(spawnCommand, params, defaultCwd, authority) {
2079
2199
  const spawnOptions = buildTerminalSpawnOptions(spawnCommand.command, params.cwd ?? defaultCwd, params.env);
2080
- if (spawnCommand.killProcessGroup) spawnOptions.detached = true;
2200
+ if (spawnCommand.killProcessGroup || process.platform !== "win32") spawnOptions.detached = true;
2201
+ assertControlAuthority(authority);
2081
2202
  const proc = spawn(spawnCommand.command, spawnCommand.args, spawnOptions);
2082
2203
  await waitForSpawn(proc);
2083
2204
  return proc;
@@ -2104,7 +2225,7 @@ function commandPathExists(command, cwd) {
2104
2225
  async function listDescendantPids(rootPid, timeoutMs) {
2105
2226
  let output;
2106
2227
  try {
2107
- output = await runProcessListCommand(timeoutMs);
2228
+ output = await runWindowsProcessListCommand(timeoutMs);
2108
2229
  } catch {
2109
2230
  return [];
2110
2231
  }
@@ -2137,38 +2258,10 @@ function parseProcessListLine(line) {
2137
2258
  parentPid
2138
2259
  };
2139
2260
  }
2140
- async function runProcessListCommand(timeoutMs) {
2141
- if (process.platform === "win32") return await runWindowsProcessListCommand(timeoutMs);
2142
- return await runTimedExecFile("ps", ["-eo", "pid=,ppid="], { timeoutMs });
2143
- }
2144
2261
  async function rememberProcessGroupPids(terminal) {
2145
2262
  const processGroupId = terminal.process.pid;
2146
2263
  if (!terminal.killProcessGroup || !processGroupId) return;
2147
- if (process.platform === "win32") {
2148
- for (const pid of await listDescendantPids(processGroupId, terminal.processHelperTimeoutMs)) terminal.descendantPids.add(pid);
2149
- return;
2150
- }
2151
- for (const pid of await listProcessGroupPids(processGroupId, terminal.processHelperTimeoutMs)) if (pid !== processGroupId) terminal.descendantPids.add(pid);
2152
- }
2153
- async function listProcessGroupPids(processGroupId, timeoutMs) {
2154
- let output;
2155
- try {
2156
- output = await runProcessGroupListCommand(timeoutMs);
2157
- } catch {
2158
- return [];
2159
- }
2160
- const pids = [];
2161
- for (const line of output.split("\n")) {
2162
- const match = line.trim().match(/^(\d+)\s+(\d+)$/);
2163
- if (!match) continue;
2164
- const pid = Number(match[1]);
2165
- const pgid = Number(match[2]);
2166
- if (Number.isInteger(pid) && Number.isInteger(pgid) && pid > 0 && pgid === processGroupId) pids.push(pid);
2167
- }
2168
- return pids;
2169
- }
2170
- async function runProcessGroupListCommand(timeoutMs) {
2171
- return await runTimedExecFile("ps", ["-eo", "pid=,pgid="], { timeoutMs });
2264
+ for (const pid of await listDescendantPids(processGroupId, terminal.processHelperTimeoutMs)) terminal.descendantPids.add(pid);
2172
2265
  }
2173
2266
  async function runWindowsProcessListCommand(timeoutMs) {
2174
2267
  const command = ["Get-CimInstance Win32_Process |", "ForEach-Object { \"$($_.ProcessId) $($_.ParentProcessId)\" }"].join(" ");
@@ -2196,22 +2289,11 @@ async function killWindowsProcessTree(pid, signal, timeoutMs = PROCESS_HELPER_TI
2196
2289
  });
2197
2290
  } catch {}
2198
2291
  }
2199
- function sendSignal(pid, signal) {
2200
- try {
2201
- process.kill(pid, signal);
2202
- } catch {}
2203
- }
2204
- function hasLiveProcessGroup(processGroupId) {
2205
- try {
2206
- process.kill(-processGroupId, 0);
2207
- return true;
2208
- } catch {
2209
- return false;
2210
- }
2211
- }
2212
- function hasLiveTerminalProcessGroup(terminal) {
2213
- const pid = terminal.process.pid;
2214
- return Boolean(terminal.killProcessGroup && pid && process.platform !== "win32" && hasLiveProcessGroup(pid));
2292
+ async function hasLiveTerminalDescendants(terminal, timeoutMs) {
2293
+ const descendants = terminal.descendants;
2294
+ if (!descendants) return false;
2295
+ if (timeoutMs > 0) await descendants.capture(Math.min(terminal.processHelperTimeoutMs, timeoutMs));
2296
+ return descendants.hasTrackedProcesses();
2215
2297
  }
2216
2298
  function hasLivePid(pids) {
2217
2299
  for (const pid of pids) try {
@@ -2362,14 +2444,13 @@ var AcpClient = class {
2362
2444
  cancellingSessionIds = /* @__PURE__ */ new Set();
2363
2445
  permissionAbortControllers = /* @__PURE__ */ new Map();
2364
2446
  closing = false;
2447
+ closeTask;
2365
2448
  closeEpoch = 0;
2366
2449
  agentStartedAt;
2367
2450
  lastAgentExit;
2368
2451
  lastKnownPid;
2369
2452
  promptPermissionFailures = /* @__PURE__ */ new Map();
2370
2453
  pendingConnectionRequests = /* @__PURE__ */ new Set();
2371
- modelConfigIds = /* @__PURE__ */ new Map();
2372
- legacyModelSessionIds = /* @__PURE__ */ new Set();
2373
2454
  constructor(options) {
2374
2455
  this.options = {
2375
2456
  ...options,
@@ -2460,7 +2541,7 @@ var AcpClient = class {
2460
2541
  this.terminalManager.updatePermissionPolicy(this.options.permissionMode, this.options.nonInteractivePermissions);
2461
2542
  }
2462
2543
  hasLiveConnection() {
2463
- return this.connection != null && !this.connection.signal.aborted && this.agent != null && isChildProcessRunning(this.agent);
2544
+ return !this.closing && this.connection != null && !this.connection.signal.aborted && this.agent != null && isChildProcessRunning(this.agent);
2464
2545
  }
2465
2546
  hasReusableSession(sessionId) {
2466
2547
  return this.hasLiveConnection() && this.loadedSessionId === sessionId;
@@ -2473,21 +2554,27 @@ var AcpClient = class {
2473
2554
  hasUnresolvedPrompt() {
2474
2555
  return this.activePrompt !== void 0;
2475
2556
  }
2476
- endPromptElicitation(sessionId) {
2477
- const active = this.activePrompt;
2478
- if (!active || active.sessionId !== sessionId) return;
2479
- active.elicitationHandler = void 0;
2480
- active.elicitationController.abort();
2557
+ abortSessionRequests(sessionId) {
2558
+ const owners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
2559
+ const fallback = this.takePermissionAbortController(sessionId);
2560
+ for (const owner of owners) {
2561
+ owner.elicitationHandler = void 0;
2562
+ owner.elicitationController.abort();
2563
+ owner.requestController.abort();
2564
+ }
2565
+ fallback?.abort();
2481
2566
  }
2482
- async start() {
2567
+ async start(authority) {
2568
+ assertControlAuthority(authority);
2483
2569
  if (this.hasLiveConnection()) return;
2484
- if (this.connection || this.agent) await this.close();
2485
- const epoch = this.closeEpoch;
2570
+ const epoch = await this.waitForPreviousClose();
2486
2571
  const maxMessageBytes = readMaxAcpMessageBytes();
2487
2572
  const launch = await this.resolveAgentLaunchPlan();
2573
+ assertControlAuthority(authority);
2488
2574
  this.logAgentLaunch(launch);
2489
2575
  await this.ensureLaunchSupport(launch);
2490
- const { child, process: startedProcess } = await this.spawnAgentProcess(launch);
2576
+ assertControlAuthority(authority);
2577
+ const { child, process: startedProcess } = await this.spawnAgentProcess(launch, authority);
2491
2578
  if (this.closeEpoch === epoch) {
2492
2579
  this.agent = child;
2493
2580
  this.closing = false;
@@ -2510,7 +2597,7 @@ var AcpClient = class {
2510
2597
  startupFailure.dispose();
2511
2598
  throw error;
2512
2599
  }
2513
- await this.stopIfClosedDuringLaunch(epoch, child, startupFailure);
2600
+ await this.stopIfClosedDuringLaunch(epoch, child, startupFailure, authority);
2514
2601
  const input = Writable.toWeb(child.stdin);
2515
2602
  const output = Readable.toWeb(child.stdout);
2516
2603
  let connection;
@@ -2525,9 +2612,10 @@ var AcpClient = class {
2525
2612
  elicitationModes: this.options.elicitationModes
2526
2613
  });
2527
2614
  connection = this.createConnection(stream, launch, capabilities);
2528
- connection.signal.addEventListener("abort", () => {
2529
- this.recordAgentExit(child, "connection_close", child.exitCode ?? null, child.signalCode ?? null);
2530
- }, { once: true });
2615
+ const onConnectionClose = () => {
2616
+ this.handleAgentDisconnect(child, "connection_close", child.exitCode ?? null, child.signalCode ?? null);
2617
+ };
2618
+ connection.signal.addEventListener("abort", onConnectionClose, { once: true });
2531
2619
  await this.initializeAgentConnection({
2532
2620
  child,
2533
2621
  connection,
@@ -2536,6 +2624,14 @@ var AcpClient = class {
2536
2624
  launch,
2537
2625
  capabilities
2538
2626
  });
2627
+ if (connection.signal.aborted) onConnectionClose();
2628
+ }
2629
+ async waitForPreviousClose() {
2630
+ const cleanup = this.closeTask ?? (this.connection || this.agent ? this.close() : void 0);
2631
+ const epoch = this.closeEpoch;
2632
+ await cleanup;
2633
+ if (this.closeEpoch !== epoch) throw new Error("ACP client was closed while the agent was starting");
2634
+ return epoch;
2539
2635
  }
2540
2636
  async resolveAgentLaunchPlan() {
2541
2637
  const configuredCommand = resolveAgentCommandParts(this.options.agentCommand, this.options.agentArgv);
@@ -2576,7 +2672,7 @@ var AcpClient = class {
2576
2672
  this.log(`resolved system Claude Code executable: ${claudeExe}`);
2577
2673
  }
2578
2674
  }
2579
- async spawnAgentProcess(plan) {
2675
+ async spawnAgentProcess(plan, authority) {
2580
2676
  const spawnCommand = buildAgentSpawnCommand(plan.spawnCommand, plan.args, process.platform, plan.spawnOptions.env);
2581
2677
  const launch = Object.freeze({
2582
2678
  launchId: randomUUID(),
@@ -2586,6 +2682,7 @@ var AcpClient = class {
2586
2682
  cwd: this.options.cwd
2587
2683
  });
2588
2684
  await this.options.processLifecycle?.onBeforeSpawn?.(launch);
2685
+ assertControlAuthority(authority);
2589
2686
  let spawnedChild;
2590
2687
  try {
2591
2688
  spawnedChild = spawn(spawnCommand.command, spawnCommand.args, {
@@ -2634,19 +2731,19 @@ var AcpClient = class {
2634
2731
  createConnection(stream, launch, capabilities) {
2635
2732
  const app = client({ name: "acpx" }).onNotification(methods.client.session.update, async ({ params }) => {
2636
2733
  await this.handleSessionUpdate(params);
2637
- }).onNotification(methods.client.elicitation.complete, async () => {}).onRequest(methods.client.session.requestPermission, async ({ params }) => {
2638
- return await this.handlePermissionRequest(params);
2734
+ }).onNotification(methods.client.elicitation.complete, async () => {}).onRequest(methods.client.session.requestPermission, async ({ params, signal }) => {
2735
+ return await this.handlePermissionRequest(params, signal);
2639
2736
  }).onRequest(methods.client.elicitation.create, async ({ params, requestId, signal }) => {
2640
2737
  return await this.handleElicitationRequest(params, requestId, signal);
2641
2738
  });
2642
- if (capabilities.fs?.readTextFile) app.onRequest(methods.client.fs.readTextFile, async ({ params }) => {
2643
- return await this.handleReadTextFile(params);
2739
+ if (capabilities.fs?.readTextFile) app.onRequest(methods.client.fs.readTextFile, async ({ params, signal }) => {
2740
+ return await this.handleReadTextFile(params, signal);
2644
2741
  });
2645
- if (capabilities.fs?.writeTextFile) app.onRequest(methods.client.fs.writeTextFile, async ({ params }) => {
2646
- return await this.handleWriteTextFile(params);
2742
+ if (capabilities.fs?.writeTextFile) app.onRequest(methods.client.fs.writeTextFile, async ({ params, signal }) => {
2743
+ return await this.handleWriteTextFile(params, signal);
2647
2744
  });
2648
- if (capabilities.terminal) app.onRequest(methods.client.terminal.create, async ({ params }) => {
2649
- return await this.handleCreateTerminal(params);
2745
+ if (capabilities.terminal) app.onRequest(methods.client.terminal.create, async ({ params, signal }) => {
2746
+ return await this.handleCreateTerminal(params, signal);
2650
2747
  }).onRequest(methods.client.terminal.output, async ({ params }) => {
2651
2748
  return await this.terminalManager.terminalOutput(params);
2652
2749
  }).onRequest(methods.client.terminal.waitForExit, async ({ params }) => {
@@ -2707,7 +2804,8 @@ var AcpClient = class {
2707
2804
  onPromptRequestWritten: (active, owner) => this.onPromptRequestWritten(active, owner)
2708
2805
  });
2709
2806
  }
2710
- async createSession(cwd = this.options.cwd) {
2807
+ async createSession(cwd = this.options.cwd, authority) {
2808
+ assertControlAuthority(authority);
2711
2809
  const connection = this.getConnection();
2712
2810
  const { command, args } = resolveAgentCommandParts(this.options.agentCommand, this.options.agentArgv);
2713
2811
  const claudeAcp = isClaudeAcpCommand(command, args);
@@ -2718,7 +2816,7 @@ var AcpClient = class {
2718
2816
  cwd: sessionCwd,
2719
2817
  mcpServers: this.options.mcpServers ?? [],
2720
2818
  _meta: buildClaudeCodeOptionsMeta(this.options.sessionOptions, claudeAcp)
2721
- }));
2819
+ }), authority);
2722
2820
  result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise;
2723
2821
  } catch (error) {
2724
2822
  if (claudeAcp && error instanceof TimeoutError) throw new ClaudeAcpSessionCreateTimeoutError(buildClaudeAcpSessionCreateTimeoutMessage(), {
@@ -2733,7 +2831,6 @@ var AcpClient = class {
2733
2831
  configOptions,
2734
2832
  response: result
2735
2833
  });
2736
- this.rememberSessionModels(result.sessionId, models);
2737
2834
  await this.captureAgentDescendants(this.agent);
2738
2835
  return {
2739
2836
  sessionId: result.sessionId,
@@ -2751,6 +2848,7 @@ var AcpClient = class {
2751
2848
  async loadSessionWithOptions(sessionId, cwd = this.options.cwd, options = {}) {
2752
2849
  const connection = this.getConnection();
2753
2850
  const sessionCwd = await resolveAgentSessionCwd(cwd, this.options.agentCommand);
2851
+ assertControlAuthority(options.authority);
2754
2852
  const previousSuppression = this.applySessionUpdateSuppression(Boolean(options.suppressReplayUpdates));
2755
2853
  let response;
2756
2854
  try {
@@ -2758,28 +2856,27 @@ var AcpClient = class {
2758
2856
  sessionId,
2759
2857
  cwd: sessionCwd,
2760
2858
  mcpServers: this.options.mcpServers ?? []
2761
- }));
2859
+ }), options.authority);
2762
2860
  await this.waitForSessionUpdateDrain(options.replayIdleMs ?? REPLAY_IDLE_MS, options.replayDrainTimeoutMs ?? REPLAY_DRAIN_TIMEOUT_MS);
2763
2861
  } finally {
2764
2862
  this.restoreSessionUpdateSuppression(previousSuppression);
2765
2863
  }
2766
2864
  this.loadedSessionId = sessionId;
2767
2865
  const result = toReconnectedSessionResult(response);
2768
- this.updateRememberedSessionModels(sessionId, result);
2769
2866
  await this.captureAgentDescendants(this.agent);
2770
2867
  return result;
2771
2868
  }
2772
- async resumeSession(sessionId, cwd = this.options.cwd) {
2869
+ async resumeSession(sessionId, cwd = this.options.cwd, authority) {
2870
+ assertControlAuthority(authority);
2773
2871
  const connection = this.getConnection();
2774
2872
  const sessionCwd = await resolveAgentSessionCwd(cwd, this.options.agentCommand);
2775
2873
  const response = await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.resume, {
2776
2874
  sessionId,
2777
2875
  cwd: sessionCwd,
2778
2876
  mcpServers: this.options.mcpServers ?? []
2779
- }));
2877
+ }), authority);
2780
2878
  this.loadedSessionId = sessionId;
2781
2879
  const result = toReconnectedSessionResult(response);
2782
- this.updateRememberedSessionModels(sessionId, result);
2783
2880
  await this.captureAgentDescendants(this.agent);
2784
2881
  return result;
2785
2882
  }
@@ -2801,6 +2898,7 @@ var AcpClient = class {
2801
2898
  const normalizedPrompt = this.normalizePromptForAgent(prompt);
2802
2899
  const restoreConsoleError = this.options.suppressSdkConsoleErrors ? installSdkConsoleErrorSuppression() : void 0;
2803
2900
  const previousActivePrompt = this.activePrompt;
2901
+ const replacedOwners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
2804
2902
  const activePrompt = this.beginActivePrompt(sessionId, onRequestWritten, onElicitation, onPermissionRequest, authority);
2805
2903
  try {
2806
2904
  const promptPromise = this.runConnectionRequest(() => connection.agent.request(methods.agent.session.prompt, {
@@ -2809,6 +2907,7 @@ var AcpClient = class {
2809
2907
  }), authority);
2810
2908
  activePrompt.promise = promptPromise;
2811
2909
  previousActivePrompt?.elicitationController?.abort();
2910
+ for (const owner of replacedOwners) owner.requestController.abort();
2812
2911
  return this.returnPromptResponseOrPermissionFailure(sessionId, await promptPromise);
2813
2912
  } catch (error) {
2814
2913
  if (activePrompt.admissionFailure) throw activePrompt.admissionFailure.error;
@@ -2827,7 +2926,8 @@ var AcpClient = class {
2827
2926
  authority,
2828
2927
  elicitationHandler,
2829
2928
  permissionHandler,
2830
- elicitationController: new AbortController()
2929
+ elicitationController: new AbortController(),
2930
+ requestController: new AbortController()
2831
2931
  };
2832
2932
  this.activePrompt = active;
2833
2933
  this.pendingPromptOwners.push(active);
@@ -2866,6 +2966,7 @@ var AcpClient = class {
2866
2966
  this.promptPermissionFailures.delete(active.sessionId);
2867
2967
  }
2868
2968
  active.elicitationController.abort();
2969
+ active.requestController.abort();
2869
2970
  permissionController?.abort();
2870
2971
  }
2871
2972
  normalizePromptForAgent(prompt) {
@@ -2889,33 +2990,41 @@ var AcpClient = class {
2889
2990
  modeId
2890
2991
  }), authority, (error) => maybeWrapSessionControlError("session/set_mode", error, `for mode "${modeId}"`));
2891
2992
  }
2892
- async setSessionConfigOption(sessionId, configId, value, authority) {
2993
+ async setSessionConfigOption(sessionId, configId, value, models, authority) {
2994
+ assertControlAuthority(authority);
2995
+ const resolvedValue = models?.configId === configId ? this.resolveSessionModelId(value, models) : value;
2893
2996
  const connection = this.getConnection();
2894
2997
  return await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
2895
2998
  sessionId,
2896
2999
  configId,
2897
- value
3000
+ value: resolvedValue
2898
3001
  }), authority, (error) => maybeWrapSessionControlError("session/set_config_option", error, `for "${configId}"="${value}"`));
2899
3002
  }
2900
- async setSessionModel(sessionId, modelId, controlOverride, authority) {
2901
- const control = this.resolveModelControl(sessionId, controlOverride);
2902
- if (!control) throw new RequestedModelUnsupportedError(`Cannot set model "${modelId}": the ACP session did not advertise a model config option or legacy session/set_model support.`, "missing-capability");
2903
- const resolvedModelId = resolveRequestedModelId({
3003
+ async setSessionModel(sessionId, modelId, models, authority) {
3004
+ assertControlAuthority(authority);
3005
+ if (!models) throw new RequestedModelUnsupportedError(`Cannot set model "${modelId}": the ACP session did not advertise a model config option or legacy session/set_model support.`, "missing-capability");
3006
+ const resolvedModelId = this.resolveSessionModelId(modelId, models);
3007
+ return models.configId ? await this.setSessionModelThroughConfig(sessionId, resolvedModelId, models.configId, authority) : await this.setSessionModelThroughLegacyMethod(sessionId, resolvedModelId, authority);
3008
+ }
3009
+ resolveSessionModelId(modelId, models) {
3010
+ const params = {
2904
3011
  requestedModel: modelId,
2905
- models: controlOverride?.availableModels ? { availableModels: controlOverride.availableModels } : void 0,
3012
+ models,
2906
3013
  agentCommand: this.options.agentCommand
3014
+ };
3015
+ assertRequestedModelSupported({
3016
+ ...params,
3017
+ context: "apply"
2907
3018
  });
2908
- return control.kind === "config_option" ? await this.setSessionModelThroughConfig(sessionId, resolvedModelId, control.configId, authority) : await this.setSessionModelThroughLegacyMethod(sessionId, resolvedModelId, authority);
3019
+ return resolveRequestedModelId(params);
2909
3020
  }
2910
3021
  async setSessionModelThroughConfig(sessionId, modelId, configId, authority) {
2911
3022
  const connection = this.getConnection();
2912
- const response = await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
3023
+ return await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
2913
3024
  sessionId,
2914
3025
  configId,
2915
3026
  value: modelId
2916
3027
  }), authority, (error) => this.throwSessionModelError("session/set_config_option", modelId, error));
2917
- this.rememberSessionModels(sessionId, modelStateFromConfigOptions(response.configOptions));
2918
- return response;
2919
3028
  }
2920
3029
  async setSessionModelThroughLegacyMethod(sessionId, modelId, authority) {
2921
3030
  const connection = this.getConnection();
@@ -2931,39 +3040,10 @@ var AcpClient = class {
2931
3040
  const summary = acp ? formatSessionControlAcpSummary(acp) : error instanceof Error ? error.message : String(error);
2932
3041
  throw new Error(`Failed ${method} for model "${modelId}": ${summary}`, { cause: error });
2933
3042
  }
2934
- resolveModelControl(sessionId, controlOverride) {
2935
- if (controlOverride) return controlOverride.configId ? {
2936
- kind: "config_option",
2937
- configId: controlOverride.configId
2938
- } : { kind: "legacy_set_model" };
2939
- const configId = this.modelConfigIds.get(sessionId);
2940
- if (configId) return {
2941
- kind: "config_option",
2942
- configId
2943
- };
2944
- return this.legacyModelSessionIds.has(sessionId) ? { kind: "legacy_set_model" } : void 0;
2945
- }
2946
- rememberSessionModels(sessionId, models) {
2947
- if (!models) {
2948
- this.modelConfigIds.delete(sessionId);
2949
- this.legacyModelSessionIds.delete(sessionId);
2950
- return;
2951
- }
2952
- if (models.configId) {
2953
- this.modelConfigIds.set(sessionId, models.configId);
2954
- this.legacyModelSessionIds.delete(sessionId);
2955
- return;
2956
- }
2957
- this.modelConfigIds.delete(sessionId);
2958
- this.legacyModelSessionIds.add(sessionId);
2959
- }
2960
- updateRememberedSessionModels(sessionId, result) {
2961
- const explicitConfigRemoval = result.configOptionsPresent && this.modelConfigIds.has(sessionId);
2962
- if (result.models || result.legacyModelMetadataPresent || explicitConfigRemoval) this.rememberSessionModels(sessionId, result.models);
2963
- }
2964
3043
  async cancel(sessionId) {
2965
3044
  const connection = this.getConnection();
2966
- const active = this.activePrompt?.sessionId === sessionId ? this.activePrompt : void 0;
3045
+ const owners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
3046
+ const active = owners.at(-1);
2967
3047
  const cancellation = active?.cancelPromise ?? this.runConnectionRequest(() => connection.agent.notify(methods.agent.session.cancel, { sessionId })).catch((error) => {
2968
3048
  if (active?.cancelPromise === cancellation) active.cancelPromise = void 0;
2969
3049
  throw error;
@@ -2972,19 +3052,20 @@ var AcpClient = class {
2972
3052
  if (active) {
2973
3053
  active.cancelPromise = cancellation;
2974
3054
  this.cancellingSessionIds.add(sessionId);
2975
- active.elicitationController?.abort();
2976
3055
  } else this.cancellingSessionIds.delete(sessionId);
3056
+ for (const owner of owners) {
3057
+ owner.elicitationController.abort();
3058
+ owner.requestController.abort();
3059
+ }
2977
3060
  permissionController?.abort();
2978
3061
  await cancellation;
2979
3062
  }
2980
3063
  async closeSession(sessionId) {
2981
3064
  const connection = this.getConnection();
2982
3065
  this.cancellingSessionIds.add(sessionId);
2983
- if (this.activePrompt?.sessionId === sessionId) this.activePrompt.elicitationController?.abort();
3066
+ this.abortSessionRequests(sessionId);
2984
3067
  await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.close, { sessionId }));
2985
3068
  if (this.loadedSessionId === sessionId) this.loadedSessionId = void 0;
2986
- this.modelConfigIds.delete(sessionId);
2987
- this.legacyModelSessionIds.delete(sessionId);
2988
3069
  }
2989
3070
  async listSessions(params = {}) {
2990
3071
  const connection = this.getConnection();
@@ -3018,15 +3099,23 @@ var AcpClient = class {
3018
3099
  if (timer) clearTimeout(timer);
3019
3100
  }
3020
3101
  }
3021
- async close() {
3102
+ close() {
3022
3103
  this.closing = true;
3023
3104
  this.closeEpoch += 1;
3105
+ if (this.closeTask) return this.closeTask;
3106
+ this.closeTask = Promise.resolve().then(() => this.finishClose()).finally(() => {
3107
+ this.closeTask = void 0;
3108
+ });
3109
+ return this.closeTask;
3110
+ }
3111
+ async finishClose() {
3112
+ const permissionControllers = [...this.permissionAbortControllers.values()];
3113
+ this.permissionAbortControllers.clear();
3114
+ const owners = [...this.pendingPromptOwners];
3024
3115
  this.abortActiveElicitation();
3025
- await this.terminalManager.shutdown();
3026
- const agent = this.agent;
3027
- if (agent) await this.terminateAgentProcess(agent);
3028
- this.closeConnection();
3029
- if (this.pendingConnectionRequests.size > 0) this.rejectPendingConnectionRequests(this.lastAgentExit ? new AgentDisconnectedError(this.lastAgentExit.reason, this.lastAgentExit.exitCode, this.lastAgentExit.signal, { outputAlreadyEmitted: Boolean(this.activePrompt) }) : new AgentDisconnectedError("connection_close", null, null, { outputAlreadyEmitted: Boolean(this.activePrompt) }));
3116
+ for (const owner of owners) owner.requestController.abort();
3117
+ for (const controller of permissionControllers) controller.abort();
3118
+ await this.retireNativeResources();
3030
3119
  this.sessionUpdateChain = Promise.resolve();
3031
3120
  this.observedSessionUpdates = 0;
3032
3121
  this.processedSessionUpdates = 0;
@@ -3039,16 +3128,35 @@ var AcpClient = class {
3039
3128
  this.permissionAbortControllers.clear();
3040
3129
  this.promptPermissionFailures.clear();
3041
3130
  this.loadedSessionId = void 0;
3042
- this.modelConfigIds.clear();
3043
- this.legacyModelSessionIds.clear();
3044
3131
  this.initResult = void 0;
3045
3132
  this.connection = void 0;
3046
3133
  this.agent = void 0;
3047
3134
  }
3048
- async stopIfClosedDuringLaunch(epoch, child, startupFailure) {
3135
+ async retireNativeResources() {
3136
+ const agent = this.agent;
3137
+ const failures = [];
3138
+ try {
3139
+ await this.terminalManager.shutdown();
3140
+ } catch (error) {
3141
+ failures.push(error);
3142
+ }
3143
+ try {
3144
+ if (agent) await this.terminateAgentProcess(agent);
3145
+ } catch (error) {
3146
+ failures.push(error);
3147
+ }
3148
+ try {
3149
+ this.closeConnection();
3150
+ } finally {
3151
+ this.rejectPendingConnectionRequests(this.lastAgentExit ? new AgentDisconnectedError(this.lastAgentExit.reason, this.lastAgentExit.exitCode, this.lastAgentExit.signal, { outputAlreadyEmitted: Boolean(this.activePrompt) }) : new AgentDisconnectedError("connection_close", null, null, { outputAlreadyEmitted: Boolean(this.activePrompt) }));
3152
+ }
3153
+ if (failures.length > 0) throw new AggregateError(failures, "ACP client cleanup failed", { cause: failures[0] });
3154
+ }
3155
+ async stopIfClosedDuringLaunch(epoch, child, startupFailure, authority) {
3049
3156
  if (this.closeEpoch === epoch) return;
3050
3157
  startupFailure.dispose();
3051
3158
  await this.terminateAgentProcess(child);
3159
+ assertControlAuthority(authority);
3052
3160
  throw new Error("ACP client was closed while the agent was starting");
3053
3161
  }
3054
3162
  abortActiveElicitation() {
@@ -3255,27 +3363,41 @@ var AcpClient = class {
3255
3363
  await connection.agent.request(methods.agent.authenticate, { methodId: selected.methodId });
3256
3364
  this.log(`authenticated with method ${selected.methodId} (${selected.source})`);
3257
3365
  }
3258
- async handlePermissionRequest(params) {
3259
- if (this.cancellingSessionIds.has(params.sessionId)) return cancelledPermissionResponse();
3260
- if (this.initResult?.agentInfo?.name === "antigravity-acp" && params.toolCall.toolCallId.startsWith("interaction_")) return this.handleModePermissionError(params.sessionId, new PermissionPromptUnavailableError("Antigravity requested a user answer. acpx cannot answer Antigravity interaction questions; continue in an interactive client.")).response;
3261
- const hostResponse = await this.tryHandlePermissionRequestWithHost(params);
3262
- if (hostResponse) return hostResponse;
3263
- const { response, recorded } = await this.resolvePermissionRequestFromMode(params);
3264
- if (!recorded) {
3265
- const decision = classifyPermissionDecision(params, response);
3266
- this.recordPermissionDecision(decision);
3366
+ async handlePermissionRequest(params, requestSignal) {
3367
+ const owner = this.captureDelegatedRequestOwner(params.sessionId, requestSignal);
3368
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3369
+ if (!this.hasPermissionOwner(params.sessionId)) return this.finishPermissionRequest(params, owner, cancelledPermissionResponse());
3370
+ if (this.initResult?.agentInfo?.name === "antigravity-acp" && params.toolCall.toolCallId.startsWith("interaction_")) {
3371
+ const response = this.handleModePermissionError(params.sessionId, new PermissionPromptUnavailableError("Antigravity requested a user answer. acpx cannot answer Antigravity interaction questions; continue in an interactive client."));
3372
+ return this.finishPermissionRequest(params, owner, response);
3267
3373
  }
3268
- return response;
3374
+ const response = await this.tryHandlePermissionRequestWithHost(params, owner) ?? await this.resolvePermissionRequestFromMode(params, owner);
3375
+ return this.finishPermissionRequest(params, owner, response);
3376
+ }
3377
+ finishPermissionRequest(params, owner, response) {
3378
+ const result = this.isDelegatedRequestActive(owner) ? response : cancelledPermissionResponse();
3379
+ this.recordPermissionDecision(classifyPermissionDecision(params, result));
3380
+ return result;
3381
+ }
3382
+ hasPermissionOwner(sessionId) {
3383
+ return !this.pendingPromptOwners.some((owner) => owner.permissionHandler !== void 0) || this.pendingPromptOwners.some((owner) => owner.sessionId === sessionId);
3269
3384
  }
3270
3385
  async handleElicitationRequest(request, requestId, requestSignal) {
3271
3386
  const resolved = this.resolveElicitationOwner(request);
3272
3387
  if ("response" in resolved) return resolved.response;
3273
3388
  const { active, handler } = resolved.owner;
3274
- const signal = AbortSignal.any([requestSignal, active.elicitationController.signal]);
3275
- const outcome = await raceWithAbort(signal, Promise.resolve().then(async () => await handler(request, {
3276
- requestId,
3277
- signal
3278
- })).then((response) => ({
3389
+ const signal = AbortSignal.any([
3390
+ requestSignal,
3391
+ active.elicitationController.signal,
3392
+ active.authority?.signal
3393
+ ].filter((candidate) => candidate !== void 0));
3394
+ const outcome = await raceWithAbort(signal, Promise.resolve().then(async () => {
3395
+ signal.throwIfAborted();
3396
+ return await handler(request, {
3397
+ requestId,
3398
+ signal
3399
+ });
3400
+ }).then((response) => ({
3279
3401
  kind: "response",
3280
3402
  response
3281
3403
  }), (error) => ({
@@ -3310,62 +3432,65 @@ var AcpClient = class {
3310
3432
  isElicitationSessionCancelling(sessionId) {
3311
3433
  return this.closing || this.cancellingSessionIds.has(sessionId);
3312
3434
  }
3313
- resolvePermissionOwner(sessionId) {
3314
- const active = this.activePrompt;
3315
- const handler = active?.permissionHandler ?? this.options.onPermissionRequest;
3316
- if (!handler) return;
3317
- const signal = this.cancellationSignalForSession(sessionId);
3318
- if (!active?.permissionHandler) return {
3319
- handler,
3320
- signal
3321
- };
3435
+ captureDelegatedRequestOwner(sessionId, requestSignal) {
3436
+ const active = this.pendingPromptOwners.findLast((owner) => owner.sessionId === sessionId);
3437
+ const signal = AbortSignal.any([
3438
+ active?.requestController.signal ?? this.cancellationSignalForSession(sessionId),
3439
+ active?.authority?.signal,
3440
+ requestSignal
3441
+ ].filter((candidate) => candidate !== void 0));
3442
+ const epoch = this.closeEpoch;
3322
3443
  return {
3323
- handler,
3324
- signal: active.sessionId === sessionId ? AbortSignal.any([signal, active.elicitationController.signal]) : AbortSignal.abort()
3444
+ signal,
3445
+ permissionHandler: active?.permissionHandler ?? this.options.onPermissionRequest,
3446
+ assertActive: () => {
3447
+ if (this.closing || this.closeEpoch !== epoch || this.cancellingSessionIds.has(sessionId)) throw RequestError.requestCancelled();
3448
+ }
3325
3449
  };
3326
3450
  }
3327
- async tryHandlePermissionRequestWithHost(params) {
3328
- const owner = this.resolvePermissionOwner(params.sessionId);
3329
- if (!owner) return;
3330
- const { handler, signal } = owner;
3331
- if (signal.aborted) return this.hostPermissionDecisionResponse(params, signal, void 0);
3451
+ isDelegatedRequestActive(owner) {
3452
+ try {
3453
+ assertControlAuthority(owner);
3454
+ return true;
3455
+ } catch {
3456
+ return false;
3457
+ }
3458
+ }
3459
+ async tryHandlePermissionRequestWithHost(params, owner) {
3460
+ const handler = owner.permissionHandler;
3461
+ if (!handler) return;
3462
+ const { signal } = owner;
3463
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3332
3464
  try {
3333
3465
  const decision = await raceWithAbort(signal, handler({
3334
3466
  sessionId: params.sessionId,
3335
3467
  raw: params,
3336
3468
  inferredKind: inferToolKind(params)
3337
3469
  }, { signal }));
3338
- return this.hostPermissionDecisionResponse(params, signal, decision);
3470
+ return this.hostPermissionDecisionResponse(params, owner, decision);
3339
3471
  } catch (error) {
3340
- return this.hostPermissionErrorResponse(params, signal, error);
3472
+ return this.hostPermissionErrorResponse(owner, error);
3341
3473
  }
3342
3474
  }
3343
- hostPermissionDecisionResponse(params, signal, decision) {
3344
- if (signal.aborted || this.cancellingSessionIds.has(params.sessionId)) {
3345
- this.recordPermissionDecision("cancelled");
3346
- return cancelledPermissionResponse();
3347
- }
3475
+ hostPermissionDecisionResponse(params, owner, decision) {
3476
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3348
3477
  if (!decision) return;
3349
3478
  const response = decisionToResponse(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), decision);
3350
- this.recordPermissionDecision(classifyPermissionDecision(params, response));
3351
3479
  return decision.outcome === "cancel" ? response : this.explainPermissionRefusal(params, response);
3352
3480
  }
3353
- hostPermissionErrorResponse(params, signal, error) {
3354
- if (signal.aborted || this.cancellingSessionIds.has(params.sessionId)) {
3355
- this.recordPermissionDecision("cancelled");
3356
- return cancelledPermissionResponse();
3357
- }
3481
+ hostPermissionErrorResponse(owner, error) {
3482
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3358
3483
  this.log(`onPermissionRequest threw, falling through to mode-based resolver: ${error instanceof Error ? error.message : String(error)}`);
3359
3484
  }
3360
- async resolvePermissionRequestFromMode(params) {
3485
+ async resolvePermissionRequestFromMode(params, owner) {
3361
3486
  try {
3362
- const result = await resolvePermissionRequestWithDetails(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), this.options.permissionMode, this.options.nonInteractivePermissions ?? "deny", this.options.permissionPolicy);
3487
+ assertControlAuthority(owner);
3488
+ const result = await resolvePermissionRequestWithDetails(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), this.options.permissionMode, this.options.nonInteractivePermissions ?? "deny", this.options.permissionPolicy, owner.signal);
3489
+ assertControlAuthority(owner);
3363
3490
  this.emitPermissionEscalation(result.escalation);
3364
- return {
3365
- response: this.explainPermissionRefusal(params, result.response),
3366
- recorded: false
3367
- };
3491
+ return this.explainPermissionRefusal(params, result.response);
3368
3492
  } catch (error) {
3493
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3369
3494
  return this.handleModePermissionError(params.sessionId, error);
3370
3495
  }
3371
3496
  }
@@ -3388,16 +3513,12 @@ var AcpClient = class {
3388
3513
  handleModePermissionError(sessionId, error) {
3389
3514
  if (!(error instanceof PermissionPromptUnavailableError)) throw error;
3390
3515
  this.notePromptPermissionFailure(sessionId, error);
3391
- this.recordPermissionDecision("cancelled");
3392
- return {
3393
- response: cancelledPermissionResponse(),
3394
- recorded: true
3395
- };
3516
+ return cancelledPermissionResponse();
3396
3517
  }
3397
3518
  attachAgentLifecycleObservers(child, startedProcess, exitNotificationBarrier) {
3398
3519
  const onExit = (exitCode, signal) => {
3399
3520
  const exitedAt = isoNow$2();
3400
- this.recordAgentExit(child, "process_exit", exitCode, signal);
3521
+ this.handleAgentDisconnect(child, "process_exit", exitCode, signal);
3401
3522
  this.terminateAgentProcess(child);
3402
3523
  exitNotificationBarrier.then(() => {
3403
3524
  this.notifyProcessExit(startedProcess, exitCode, signal, exitedAt);
@@ -3409,10 +3530,17 @@ var AcpClient = class {
3409
3530
  onExit(child.exitCode, child.signalCode);
3410
3531
  }
3411
3532
  child.once("close", (exitCode, signal) => {
3412
- this.recordAgentExit(child, "process_close", exitCode, signal);
3533
+ this.handleAgentDisconnect(child, "process_close", exitCode, signal);
3413
3534
  });
3414
3535
  child.stdout.once("close", () => {
3415
- this.recordAgentExit(child, "pipe_close", child.exitCode ?? null, child.signalCode ?? null);
3536
+ this.handleAgentDisconnect(child, "pipe_close", child.exitCode ?? null, child.signalCode ?? null);
3537
+ });
3538
+ }
3539
+ handleAgentDisconnect(child, reason, exitCode, signal) {
3540
+ this.recordAgentExit(child, reason, exitCode, signal);
3541
+ if (this.agent !== child || this.closing || !this.connection) return;
3542
+ this.close().catch((error) => {
3543
+ this.log(`disconnected agent cleanup failed: ${String(error)}`);
3416
3544
  });
3417
3545
  }
3418
3546
  notifyProcessSpawnFailure(launch, error) {
@@ -3519,28 +3647,30 @@ var AcpClient = class {
3519
3647
  pending.reject(error);
3520
3648
  }
3521
3649
  }
3522
- async handleReadTextFile(params) {
3650
+ async handleReadTextFile(params, requestSignal) {
3523
3651
  try {
3524
- return await this.filesystem.readTextFile(params);
3652
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.filesystem.readTextFile(params, owner));
3525
3653
  } catch (error) {
3526
- this.recordPermissionError(params.sessionId, error);
3527
3654
  if (error instanceof FsSafeError && error.code === "not-found" || error instanceof Error && "code" in error && error.code === "ENOENT") throw RequestError.resourceNotFound(pathToFileURL(params.path).href);
3528
3655
  throw error;
3529
3656
  }
3530
3657
  }
3531
- async handleWriteTextFile(params) {
3532
- try {
3533
- return await this.filesystem.writeTextFile(params);
3534
- } catch (error) {
3535
- this.recordPermissionError(params.sessionId, error);
3536
- throw error;
3537
- }
3658
+ async handleWriteTextFile(params, requestSignal) {
3659
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.filesystem.writeTextFile(params, owner));
3660
+ }
3661
+ async handleCreateTerminal(params, requestSignal) {
3662
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.terminalManager.createTerminal(params, owner));
3538
3663
  }
3539
- async handleCreateTerminal(params) {
3664
+ async runDelegatedOperation(sessionId, requestSignal, run) {
3665
+ const owner = this.captureDelegatedRequestOwner(sessionId, requestSignal);
3540
3666
  try {
3541
- return await this.terminalManager.createTerminal(params);
3667
+ assertControlAuthority(owner);
3668
+ const result = await run(owner);
3669
+ assertControlAuthority(owner);
3670
+ return result;
3542
3671
  } catch (error) {
3543
- this.recordPermissionError(params.sessionId, error);
3672
+ if (!this.isDelegatedRequestActive(owner)) throw RequestError.requestCancelled();
3673
+ this.recordPermissionError(sessionId, error);
3544
3674
  throw error;
3545
3675
  }
3546
3676
  }
@@ -3774,7 +3904,6 @@ function ensureAgentMessage(conversation) {
3774
3904
  return created;
3775
3905
  }
3776
3906
  function appendAgentText(agent, text) {
3777
- if (!text.trim()) return;
3778
3907
  const last = agent.content.at(-1);
3779
3908
  if (last && isAgentTextContent(last)) {
3780
3909
  last.Text = trimRuntimeText(`${last.Text}${text}`, MAX_RUNTIME_AGENT_TEXT_CHARS);
@@ -3784,7 +3913,6 @@ function appendAgentText(agent, text) {
3784
3913
  agent.content.push(next);
3785
3914
  }
3786
3915
  function appendAgentThinking(agent, text) {
3787
- if (!text.trim()) return;
3788
3916
  const last = agent.content.at(-1);
3789
3917
  if (last && isAgentThinkingContent(last)) {
3790
3918
  last.Thinking.text = trimRuntimeText(`${last.Thinking.text}${text}`, MAX_RUNTIME_THINKING_CHARS);
@@ -3841,8 +3969,7 @@ function ensureToolUseContent(agent, toolCallId) {
3841
3969
  return created;
3842
3970
  }
3843
3971
  function upsertToolResult(agent, toolCallId, patch) {
3844
- const existing = agent.tool_results[toolCallId];
3845
- const fallback = existingToolResultValues(existing);
3972
+ const fallback = existingToolResultValues(hasOwn(agent.tool_results, toolCallId) ? agent.tool_results[toolCallId] : void 0);
3846
3973
  const next = {
3847
3974
  tool_use_id: toolCallId,
3848
3975
  tool_name: patch.tool_name ?? fallback.tool_name,
@@ -3850,7 +3977,12 @@ function upsertToolResult(agent, toolCallId, patch) {
3850
3977
  content: patch.content ?? fallback.content,
3851
3978
  output: patch.output ?? fallback.output
3852
3979
  };
3853
- agent.tool_results[toolCallId] = next;
3980
+ Object.defineProperty(agent.tool_results, toolCallId, {
3981
+ value: next,
3982
+ writable: true,
3983
+ enumerable: true,
3984
+ configurable: true
3985
+ });
3854
3986
  }
3855
3987
  function existingToolResultValues(existing) {
3856
3988
  if (existing) return existing;
@@ -3891,7 +4023,7 @@ function applyToolResultUpdate(agent, tool, update) {
3891
4023
  const output = hasOwn(update, "rawOutput") ? deepClone(update.rawOutput) : void 0;
3892
4024
  upsertToolResult(agent, update.toolCallId, {
3893
4025
  tool_name: tool.name,
3894
- is_error: statusIndicatesError(status),
4026
+ is_error: status === void 0 ? void 0 : statusIndicatesError(status),
3895
4027
  content: output === void 0 ? void 0 : toToolResultContent(output),
3896
4028
  output
3897
4029
  });
@@ -4037,20 +4169,6 @@ function recordPromptSubmission(conversation, prompt, timestamp = isoNow(), mess
4037
4169
  trimConversationForRuntime(conversation);
4038
4170
  return promptMessageId;
4039
4171
  }
4040
- function agentMessageHasObservedReply(message) {
4041
- return message.content.length > 0 || Object.keys(message.tool_results).length > 0;
4042
- }
4043
- function hasAgentReplyAfterPrompt(conversation, promptMessageId) {
4044
- let sawPrompt = false;
4045
- for (const message of conversation.messages) {
4046
- if (!sawPrompt) {
4047
- if (isUserMessage(message) && message.User.id === promptMessageId) sawPrompt = true;
4048
- continue;
4049
- }
4050
- if (isAgentMessage(message) && agentMessageHasObservedReply(message.Agent)) return true;
4051
- }
4052
- return false;
4053
- }
4054
4172
  function recordSessionUpdate(conversation, state, notification, timestamp = isoNow(), userMessageId) {
4055
4173
  const acpx = ensureAcpxState$1(state);
4056
4174
  const update = notification.update;
@@ -4331,10 +4449,7 @@ function getDesiredModeId(state) {
4331
4449
  function getDesiredConfigOptions(state) {
4332
4450
  const desired = state?.desired_config_options;
4333
4451
  if (!desired) return {};
4334
- return Object.fromEntries(Object.entries(desired).flatMap(([configId, value]) => {
4335
- const normalizedConfigId = normalizeModeId(configId);
4336
- return normalizedConfigId && typeof value === "string" ? [[normalizedConfigId, value]] : [];
4337
- }));
4452
+ return Object.fromEntries(Object.entries(desired).filter(([, value]) => typeof value === "string"));
4338
4453
  }
4339
4454
  function setDesiredModeId(record, modeId) {
4340
4455
  const acpx = ensureAcpxState(record.acpx);
@@ -4344,10 +4459,9 @@ function setDesiredModeId(record, modeId) {
4344
4459
  record.acpx = acpx;
4345
4460
  }
4346
4461
  function clearDesiredConfigOption(state, configId) {
4347
- const normalizedConfigId = normalizeModeId(configId);
4348
- if (!normalizedConfigId || !state.desired_config_options) return;
4462
+ if (typeof configId !== "string" || !state.desired_config_options) return;
4349
4463
  const desired = { ...state.desired_config_options };
4350
- delete desired[normalizedConfigId];
4464
+ delete desired[configId];
4351
4465
  if (Object.keys(desired).length > 0) state.desired_config_options = desired;
4352
4466
  else delete state.desired_config_options;
4353
4467
  }
@@ -4386,7 +4500,7 @@ async function applyRequestedModelIfAdvertised(params) {
4386
4500
  if (params.models.currentModelId === requestedModel) return { applied: true };
4387
4501
  return {
4388
4502
  applied: true,
4389
- response: await withTimeout(params.client.setSessionModel(params.sessionId, requestedModel, params.models), params.timeoutMs)
4503
+ response: await withTimeout(params.client.setSessionModel(params.sessionId, requestedModel, params.models, params.authority), params.timeoutMs)
4390
4504
  };
4391
4505
  }
4392
4506
  //#endregion
@@ -4411,9 +4525,9 @@ function applyAcceptedConfigOptions(state, response) {
4411
4525
  if (!response) return next;
4412
4526
  applyConfigOptionsModelState(next, response.configOptions);
4413
4527
  if (!next.desired_config_options) return next;
4414
- const desired = {};
4415
- for (const option of response.configOptions) if (typeof option.currentValue === "string" && Object.hasOwn(next.desired_config_options, option.id)) desired[option.id] = option.currentValue;
4416
- if (Object.keys(desired).length > 0) next.desired_config_options = desired;
4528
+ const desiredEntries = [];
4529
+ 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]);
4530
+ if (desiredEntries.length > 0) next.desired_config_options = Object.fromEntries(desiredEntries);
4417
4531
  else delete next.desired_config_options;
4418
4532
  return next;
4419
4533
  }
@@ -4431,8 +4545,7 @@ function applyModelSelection(state, modelId, response) {
4431
4545
  function applyConfigOptionSelection(state, configId, value, response, modelConfigId = advertisedModelState(state)?.configId) {
4432
4546
  if (configId === modelConfigId || configId === modelStateFromConfigOptions(response.configOptions)?.configId) return applyModelSelection(state, value, response);
4433
4547
  const next = cloneSessionAcpxState(state) ?? {};
4434
- if (configId === "mode") next.desired_mode_id = value;
4435
- else next.desired_config_options = {
4548
+ next.desired_config_options = {
4436
4549
  ...next.desired_config_options,
4437
4550
  [configId]: value
4438
4551
  };
@@ -4443,6 +4556,7 @@ function applyConfigOptionSelection(state, configId, value, response, modelConfi
4443
4556
  const LOCK_RETRY_MS = 15;
4444
4557
  const INCOMPLETE_RESERVATION_GRACE_MS = 15e3;
4445
4558
  let lastCreatedAt = 0;
4559
+ const localTurns = /* @__PURE__ */ new Map();
4446
4560
  async function readLock(filePath) {
4447
4561
  try {
4448
4562
  const stat = await fs$1.lstat(filePath, { bigint: true });
@@ -4456,14 +4570,37 @@ async function readLock(filePath) {
4456
4570
  throw error;
4457
4571
  }
4458
4572
  }
4459
- function lockPid(payload) {
4573
+ function parseLock(payload) {
4460
4574
  try {
4461
- const pid = JSON.parse(payload)?.pid;
4462
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
4575
+ return JSON.parse(payload);
4463
4576
  } catch {
4464
4577
  return;
4465
4578
  }
4466
4579
  }
4580
+ async function tryAcquireGuard(filePath, owner, signal) {
4581
+ try {
4582
+ return await acquireFileLock(filePath, {
4583
+ managerKey: "acpx.session-turn",
4584
+ lockPath: `${filePath}.guard`,
4585
+ staleMs: Infinity,
4586
+ timeoutMs: LOCK_RETRY_MS,
4587
+ retry: {
4588
+ retries: 8,
4589
+ minTimeout: 1,
4590
+ maxTimeout: 2,
4591
+ factor: 1,
4592
+ randomize: false
4593
+ },
4594
+ staleRecovery: "remove-if-unchanged",
4595
+ payload: () => owner.payload,
4596
+ shouldReclaim: ({ payload }) => owner.hasExited(payload, false, signal),
4597
+ shouldRemoveStaleLock: ({ payload }) => owner.hasExited(payload, true, signal)
4598
+ });
4599
+ } catch (error) {
4600
+ if (error.code === "file_lock_timeout") return;
4601
+ throw error;
4602
+ }
4603
+ }
4467
4604
  async function removeObservedLock(filePath, observed) {
4468
4605
  const current = await readLock(filePath);
4469
4606
  if (!current) return true;
@@ -4473,83 +4610,198 @@ async function removeObservedLock(filePath, observed) {
4473
4610
  });
4474
4611
  return true;
4475
4612
  }
4476
- async function recoverAbandonedLock(filePath) {
4613
+ async function recoverAbandonedLock(filePath, owner, signal) {
4477
4614
  const observed = await readLock(filePath);
4478
- if (!observed) return;
4479
- const pid = lockPid(observed.payload);
4480
- if (pid === process.pid || isProcessAlive(pid)) return;
4481
- if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return;
4482
- if (await removeObservedLock(filePath, observed)) incrementPerfCounter("session.events.stale_lock_recovered");
4615
+ if (!observed) return true;
4616
+ const payload = parseLock(observed.payload);
4617
+ const pid = lockOwnerPid(payload);
4618
+ if (pid && (!await owner.hasExited(payload, false, signal) || !await owner.hasExited(payload, true, signal))) return false;
4619
+ if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return false;
4620
+ if (await removeObservedLock(filePath, observed)) {
4621
+ incrementPerfCounter("session.events.stale_lock_recovered");
4622
+ return true;
4623
+ }
4624
+ return false;
4483
4625
  }
4484
4626
  async function publishLock(filePath, payload, signal) {
4485
- try {
4486
- await withTempFile({
4487
- rootDir: path.dirname(filePath),
4488
- prefix: "session-turn",
4489
- fileName: "lock"
4490
- }, async (temporaryPath) => {
4491
- await fs$1.writeFile(temporaryPath, payload, {
4627
+ await withTempFile({
4628
+ rootDir: path.dirname(filePath),
4629
+ prefix: "session-turn",
4630
+ fileName: "lock"
4631
+ }, async (temporaryPath) => {
4632
+ await fs$1.writeFile(temporaryPath, payload, {
4633
+ encoding: "utf8",
4634
+ flag: "wx",
4635
+ mode: 384
4636
+ });
4637
+ signal?.throwIfAborted();
4638
+ try {
4639
+ await fs$1.link(temporaryPath, filePath);
4640
+ } catch (error) {
4641
+ if (!isHardlinkFallbackError(error)) throw error;
4642
+ await fs$1.writeFile(filePath, payload, {
4492
4643
  encoding: "utf8",
4493
4644
  flag: "wx",
4494
4645
  mode: 384
4495
4646
  });
4496
- signal?.throwIfAborted();
4497
- try {
4498
- await fs$1.link(temporaryPath, filePath);
4499
- } catch (error) {
4500
- if (!isHardlinkFallbackError(error)) throw error;
4501
- await fs$1.writeFile(filePath, payload, {
4502
- encoding: "utf8",
4503
- flag: "wx",
4504
- mode: 384
4505
- });
4506
- }
4507
- });
4508
- } catch (error) {
4509
- await removePublishedLock(filePath, payload).catch(() => {});
4510
- throw error;
4511
- }
4647
+ }
4648
+ });
4512
4649
  }
4513
4650
  async function removePublishedLock(filePath, payload) {
4514
4651
  const current = await readLock(filePath);
4515
4652
  if (current?.payload === payload) await removeObservedLock(filePath, current);
4516
4653
  }
4517
- async function observePublishedLock(filePath, payload, signal) {
4654
+ async function rejectAcquisition(filePath, receipt, error) {
4655
+ const failures = [error];
4656
+ await disposeTurn(filePath, receipt).catch((cleanupError) => {
4657
+ failures.push(...cleanupError instanceof AggregateError ? cleanupError.errors : [cleanupError]);
4658
+ });
4659
+ if (failures.length > 1) throw new AggregateError(failures, "Session turn acquisition and cleanup failed");
4660
+ throw error;
4661
+ }
4662
+ async function releaseTurnGuard(receipt) {
4663
+ await receipt.guard?.release();
4664
+ receipt.guard = void 0;
4665
+ }
4666
+ async function removeTurnMarker(filePath, receipt) {
4667
+ if (!receipt.removeMarker) return true;
4668
+ if (receipt.guard && !await receipt.guard.verifyStillHeld()) await releaseTurnGuard(receipt);
4669
+ receipt.guard ??= await tryAcquireGuard(filePath, receipt.owner);
4670
+ if (!receipt.guard) return false;
4671
+ await receipt.removeMarker();
4672
+ receipt.removeMarker = void 0;
4673
+ return true;
4674
+ }
4675
+ async function settleTurn(filePath, receipt) {
4676
+ if (receipt.settled) return true;
4677
+ const pending = receipt.pending ??= (async () => {
4678
+ receipt.failed = false;
4679
+ const failures = [];
4680
+ try {
4681
+ if (!await removeTurnMarker(filePath, receipt)) return false;
4682
+ } catch (error) {
4683
+ failures.push(error);
4684
+ }
4685
+ if (failures.length === 0 || !receipt.admitted) try {
4686
+ await releaseTurnGuard(receipt);
4687
+ } catch (error) {
4688
+ failures.push(error);
4689
+ }
4690
+ if (failures.length > 1) throw new AggregateError(failures, "Session turn cleanup failed");
4691
+ if (failures.length === 1) throw failures[0];
4692
+ receipt.settled = true;
4693
+ localTurns.delete(filePath);
4694
+ return true;
4695
+ })();
4696
+ try {
4697
+ return await pending;
4698
+ } finally {
4699
+ if (receipt.pending === pending) {
4700
+ receipt.pending = void 0;
4701
+ receipt.failed = !receipt.settled;
4702
+ }
4703
+ }
4704
+ }
4705
+ async function disposeTurn(filePath, receipt) {
4706
+ if (!await settleTurn(filePath, receipt)) throw new Error(`Session turn cleanup is waiting for its guard: ${filePath}`);
4707
+ }
4708
+ async function tryPublishLock(filePath, payload, owner, signal) {
4709
+ for (;;) try {
4710
+ await publishLock(filePath, payload, signal);
4711
+ return true;
4712
+ } catch (error) {
4713
+ if (error.code !== "EEXIST") throw error;
4714
+ if (!await recoverAbandonedLock(filePath, owner, signal)) return false;
4715
+ }
4716
+ }
4717
+ async function tryAcquireTurnCleanup(filePath, payload, owner, signal) {
4718
+ const previous = localTurns.get(filePath);
4719
+ if (previous) {
4720
+ if (previous.failed) await settleTurn(filePath, previous);
4721
+ return;
4722
+ }
4723
+ const receipt = {
4724
+ owner,
4725
+ removeMarker: async () => await removePublishedLock(filePath, payload),
4726
+ admitted: false,
4727
+ failed: false,
4728
+ settled: false
4729
+ };
4730
+ localTurns.set(filePath, receipt);
4731
+ try {
4732
+ receipt.guard = await tryAcquireGuard(filePath, owner, signal);
4733
+ } catch (error) {
4734
+ localTurns.delete(filePath);
4735
+ throw error;
4736
+ }
4737
+ if (!receipt.guard) {
4738
+ localTurns.delete(filePath);
4739
+ return;
4740
+ }
4741
+ return receipt;
4742
+ }
4743
+ async function tryAcquireTurn(filePath, payload, owner, signal) {
4744
+ const receipt = await tryAcquireTurnCleanup(filePath, payload, owner, signal);
4745
+ if (!receipt) return;
4518
4746
  try {
4747
+ signal?.throwIfAborted();
4748
+ if (!await tryPublishLock(filePath, payload, owner, signal)) {
4749
+ await disposeTurn(filePath, receipt);
4750
+ return;
4751
+ }
4519
4752
  const observed = await readLock(filePath);
4520
4753
  if (observed?.payload !== payload) throw new Error(`Session turn ownership changed before admission: ${filePath}`);
4521
4754
  signal?.throwIfAborted();
4522
- return { [Symbol.asyncDispose]: async () => {
4523
- await removeObservedLock(filePath, observed);
4524
- } };
4755
+ receipt.admitted = true;
4756
+ receipt.removeMarker = async () => await removeObservedLock(filePath, observed);
4757
+ return { [Symbol.asyncDispose]: async () => await disposeTurn(filePath, receipt) };
4525
4758
  } catch (error) {
4526
- await removePublishedLock(filePath, payload).catch(() => {});
4527
- throw error;
4759
+ return await rejectAcquisition(filePath, receipt, error);
4528
4760
  }
4529
4761
  }
4530
4762
  async function acquireSessionTurn(recordId, signal) {
4531
- const filePath = sessionEventLockPath(recordId);
4532
- await fs$1.mkdir(path.dirname(filePath), {
4763
+ const requestedPath = sessionEventLockPath(recordId);
4764
+ await fs$1.mkdir(path.dirname(requestedPath), {
4533
4765
  recursive: true,
4534
4766
  mode: 448
4535
4767
  });
4768
+ const filePath = path.join(await fs$1.realpath(path.dirname(requestedPath)), path.basename(requestedPath));
4769
+ const owner = await createLockOwner({ signal });
4536
4770
  lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);
4537
4771
  const payload = `${JSON.stringify({
4538
- pid: process.pid,
4772
+ ...owner.payload,
4539
4773
  created_at: new Date(lastCreatedAt).toISOString()
4540
4774
  }, null, 2)}\n`;
4541
4775
  for (;;) {
4542
4776
  signal?.throwIfAborted();
4543
- try {
4544
- await publishLock(filePath, payload, signal);
4545
- break;
4546
- } catch (error) {
4547
- if (error.code !== "EEXIST") throw error;
4548
- await recoverAbandonedLock(filePath);
4549
- await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4550
- }
4777
+ const turn = await tryAcquireTurn(filePath, payload, owner, signal);
4778
+ if (turn) return turn;
4779
+ await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4551
4780
  }
4552
- return await observePublishedLock(filePath, payload, signal);
4781
+ }
4782
+ //#endregion
4783
+ //#region src/session/execution/direct-lifetime.ts
4784
+ function ownDirectClient(client, signal) {
4785
+ let closing;
4786
+ const cancel = async () => {
4787
+ await withTimeout(client.cancelActivePrompt(2500), 2500).catch(() => {});
4788
+ await client.close();
4789
+ };
4790
+ const onAbort = () => {
4791
+ closing ??= cancel();
4792
+ closing.catch(() => {});
4793
+ };
4794
+ signal?.addEventListener("abort", onAbort, { once: true });
4795
+ if (signal?.aborted) onAbort();
4796
+ const release = () => signal?.removeEventListener("abort", onAbort);
4797
+ return Object.assign(() => {
4798
+ release();
4799
+ closing ??= client.close();
4800
+ return closing;
4801
+ }, { release });
4802
+ }
4803
+ function directExecutionError(error, signal) {
4804
+ return signal?.aborted && error instanceof AgentDisconnectedError ? signal.reason : error;
4553
4805
  }
4554
4806
  //#endregion
4555
4807
  //#region src/session/queue/ipc-server.ts
@@ -4584,9 +4836,12 @@ function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options =
4584
4836
  acp: normalized.acp
4585
4837
  };
4586
4838
  }
4839
+ function armSocketDrainTimeout(socket) {
4840
+ if (process.platform !== "win32") socket.setTimeout(QUEUE_SOCKET_DRAIN_TIMEOUT_MS);
4841
+ }
4587
4842
  function writeQueueMessage(socket, message) {
4588
4843
  if (socket.destroyed || !socket.writable) return;
4589
- socket.write(`${JSON.stringify(message)}\n`);
4844
+ if (!socket.write(`${JSON.stringify(message)}\n`) && !socket.timeout) armSocketDrainTimeout(socket);
4590
4845
  }
4591
4846
  var SessionQueueOwner = class SessionQueueOwner {
4592
4847
  server;
@@ -4730,6 +4985,7 @@ var SessionQueueOwner = class SessionQueueOwner {
4730
4985
  return true;
4731
4986
  }
4732
4987
  handleControlRequest(options) {
4988
+ this.taskSockets.add(options.socket);
4733
4989
  writeQueueMessage(options.socket, {
4734
4990
  type: "accepted",
4735
4991
  requestId: options.requestId,
@@ -4753,6 +5009,7 @@ var SessionQueueOwner = class SessionQueueOwner {
4753
5009
  if (socket.destroyed || this.drainingSockets.has(socket)) return;
4754
5010
  this.taskSockets.delete(socket);
4755
5011
  this.drainingSockets.add(socket);
5012
+ armSocketDrainTimeout(socket);
4756
5013
  socket.end(() => {
4757
5014
  this.drainingSockets.delete(socket);
4758
5015
  socket.destroy();
@@ -4902,6 +5159,10 @@ var SessionQueueOwner = class SessionQueueOwner {
4902
5159
  handleConnection(socket) {
4903
5160
  this.sockets.add(socket);
4904
5161
  socket.setEncoding("utf8");
5162
+ socket.on("timeout", () => socket.destroy());
5163
+ socket.on("drain", () => {
5164
+ if (!socket.writableEnded) socket.setTimeout(0);
5165
+ });
4905
5166
  socket.once("close", () => {
4906
5167
  this.sockets.delete(socket);
4907
5168
  this.taskSockets.delete(socket);
@@ -4990,6 +5251,10 @@ async function replayDesiredMode(params) {
4990
5251
  await withTimeout(params.client.setSessionMode(params.sessionId, params.desiredModeId, params.replay.authority), params.timeoutMs);
4991
5252
  params.replay.acknowledged = true;
4992
5253
  if (params.verbose) process.stderr.write(`[acpx] replayed desired mode ${params.desiredModeId} on fresh ACP session ${params.sessionId} (previous ${params.previousSessionId})\n`);
5254
+ return {
5255
+ models: advertisedModelState(params.record.acpx),
5256
+ configOptionsPresent: params.record.acpx?.config_options !== void 0
5257
+ };
4993
5258
  } catch (error) {
4994
5259
  rethrowAuthorityFailure(params.replay, error);
4995
5260
  throw new SessionModeReplayError(`Failed to replay saved session mode ${params.desiredModeId} on fresh ACP session ${params.sessionId}: ${formatErrorMessage(error)}`, {
@@ -5042,7 +5307,7 @@ async function replayDesiredConfigOptions(params) {
5042
5307
  for (const [configId, value] of Object.entries(params.desiredConfigOptions)) {
5043
5308
  if (configId === replacingKey || acceptedConfigOptions && !acceptsSavedConfigValue(acceptedConfigOptions.find((option) => option.id === configId), value)) continue;
5044
5309
  try {
5045
- const response = await withTimeout(params.client.setSessionConfigOption(params.sessionId, configId, value, params.replay.authority), params.timeoutMs);
5310
+ const response = await withTimeout(params.client.setSessionConfigOption(params.sessionId, configId, value, advertisedModelState(params.record.acpx), params.replay.authority), params.timeoutMs);
5046
5311
  params.replay.acknowledged = true;
5047
5312
  params.record.acpx = applyConfigOptionSelection(params.record.acpx, configId, value, response);
5048
5313
  acceptedConfigOptions = response.configOptions;
@@ -5068,18 +5333,20 @@ function restoreOriginalSessionState(params) {
5068
5333
  async function connectAndLoadSession(options) {
5069
5334
  const record = options.record;
5070
5335
  const client = options.client;
5336
+ assertControlAuthority(options.authority);
5071
5337
  const sameSessionOnly = requiresSameSession(options.resumePolicy) || Boolean(record.importedFrom);
5072
5338
  const originalSessionId = record.acpSessionId;
5073
5339
  const originalAgentSessionId = record.agentSessionId;
5074
5340
  const originalAcpx = cloneSessionAcpxState(record.acpx);
5075
- const desiredModeId = getDesiredModeId(record.acpx);
5341
+ const desiredModeId = options.replacingMode ? void 0 : getDesiredModeId(record.acpx);
5076
5342
  const desiredModelId = getDesiredModelId(record.acpx);
5077
5343
  const desiredConfigOptions = getDesiredConfigOptions(record.acpx);
5078
5344
  const storedProcessAlive = isProcessAlive(record.pid);
5079
5345
  logReconnectAttempt(record, storedProcessAlive, Boolean(record.pid) && !storedProcessAlive, options.verbose);
5080
5346
  const reusingLoadedSession = client.hasReusableSession(record.acpSessionId);
5081
5347
  if (reusingLoadedSession) incrementPerfCounter("runtime.connect_and_load.reused_session");
5082
- else await withTimeout(client.start(), options.timeoutMs);
5348
+ else await withTimeout(client.start(options.authority), options.timeoutMs);
5349
+ assertControlAuthority(options.authority);
5083
5350
  options.onClientAvailable?.(options.activeController);
5084
5351
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
5085
5352
  record.closed = false;
@@ -5090,9 +5357,11 @@ async function connectAndLoadSession(options) {
5090
5357
  record,
5091
5358
  reusingLoadedSession,
5092
5359
  sameSessionOnly,
5093
- timeoutMs: options.timeoutMs
5360
+ timeoutMs: options.timeoutMs,
5361
+ authority: options.authority
5094
5362
  });
5095
5363
  const { resumed, loadError, sessionId, createdFreshSession, pendingAgentSessionId, sessionModels } = loadState;
5364
+ applyReconnectedModelState(record, sessionModels, loadState.configOptionsPresent, loadState.legacyModelMetadataPresent, createdFreshSession);
5096
5365
  const preferenceReplay = await replaySessionPreferences({
5097
5366
  client,
5098
5367
  record,
@@ -5123,14 +5392,18 @@ async function connectAndLoadSession(options) {
5123
5392
  };
5124
5393
  }
5125
5394
  function resolveModelsAfterReplay(replay, initialModels) {
5126
- if (replay.configReplay.replayed) return replay.configReplay.models ?? preserveLegacyModels(replay.modelReplay.replayed ? replay.modelReplay.models : initialModels);
5127
- return replay.modelReplay.replayed ? replay.modelReplay.models : initialModels;
5395
+ const models = resolveModeReplayModels(replay.modeMetadata, initialModels);
5396
+ if (replay.configReplay.replayed) return replay.configReplay.models ?? preserveLegacyModels(replay.modelReplay.replayed ? replay.modelReplay.models : models);
5397
+ return replay.modelReplay.replayed ? replay.modelReplay.models : models;
5398
+ }
5399
+ function resolveModeReplayModels(modeMetadata, initialModels) {
5400
+ return modeMetadata ? modeMetadata.models : initialModels;
5128
5401
  }
5129
5402
  function preserveLegacyModels(models) {
5130
5403
  return models && !models.configId ? models : void 0;
5131
5404
  }
5132
5405
  function resolveConfigOptionsPresenceAfterReplay(replay, initiallyPresent) {
5133
- return initiallyPresent || replay.configReplay.replayed || replay.modelReplay.configOptions !== void 0;
5406
+ return initiallyPresent || replay.modeMetadata?.configOptionsPresent === true || replay.configReplay.replayed || replay.modelReplay.configOptions !== void 0;
5134
5407
  }
5135
5408
  function applyReconnectedModelState(record, sessionModels, configOptionsPresent, legacyModelMetadataPresent, createdFreshSession) {
5136
5409
  clearOmittedFreshSessionConfigOptions(record, createdFreshSession, configOptionsPresent);
@@ -5171,12 +5444,13 @@ async function replaySessionPreferences(params) {
5171
5444
  };
5172
5445
  let modelReplay = { replayed: false };
5173
5446
  let configReplay = { replayed: false };
5447
+ let modeMetadata;
5174
5448
  const replay = createReplayState(params.authority);
5175
- const replacingModel = replacesModel(params.replacingConfigOption, params.sessionModels, params.originalAcpx);
5176
5449
  try {
5177
5450
  assertControlAuthority(replay.authority);
5178
- await replayDesiredMode({
5451
+ modeMetadata = await replayDesiredMode({
5179
5452
  client: params.client,
5453
+ record: params.record,
5180
5454
  sessionId: params.sessionId,
5181
5455
  desiredModeId: params.createdFreshSession ? params.desiredModeId : void 0,
5182
5456
  replay,
@@ -5184,6 +5458,8 @@ async function replaySessionPreferences(params) {
5184
5458
  timeoutMs: params.timeoutMs,
5185
5459
  verbose: params.verbose
5186
5460
  });
5461
+ const effectiveModels = resolveModeReplayModels(modeMetadata, params.sessionModels);
5462
+ const replacingModel = replacesModel(params.replacingConfigOption, effectiveModels, params.originalAcpx);
5187
5463
  modelReplay = await replayDesiredModel({
5188
5464
  client: params.client,
5189
5465
  sessionId: params.sessionId,
@@ -5191,7 +5467,7 @@ async function replaySessionPreferences(params) {
5191
5467
  replay,
5192
5468
  previousSessionId: params.originalSessionId,
5193
5469
  record: params.record,
5194
- models: params.sessionModels,
5470
+ models: effectiveModels,
5195
5471
  createdFreshSession: params.createdFreshSession,
5196
5472
  timeoutMs: params.timeoutMs,
5197
5473
  verbose: params.verbose,
@@ -5218,7 +5494,8 @@ async function replaySessionPreferences(params) {
5218
5494
  reconcileAgentSessionId(params.record, params.pendingAgentSessionId);
5219
5495
  return {
5220
5496
  modelReplay,
5221
- configReplay
5497
+ configReplay,
5498
+ modeMetadata
5222
5499
  };
5223
5500
  }
5224
5501
  function settleFailedReplay(params, replay, error) {
@@ -5250,11 +5527,14 @@ async function loadOrCreateRuntimeSession(params) {
5250
5527
  record: params.record,
5251
5528
  reason: "agent does not support session/resume or session/load"
5252
5529
  });
5253
- return await createFreshRuntimeSession(params.client, params.record, params.timeoutMs);
5530
+ return await createFreshRuntimeSession(params.client, params.record, params.timeoutMs, params.authority);
5254
5531
  }
5255
5532
  async function loadRuntimeSession(params, resume) {
5256
5533
  try {
5257
- const loadResult = await withTimeout(resume ? params.client.resumeSession(params.record.acpSessionId, params.record.cwd) : params.client.loadSessionWithOptions(params.record.acpSessionId, params.record.cwd, { suppressReplayUpdates: true }), params.timeoutMs);
5534
+ const loadResult = await withTimeout(resume ? params.client.resumeSession(params.record.acpSessionId, params.record.cwd, params.authority) : params.client.loadSessionWithOptions(params.record.acpSessionId, params.record.cwd, {
5535
+ suppressReplayUpdates: true,
5536
+ ...params.authority ? { authority: params.authority } : {}
5537
+ }), params.timeoutMs);
5258
5538
  reconcileAgentSessionId(params.record, loadResult.agentSessionId);
5259
5539
  applyConfigOptionsToRecord(params.record, loadResult);
5260
5540
  return {
@@ -5267,9 +5547,14 @@ async function loadRuntimeSession(params, resume) {
5267
5547
  createdFreshSession: false
5268
5548
  };
5269
5549
  } catch (error) {
5550
+ rethrowCancelledLoad(error, params.authority);
5270
5551
  return await recoverRuntimeSessionLoadFailure(params, error);
5271
5552
  }
5272
5553
  }
5554
+ function rethrowCancelledLoad(error, authority) {
5555
+ const signal = authority?.signal;
5556
+ if (signal?.aborted && (error instanceof AgentDisconnectedError || error === signal.reason)) throw signal.reason;
5557
+ }
5273
5558
  async function recoverRuntimeSessionLoadFailure(params, error) {
5274
5559
  const loadError = formatErrorMessage(error);
5275
5560
  if (params.sameSessionOnly) throw makeSessionResumeRequiredError({
@@ -5279,12 +5564,12 @@ async function recoverRuntimeSessionLoadFailure(params, error) {
5279
5564
  });
5280
5565
  if (!shouldFallbackToNewSession(error, params.record)) throw error;
5281
5566
  return {
5282
- ...await createFreshRuntimeSession(params.client, params.record, params.timeoutMs),
5567
+ ...await createFreshRuntimeSession(params.client, params.record, params.timeoutMs, params.authority),
5283
5568
  loadError
5284
5569
  };
5285
5570
  }
5286
- async function createFreshRuntimeSession(client, record, timeoutMs) {
5287
- const createdSession = await withTimeout(client.createSession(record.cwd), timeoutMs);
5571
+ async function createFreshRuntimeSession(client, record, timeoutMs, authority) {
5572
+ const createdSession = await withTimeout(client.createSession(record.cwd, authority), timeoutMs);
5288
5573
  applyConfigOptionsToRecord(record, createdSession);
5289
5574
  return {
5290
5575
  sessionId: createdSession.sessionId,
@@ -5313,7 +5598,7 @@ function createActiveSessionController(params) {
5313
5598
  return response;
5314
5599
  },
5315
5600
  setSessionConfigOption: async (configId, value) => {
5316
- return await params.client.setSessionConfigOption(getActiveSessionId(), configId, value);
5601
+ return await params.client.setSessionConfigOption(getActiveSessionId(), configId, value, advertisedModelState(params.record.acpx));
5317
5602
  }
5318
5603
  };
5319
5604
  }
@@ -5351,6 +5636,7 @@ async function withConnectedSession(options) {
5351
5636
  client,
5352
5637
  record,
5353
5638
  resumePolicy: options.resumePolicy,
5639
+ replacingMode: options.replacingMode,
5354
5640
  replacingConfigOption: options.replacingConfigOption,
5355
5641
  authority: options.authority,
5356
5642
  timeoutMs: options.timeoutMs,
@@ -5411,6 +5697,7 @@ async function withConnectedSession(options) {
5411
5697
  function buildDirectConnectedSessionOptions(options, run) {
5412
5698
  return {
5413
5699
  sessionRecordId: options.sessionRecordId,
5700
+ replacingMode: options.replacingMode,
5414
5701
  replacingConfigOption: options.replacingConfigOption,
5415
5702
  loadRecord: resolveSessionRecord,
5416
5703
  saveRecord: writeSessionRecord,
@@ -5436,14 +5723,41 @@ function toSessionMutationResult(result) {
5436
5723
  loadError: result.loadError
5437
5724
  };
5438
5725
  }
5726
+ async function withOwnedDirectSession(options) {
5727
+ const waiting = new AbortController();
5728
+ const timeoutMs = options.timeoutMs;
5729
+ const timer = timeoutMs != null && timeoutMs > 0 ? setTimeout(() => waiting.abort(new TimeoutError(timeoutMs)), timeoutMs) : void 0;
5730
+ let ownership;
5731
+ let recordId;
5732
+ try {
5733
+ recordId = (await options.loadRecord(options.sessionRecordId)).acpxRecordId;
5734
+ ownership = await acquireSessionTurn(recordId, waiting.signal);
5735
+ } catch (error) {
5736
+ waiting.signal.throwIfAborted();
5737
+ throw error;
5738
+ } finally {
5739
+ clearTimeout(timer);
5740
+ }
5741
+ try {
5742
+ return await withConnectedSession({
5743
+ ...options,
5744
+ sessionRecordId: recordId
5745
+ });
5746
+ } finally {
5747
+ await ownership[Symbol.asyncDispose]();
5748
+ }
5749
+ }
5439
5750
  async function runSessionSetModeDirect(options) {
5440
- return toSessionMutationResult(await withConnectedSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
5751
+ return toSessionMutationResult(await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5752
+ ...options,
5753
+ replacingMode: true
5754
+ }, async ({ client, sessionId, record }) => {
5441
5755
  await withTimeout(client.setSessionMode(sessionId, options.modeId), options.timeoutMs);
5442
5756
  setDesiredModeId(record, options.modeId);
5443
5757
  })));
5444
5758
  }
5445
5759
  async function runSessionSetModelDirect(options) {
5446
- const result = await withConnectedSession(buildDirectConnectedSessionOptions({
5760
+ const result = await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5447
5761
  ...options,
5448
5762
  replacingConfigOption: { key: "model" }
5449
5763
  }, async ({ client, sessionId, record }) => {
@@ -5458,12 +5772,13 @@ async function runSessionSetModelDirect(options) {
5458
5772
  };
5459
5773
  }
5460
5774
  async function runSessionSetConfigOptionDirect(options) {
5461
- const result = await withConnectedSession(buildDirectConnectedSessionOptions({
5775
+ const result = await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5462
5776
  ...options,
5463
5777
  replacingConfigOption: { key: options.configId }
5464
5778
  }, async ({ client, sessionId, record }) => {
5465
- const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value), options.timeoutMs);
5466
- record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, response);
5779
+ const models = advertisedModelState(record.acpx);
5780
+ const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value, models), options.timeoutMs);
5781
+ record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, response, models?.configId);
5467
5782
  return response;
5468
5783
  }));
5469
5784
  return {
@@ -5482,16 +5797,31 @@ async function cancelSessionPrompt(options) {
5482
5797
  cancelled: cancelled === true
5483
5798
  };
5484
5799
  }
5485
- async function setSessionMode(options) {
5486
- if (await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose)) {
5487
- const record = await resolveSessionRecord(options.sessionId);
5800
+ /**
5801
+ * Dispatches the mode change to a running queue owner. Returns `undefined`
5802
+ * when no owner holds the session, leaving the direct-connection decision to
5803
+ * the caller.
5804
+ *
5805
+ * An owner advertising `persistsControlState` saves the accepted state itself.
5806
+ * Only the older caller-save path still reloads and rewrites the record here,
5807
+ * which is not serialized against another process doing the same.
5808
+ */
5809
+ async function setSessionModeOnOwner(options) {
5810
+ const submittedToOwner = await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose, options.assertDispatch);
5811
+ if (!submittedToOwner?.value) return;
5812
+ const record = await resolveSessionRecord(options.sessionId);
5813
+ if (!submittedToOwner.persistsControlState) {
5488
5814
  setDesiredModeId(record, options.modeId);
5489
5815
  await writeSessionRecord(record);
5490
- return {
5491
- record,
5492
- resumed: false
5493
- };
5494
5816
  }
5817
+ return {
5818
+ record,
5819
+ resumed: false
5820
+ };
5821
+ }
5822
+ async function setSessionMode(options) {
5823
+ const onOwner = await setSessionModeOnOwner(options);
5824
+ if (onOwner) return onOwner;
5495
5825
  return await runSessionSetModeDirect({
5496
5826
  sessionRecordId: options.sessionId,
5497
5827
  modeId: options.modeId,
@@ -5505,18 +5835,24 @@ async function setSessionMode(options) {
5505
5835
  verbose: options.verbose
5506
5836
  });
5507
5837
  }
5508
- async function setSessionModel(options) {
5509
- const submittedToOwner = await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose);
5510
- if (submittedToOwner) {
5511
- const record = await resolveSessionRecord(options.sessionId);
5512
- record.acpx = applyModelSelection(record.acpx, options.modelId, submittedToOwner.response);
5838
+ /** Owner-only counterpart of {@link setSessionMode} for the model control. */
5839
+ async function setSessionModelOnOwner(options) {
5840
+ const submittedToOwner = await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose, options.assertDispatch);
5841
+ if (!submittedToOwner) return;
5842
+ const record = await resolveSessionRecord(options.sessionId);
5843
+ if (!submittedToOwner.persistsControlState) {
5844
+ record.acpx = applyModelSelection(record.acpx, options.modelId, submittedToOwner.value.response);
5513
5845
  await writeSessionRecord(record);
5514
- return {
5515
- record,
5516
- response: submittedToOwner.response,
5517
- resumed: false
5518
- };
5519
5846
  }
5847
+ return {
5848
+ record,
5849
+ response: submittedToOwner.value.response,
5850
+ resumed: false
5851
+ };
5852
+ }
5853
+ async function setSessionModel(options) {
5854
+ const onOwner = await setSessionModelOnOwner(options);
5855
+ if (onOwner) return onOwner;
5520
5856
  return await runSessionSetModelDirect({
5521
5857
  sessionRecordId: options.sessionId,
5522
5858
  modelId: options.modelId,
@@ -5530,18 +5866,24 @@ async function setSessionModel(options) {
5530
5866
  verbose: options.verbose
5531
5867
  });
5532
5868
  }
5533
- async function setSessionConfigOption(options) {
5534
- const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose);
5535
- if (ownerResponse) {
5536
- const record = await resolveSessionRecord(options.sessionId);
5537
- record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, ownerResponse);
5869
+ /** Owner-only counterpart of {@link setSessionMode} for the config option control. */
5870
+ async function setSessionConfigOptionOnOwner(options) {
5871
+ const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose, options.assertDispatch);
5872
+ if (!ownerResponse) return;
5873
+ const record = await resolveSessionRecord(options.sessionId);
5874
+ if (!ownerResponse.persistsControlState) {
5875
+ record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, ownerResponse.value);
5538
5876
  await writeSessionRecord(record);
5539
- return {
5540
- record,
5541
- response: ownerResponse,
5542
- resumed: false
5543
- };
5544
5877
  }
5878
+ return {
5879
+ record,
5880
+ response: ownerResponse.value,
5881
+ resumed: false
5882
+ };
5883
+ }
5884
+ async function setSessionConfigOption(options) {
5885
+ const onOwner = await setSessionConfigOptionOnOwner(options);
5886
+ if (onOwner) return onOwner;
5545
5887
  return await runSessionSetConfigOptionDirect({
5546
5888
  sessionRecordId: options.sessionId,
5547
5889
  configId: options.configId,
@@ -5623,22 +5965,36 @@ const sessionControlTestInternals = {
5623
5965
  };
5624
5966
  async function closeSession(sessionId) {
5625
5967
  const record = await resolveSessionRecord(sessionId);
5626
- await tryCloseSessionOnRunningOwner({ sessionId: record.acpxRecordId }).catch(() => {});
5627
- await terminateQueueOwnerForSession(record.acpxRecordId);
5968
+ let selectedOwner;
5969
+ await tryCloseSessionOnRunningOwner({
5970
+ sessionId: record.acpxRecordId,
5971
+ onOwnerSelected: (owner) => {
5972
+ selectedOwner = owner;
5973
+ }
5974
+ }).catch(() => {});
5975
+ if (selectedOwner) await terminateQueueOwnerForSession(record.acpxRecordId, selectedOwner);
5628
5976
  if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) await terminateProcess(record.pid);
5629
- record.pid = void 0;
5630
- record.closed = true;
5631
- record.closedAt = isoNow$1();
5632
- await writeSessionRecord(record);
5633
- return record;
5977
+ const ownership = await acquireSessionTurn(record.acpxRecordId);
5978
+ try {
5979
+ const current = await resolveSessionRecord(record.acpxRecordId);
5980
+ current.pid = void 0;
5981
+ current.closed = true;
5982
+ current.closedAt = isoNow$1();
5983
+ await writeSessionRecord(current);
5984
+ return current;
5985
+ } finally {
5986
+ await ownership[Symbol.asyncDispose]();
5987
+ }
5634
5988
  }
5635
5989
  //#endregion
5636
5990
  //#region src/session/execution/session-management.ts
5637
5991
  async function createSessionRecordWithClient(client, options) {
5638
5992
  const cwd = absolutePath(options.cwd);
5639
- await withTimeout(client.start(), options.timeoutMs);
5993
+ await withTimeout(client.start({ signal: options.signal }), options.timeoutMs);
5994
+ assertControlAuthority({ signal: options.signal });
5640
5995
  const createdState = options.resumeSessionId ? await resumeSessionRecordWithClient(client, options, cwd) : await createFreshSessionState(client, options, cwd);
5641
5996
  const { sessionId, agentSessionId } = createdState;
5997
+ assertControlAuthority({ signal: options.signal });
5642
5998
  const lifecycle = client.getAgentLifecycleSnapshot();
5643
5999
  const record = {
5644
6000
  ...createInitialSessionRecord({
@@ -5660,10 +6016,12 @@ async function createSessionRecordWithClient(client, options) {
5660
6016
  applyConfigOptionsToRecord(record, createdState.sessionResult);
5661
6017
  applyInitialModelSelection(record, createdState.sessionResult.models, options.sessionOptions?.model, createdState.modelApplication);
5662
6018
  await writeSessionRecord(record);
6019
+ assertControlAuthority({ signal: options.signal });
5663
6020
  return record;
5664
6021
  }
5665
6022
  async function createFreshSessionState(client, options, cwd) {
5666
- const createdSession = await withTimeout(client.createSession(cwd), options.timeoutMs);
6023
+ const createdSession = await withTimeout(client.createSession(cwd, { signal: options.signal }), options.timeoutMs);
6024
+ assertControlAuthority({ signal: options.signal });
5667
6025
  const modelApplication = await applyRequestedModelIfAdvertised({
5668
6026
  client,
5669
6027
  sessionId: createdSession.sessionId,
@@ -5671,7 +6029,8 @@ async function createFreshSessionState(client, options, cwd) {
5671
6029
  models: createdSession.models,
5672
6030
  agentCommand: options.agentCommand,
5673
6031
  timeoutMs: options.timeoutMs,
5674
- onWarning: options.onModelWarning
6032
+ onWarning: options.onModelWarning,
6033
+ authority: { signal: options.signal }
5675
6034
  });
5676
6035
  return {
5677
6036
  sessionId: createdSession.sessionId,
@@ -5685,8 +6044,9 @@ async function resumeSessionRecordWithClient(client, options, cwd) {
5685
6044
  const resumeMethod = client.supportsResumeSession() ? "session/resume" : client.supportsLoadSession() ? "session/load" : void 0;
5686
6045
  if (!resumeMethod) throw new Error(`Agent command "${options.agentCommand}" does not support session/resume or session/load; cannot resume session ${options.resumeSessionId}`);
5687
6046
  try {
5688
- const resumedSession = await withTimeout(resumeMethod === "session/resume" ? client.resumeSession(options.resumeSessionId, cwd) : client.loadSession(options.resumeSessionId, cwd), options.timeoutMs);
6047
+ const resumedSession = await withTimeout(resumeMethod === "session/resume" ? client.resumeSession(options.resumeSessionId, cwd, { signal: options.signal }) : client.loadSessionWithOptions(options.resumeSessionId, cwd, { authority: { signal: options.signal } }), options.timeoutMs);
5689
6048
  const sessionModels = resumedSession.models;
6049
+ assertControlAuthority({ signal: options.signal });
5690
6050
  const modelApplication = await applyRequestedModelIfAdvertised({
5691
6051
  client,
5692
6052
  sessionId: options.resumeSessionId,
@@ -5694,7 +6054,8 @@ async function resumeSessionRecordWithClient(client, options, cwd) {
5694
6054
  models: sessionModels,
5695
6055
  agentCommand: options.agentCommand,
5696
6056
  timeoutMs: options.timeoutMs,
5697
- onWarning: options.onModelWarning
6057
+ onWarning: options.onModelWarning,
6058
+ authority: { signal: options.signal }
5698
6059
  });
5699
6060
  return {
5700
6061
  sessionId: options.resumeSessionId,
@@ -5707,7 +6068,7 @@ async function resumeSessionRecordWithClient(client, options, cwd) {
5707
6068
  }
5708
6069
  }
5709
6070
  async function createSessionWithClient(options) {
5710
- options.signal?.throwIfAborted();
6071
+ assertControlAuthority({ signal: options.signal });
5711
6072
  const client = new AcpClient({
5712
6073
  agentCommand: options.agentCommand,
5713
6074
  agentArgv: options.agentArgv,
@@ -5723,28 +6084,25 @@ async function createSessionWithClient(options) {
5723
6084
  verbose: options.verbose,
5724
6085
  sessionOptions: options.sessionOptions
5725
6086
  });
5726
- const onAbort = () => {
5727
- client.close().catch(() => {});
5728
- };
5729
- options.signal?.addEventListener("abort", onAbort, { once: true });
6087
+ const closeOwnedClient = ownDirectClient(client, options.signal);
5730
6088
  try {
5731
6089
  const create = async () => {
5732
- options.signal?.throwIfAborted();
6090
+ assertControlAuthority({ signal: options.signal });
5733
6091
  const record = await createSessionRecordWithClient(client, options);
5734
- options.signal?.throwIfAborted();
6092
+ assertControlAuthority({ signal: options.signal });
5735
6093
  return record;
5736
6094
  };
5737
6095
  return {
5738
6096
  record: options.handleProcessInterrupts === false ? await create() : await withInterrupt(create, async () => {
5739
- await client.close();
6097
+ await closeOwnedClient();
5740
6098
  }),
5741
6099
  client
5742
6100
  };
5743
6101
  } catch (error) {
5744
- await client.close();
5745
- throw error;
6102
+ await closeOwnedClient();
6103
+ throw directExecutionError(error, options.signal);
5746
6104
  } finally {
5747
- options.signal?.removeEventListener("abort", onAbort);
6105
+ closeOwnedClient.release();
5748
6106
  }
5749
6107
  }
5750
6108
  async function createSession(options) {
@@ -5810,7 +6168,7 @@ async function ensureSession(options) {
5810
6168
  }
5811
6169
  }
5812
6170
  async function ensureSessionWithOwnership(options, cwd) {
5813
- options.signal?.throwIfAborted();
6171
+ assertControlAuthority({ signal: options.signal });
5814
6172
  const gitRoot = findGitRepositoryRoot(cwd);
5815
6173
  const walkBoundary = options.walkBoundary ?? gitRoot ?? cwd;
5816
6174
  const existing = await findSessionByDirectoryWalk({
@@ -5951,16 +6309,182 @@ function installPerfMetricsCapture(options = {}) {
5951
6309
  }
5952
6310
  }
5953
6311
  //#endregion
6312
+ //#region src/session/queue/control-admission.ts
6313
+ var QueueControlDeadline = class {
6314
+ controller = new AbortController();
6315
+ admissionSignal;
6316
+ expiresAt;
6317
+ expired;
6318
+ timeoutError;
6319
+ rejectExpiry;
6320
+ timer;
6321
+ nativeSettled = false;
6322
+ admitted = false;
6323
+ constructor(timeoutMs, ownerSignal) {
6324
+ this.admissionSignal = ownerSignal ? AbortSignal.any([this.controller.signal, ownerSignal]) : this.controller.signal;
6325
+ this.expiresAt = timeoutMs != null && timeoutMs > 0 ? performance.now() + timeoutMs : void 0;
6326
+ this.timeoutError = this.expiresAt === void 0 ? void 0 : new TimeoutError(timeoutMs);
6327
+ this.expired = new Promise((_resolve, reject) => {
6328
+ this.rejectExpiry = reject;
6329
+ if (this.expiresAt !== void 0) this.timer = setTimeout(() => this.expire(), timeoutMs);
6330
+ });
6331
+ this.expired.catch(() => {});
6332
+ }
6333
+ get signal() {
6334
+ return this.admissionSignal;
6335
+ }
6336
+ get timeoutSignal() {
6337
+ return this.controller.signal;
6338
+ }
6339
+ get remainingMs() {
6340
+ this.assertActive();
6341
+ return this.expiresAt === void 0 || this.nativeSettled ? void 0 : Math.max(1, this.expiresAt - performance.now());
6342
+ }
6343
+ get nativeAdmitted() {
6344
+ return this.admitted;
6345
+ }
6346
+ get authority() {
6347
+ return {
6348
+ signal: this.signal,
6349
+ assertActive: () => {
6350
+ this.assertActive();
6351
+ this.admitted = true;
6352
+ }
6353
+ };
6354
+ }
6355
+ assertActive() {
6356
+ if (this.expiresAt !== void 0 && performance.now() >= this.expiresAt) this.expire();
6357
+ this.signal.throwIfAborted();
6358
+ }
6359
+ expire() {
6360
+ if (this.nativeSettled || this.timeoutSignal.aborted || !this.timeoutError) return;
6361
+ this.controller.abort(this.timeoutError);
6362
+ this.rejectExpiry(this.timeoutError);
6363
+ }
6364
+ responseSettled() {
6365
+ this.nativeSettled = true;
6366
+ clearTimeout(this.timer);
6367
+ this.timer = void 0;
6368
+ }
6369
+ isExpired(error) {
6370
+ return this.timeoutSignal.aborted && error === this.timeoutSignal.reason;
6371
+ }
6372
+ async wait(operation) {
6373
+ return await Promise.race([operation, this.expired]);
6374
+ }
6375
+ };
6376
+ function deferred() {
6377
+ let resolve;
6378
+ let reject;
6379
+ const promise = new Promise((accept, fail) => {
6380
+ resolve = accept;
6381
+ reject = fail;
6382
+ });
6383
+ promise.catch(() => {});
6384
+ return {
6385
+ promise,
6386
+ resolve,
6387
+ reject
6388
+ };
6389
+ }
6390
+ function drained(operation) {
6391
+ return operation.then(() => {}, () => {});
6392
+ }
6393
+ var QueueOwnerControlAdmission = class {
6394
+ options;
6395
+ idleTail = Promise.resolve();
6396
+ ticket;
6397
+ stopped = new AbortController();
6398
+ pending = 0;
6399
+ constructor(options) {
6400
+ this.options = options;
6401
+ }
6402
+ get hasPending() {
6403
+ return this.pending > 0;
6404
+ }
6405
+ beginPrompt() {
6406
+ const ticket = {
6407
+ ready: deferred(),
6408
+ released: deferred(),
6409
+ rawTail: Promise.resolve(),
6410
+ sealed: false
6411
+ };
6412
+ this.ticket = ticket;
6413
+ return {
6414
+ ticket,
6415
+ priorIdle: this.idleTail
6416
+ };
6417
+ }
6418
+ publish(ticket, controls) {
6419
+ if (!ticket.sealed) ticket.ready.resolve(controls);
6420
+ }
6421
+ seal(ticket) {
6422
+ ticket.sealed = true;
6423
+ ticket.ready.reject(new QueueConnectionError("Prompt ended before controls became ready", {
6424
+ detailCode: "QUEUE_CONTROL_REQUEST_FAILED",
6425
+ origin: "queue",
6426
+ retryable: true
6427
+ }));
6428
+ return ticket.rawTail;
6429
+ }
6430
+ release(ticket) {
6431
+ if (this.ticket === ticket) this.ticket = void 0;
6432
+ ticket.released.resolve();
6433
+ }
6434
+ stopAdmission() {
6435
+ this.stopped.abort(new QueueConnectionError("Queue owner is closing", {
6436
+ detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
6437
+ origin: "queue",
6438
+ retryable: true
6439
+ }));
6440
+ }
6441
+ async drainAdmitted() {
6442
+ await Promise.all([this.idleTail, this.ticket?.rawTail]);
6443
+ }
6444
+ async run(invoke, timeoutMs, replacingConfigOption) {
6445
+ const deadline = new QueueControlDeadline(timeoutMs, this.stopped.signal);
6446
+ this.pending += 1;
6447
+ const operation = this.enqueue(invoke, {
6448
+ deadline,
6449
+ replacingConfigOption
6450
+ });
6451
+ operation.finally(() => {
6452
+ this.pending -= 1;
6453
+ deadline.responseSettled();
6454
+ }).catch(() => {});
6455
+ return await deadline.wait(operation);
6456
+ }
6457
+ async enqueue(invoke, options) {
6458
+ options.deadline.assertActive();
6459
+ const ticket = this.ticket;
6460
+ if (ticket?.sealed) return ticket.released.promise.then(() => this.enqueue(invoke, options));
6461
+ if (ticket) {
6462
+ const operation = ticket.rawTail.then(async () => {
6463
+ const controls = await ticket.ready.promise.catch((error) => {
6464
+ options.deadline.assertActive();
6465
+ throw error;
6466
+ });
6467
+ options.deadline.assertActive();
6468
+ return await invoke(controls, options.deadline);
6469
+ });
6470
+ ticket.rawTail = drained(operation);
6471
+ return operation;
6472
+ }
6473
+ const operation = this.idleTail.then(async () => {
6474
+ options.deadline.assertActive();
6475
+ return await this.options.runIdle(invoke, options);
6476
+ });
6477
+ this.idleTail = drained(operation);
6478
+ return operation;
6479
+ }
6480
+ };
6481
+ //#endregion
5954
6482
  //#region src/session/queue/owner-turn-controller.ts
5955
6483
  var QueueOwnerTurnController = class {
5956
- options;
5957
6484
  state = "idle";
5958
6485
  pendingCancel = false;
5959
6486
  activeController;
5960
6487
  waitingTurn;
5961
- constructor(options) {
5962
- this.options = options;
5963
- }
5964
6488
  get lifecycleState() {
5965
6489
  return this.state;
5966
6490
  }
@@ -5992,20 +6516,9 @@ var QueueOwnerTurnController = class {
5992
6516
  clearActiveController() {
5993
6517
  this.activeController = void 0;
5994
6518
  }
5995
- assertCanHandleControlRequest() {
5996
- if (this.state === "closing") throw new QueueConnectionError("Queue owner is closing", {
5997
- detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
5998
- origin: "queue",
5999
- retryable: true
6000
- });
6001
- }
6002
6519
  async requestCancel() {
6003
6520
  const activeController = this.activeController;
6004
- if (activeController?.hasActivePrompt()) {
6005
- const cancelled = await activeController.requestCancelActivePrompt();
6006
- if (cancelled) this.pendingCancel = false;
6007
- return cancelled;
6008
- }
6521
+ if (activeController?.hasActivePrompt()) return await this.cancelActivePrompt(activeController);
6009
6522
  if (this.state === "starting" || this.state === "active") {
6010
6523
  this.pendingCancel = true;
6011
6524
  this.waitingTurn?.abort();
@@ -6016,32 +6529,138 @@ var QueueOwnerTurnController = class {
6016
6529
  async applyPendingCancel() {
6017
6530
  const activeController = this.activeController;
6018
6531
  if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) return false;
6019
- const cancelled = await activeController.requestCancelActivePrompt();
6020
- if (cancelled) this.pendingCancel = false;
6532
+ return await this.cancelActivePrompt(activeController);
6533
+ }
6534
+ async cancelActivePrompt(activeController) {
6535
+ const turn = this.waitingTurn;
6536
+ const cancellation = activeController.requestCancelActivePrompt();
6537
+ turn?.abort();
6538
+ const cancelled = await cancellation;
6539
+ if (cancelled && this.waitingTurn === turn) this.pendingCancel = false;
6021
6540
  return cancelled;
6022
6541
  }
6023
- async setSessionMode(modeId, timeoutMs) {
6024
- this.assertCanHandleControlRequest();
6025
- const activeController = this.activeController;
6026
- if (activeController) {
6027
- await this.options.withTimeout(async () => await activeController.setSessionMode(modeId), timeoutMs);
6028
- return;
6542
+ };
6543
+ //#endregion
6544
+ //#region src/session/execution/owned-controls.ts
6545
+ async function acceptControl(options, invoke, apply, deadline = new QueueControlDeadline()) {
6546
+ deadline.assertActive();
6547
+ const raw = invoke(deadline.authority).then(async (response) => {
6548
+ deadline.responseSettled();
6549
+ apply(response);
6550
+ await options.checkpoint();
6551
+ return response;
6552
+ }, (error) => {
6553
+ deadline.responseSettled();
6554
+ throw error;
6555
+ });
6556
+ try {
6557
+ return await deadline.wait(raw);
6558
+ } catch (error) {
6559
+ if (deadline.isExpired(error) && deadline.nativeAdmitted) {
6560
+ const [retired] = await Promise.allSettled([options.retire(), raw]);
6561
+ if (retired.status === "rejected") throw retired.reason;
6029
6562
  }
6030
- await this.options.setSessionModeFallback(modeId, timeoutMs);
6563
+ throw error;
6031
6564
  }
6032
- async setSessionModel(modelId, timeoutMs) {
6033
- this.assertCanHandleControlRequest();
6034
- const activeController = this.activeController;
6035
- if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionModel(modelId), timeoutMs);
6036
- return await this.options.setSessionModelFallback(modelId, timeoutMs);
6565
+ }
6566
+ function createOwnedSessionControls(options) {
6567
+ return {
6568
+ setSessionMode: async (modeId, deadline) => {
6569
+ await acceptControl(options, (authority) => options.client.setSessionMode(options.sessionId(), modeId, authority), () => setDesiredModeId(options.record, modeId), deadline);
6570
+ },
6571
+ setSessionModel: async (modelId, deadline) => {
6572
+ const models = advertisedModelState(options.record.acpx);
6573
+ return await acceptControl(options, (authority) => options.client.setSessionModel(options.sessionId(), modelId, models, authority), (response) => {
6574
+ options.record.acpx = applyModelSelection(options.record.acpx, modelId, response);
6575
+ }, deadline);
6576
+ },
6577
+ setSessionConfigOption: async (configId, value, deadline) => {
6578
+ const models = advertisedModelState(options.record.acpx);
6579
+ return await acceptControl(options, (authority) => options.client.setSessionConfigOption(options.sessionId(), configId, value, models, authority), (response) => {
6580
+ options.record.acpx = applyConfigOptionSelection(options.record.acpx, configId, value, response, models?.configId);
6581
+ }, deadline);
6582
+ }
6583
+ };
6584
+ }
6585
+ async function runIdleControlWithRecord(options, record, invoke) {
6586
+ const { client, deadline } = options;
6587
+ deadline.assertActive();
6588
+ let retirement;
6589
+ const retire = () => {
6590
+ retirement ??= client.close().catch((error) => {
6591
+ options.onRetirementFailure();
6592
+ throw error;
6593
+ });
6594
+ return retirement;
6595
+ };
6596
+ const onAbort = () => {
6597
+ retire().catch(() => {});
6598
+ };
6599
+ const checkpoint = async () => {
6600
+ record.lastUsedAt = isoNow$1();
6601
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
6602
+ await writeSessionRecord(record);
6603
+ };
6604
+ let sessionId = record.acpSessionId;
6605
+ const controls = createOwnedSessionControls({
6606
+ client,
6607
+ record,
6608
+ sessionId: () => sessionId,
6609
+ checkpoint,
6610
+ retire
6611
+ });
6612
+ client.setEventHandlers({ onSessionUpdate: (notification) => {
6613
+ record.acpx = recordSessionUpdate(record, record.acpx, notification);
6614
+ } });
6615
+ deadline.timeoutSignal.addEventListener("abort", onAbort, { once: true });
6616
+ let connected = false;
6617
+ let completed = false;
6618
+ try {
6619
+ await connectAndLoadSession({
6620
+ client,
6621
+ record,
6622
+ resumePolicy: "same-session-only",
6623
+ replacingConfigOption: options.replacingConfigOption,
6624
+ timeoutMs: deadline.remainingMs,
6625
+ authority: deadline.authority,
6626
+ verbose: options.verbose,
6627
+ activeController: {
6628
+ ...controls,
6629
+ hasActivePrompt: () => client.hasActivePrompt(),
6630
+ requestCancelActivePrompt: () => client.requestCancelActivePrompt()
6631
+ },
6632
+ onSessionIdResolved: (value) => {
6633
+ sessionId = value;
6634
+ }
6635
+ });
6636
+ connected = true;
6637
+ deadline.assertActive();
6638
+ const value = await invoke(controls, deadline);
6639
+ completed = true;
6640
+ return value;
6641
+ } finally {
6642
+ deadline.timeoutSignal.removeEventListener("abort", onAbort);
6643
+ try {
6644
+ if (!connected) await retire();
6645
+ await retirement;
6646
+ if (!completed || retirement) await checkpoint().catch(() => {});
6647
+ } finally {
6648
+ client.clearEventHandlers();
6649
+ }
6037
6650
  }
6038
- async setSessionConfigOption(configId, value, timeoutMs) {
6039
- this.assertCanHandleControlRequest();
6040
- const activeController = this.activeController;
6041
- if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionConfigOption(configId, value), timeoutMs);
6042
- return await this.options.setSessionConfigOptionFallback(configId, value, timeoutMs);
6651
+ }
6652
+ async function runIdleOwnerControl(options, invoke) {
6653
+ const { acpxRecordId } = await resolveSessionRecord(options.sessionId);
6654
+ const ownership = await acquireSessionTurn(acpxRecordId, options.deadline.signal).catch((error) => {
6655
+ options.deadline.assertActive();
6656
+ throw error;
6657
+ });
6658
+ try {
6659
+ return await runIdleControlWithRecord(options, await resolveSessionRecord(acpxRecordId), invoke);
6660
+ } finally {
6661
+ await ownership[Symbol.asyncDispose]();
6043
6662
  }
6044
- };
6663
+ }
6045
6664
  //#endregion
6046
6665
  //#region src/session/execution/queue-owner-process.ts
6047
6666
  function isNonEmptyStringArray(value) {
@@ -6059,27 +6678,27 @@ const NODE_TEST_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
6059
6678
  "--test-reporter",
6060
6679
  "--test-reporter-destination"
6061
6680
  ]);
6062
- const INSPECTOR_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
6681
+ const INSPECTOR_FLAGS = /* @__PURE__ */ new Set([
6063
6682
  "--inspect",
6064
6683
  "--inspect-brk",
6684
+ "--inspect-wait"
6685
+ ]);
6686
+ const INSPECTOR_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
6065
6687
  "--inspect-port",
6066
6688
  "--inspect-publish-uid",
6067
6689
  "--debug-port"
6068
6690
  ]);
6069
- const INSPECTOR_FLAG_PREFIXES = [
6070
- "--inspect=",
6071
- "--inspect-brk=",
6072
- "--inspect-port=",
6073
- "--inspect-publish-uid=",
6074
- "--debug-port="
6075
- ];
6076
6691
  function classifyExecArgv(value) {
6077
- if (value === void 0) return "drop";
6078
- if (NODE_TEST_FLAGS_WITH_VALUE.has(value) || INSPECTOR_FLAGS_WITH_VALUE.has(value)) return "drop-with-value";
6079
- return dropSingleExecArgv(value) ? "drop" : "keep";
6692
+ if (NODE_TEST_FLAGS_WITH_VALUE.has(value)) return "drop-with-value";
6693
+ if (NODE_TEST_FLAGS.has(value) || value.startsWith("--test-")) return "drop";
6694
+ return classifyInspectorArgv(value);
6080
6695
  }
6081
- function dropSingleExecArgv(value) {
6082
- return NODE_TEST_FLAGS.has(value) || value.startsWith("--test-") || INSPECTOR_FLAG_PREFIXES.some((prefix) => value.startsWith(prefix));
6696
+ function classifyInspectorArgv(value) {
6697
+ const [name] = value.split("=", 1);
6698
+ if (!name?.startsWith("--")) return "keep";
6699
+ const flag = name.replaceAll("_", "-");
6700
+ if (INSPECTOR_FLAGS_WITH_VALUE.has(flag)) return value.includes("=") ? "drop" : "drop-with-value";
6701
+ return INSPECTOR_FLAGS.has(flag) ? "drop" : "keep";
6083
6702
  }
6084
6703
  function sanitizeQueueOwnerExecArgv(execArgv = process.execArgv) {
6085
6704
  const sanitized = [];
@@ -6223,20 +6842,27 @@ function responseMetaField(meta) {
6223
6842
  return meta === void 0 ? {} : { _meta: meta };
6224
6843
  }
6225
6844
  function recoveredSessionResult(response, conversation, promptMessageId) {
6226
- recordPromptResponseUsage(conversation, response?.usage, promptMessageId);
6845
+ recordPromptResponseUsage(conversation, response.usage, promptMessageId);
6227
6846
  return {
6228
- stopReason: "end_turn",
6847
+ stopReason: response.stopReason,
6229
6848
  source: "session",
6230
- ...responseMetaField(response?._meta)
6849
+ ...responseMetaField(response._meta)
6231
6850
  };
6232
6851
  }
6233
- function abortTimedOutRequests(error, client, sessionId) {
6234
- if (error instanceof TimeoutError) client.endPromptElicitation?.(sessionId);
6852
+ function abortTimedOutRequests(error, lifetime) {
6853
+ if (error instanceof TimeoutError) lifetime.abort(error);
6854
+ }
6855
+ function promptRequestAuthority(lifetime, authority) {
6856
+ return {
6857
+ signal: authority?.signal ? AbortSignal.any([authority.signal, lifetime.signal]) : lifetime.signal,
6858
+ assertActive: () => assertControlAuthority(authority)
6859
+ };
6235
6860
  }
6236
6861
  async function runPromptTurn(params) {
6237
6862
  let settledResponse;
6863
+ const lifetime = new AbortController();
6238
6864
  try {
6239
- const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestWritten, params.onElicitation, params.onPermissionRequest, params.authority);
6865
+ const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestWritten, params.onElicitation, params.onPermissionRequest, promptRequestAuthority(lifetime, params.authority));
6240
6866
  promptPromise.then((response) => {
6241
6867
  settledResponse = response;
6242
6868
  }, () => {});
@@ -6253,14 +6879,16 @@ async function runPromptTurn(params) {
6253
6879
  ...responseMetaField(response._meta)
6254
6880
  };
6255
6881
  } catch (error) {
6256
- abortTimedOutRequests(error, params.client, params.sessionId);
6882
+ abortTimedOutRequests(error, lifetime);
6257
6883
  if (!(error instanceof TimeoutError) || !params.promptMessageId) throw error;
6258
6884
  await params.client.waitForSessionUpdatesIdle?.({
6259
6885
  idleMs: SESSION_REPLY_IDLE_MS,
6260
6886
  timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS
6261
6887
  }).catch(() => {});
6262
- if (hasAgentReplyAfterPrompt(params.conversation, params.promptMessageId)) return recoveredSessionResult(settledResponse, params.conversation, params.promptMessageId);
6888
+ if (settledResponse) return recoveredSessionResult(settledResponse, params.conversation, params.promptMessageId);
6263
6889
  throw error;
6890
+ } finally {
6891
+ lifetime.abort();
6264
6892
  }
6265
6893
  }
6266
6894
  //#endregion
@@ -6430,6 +7058,7 @@ async function applyPromptModelIfAdvertised(params) {
6430
7058
  models: advertisedModelState(params.record.acpx),
6431
7059
  agentCommand: params.record.agentCommand,
6432
7060
  timeoutMs: params.timeoutMs,
7061
+ authority: { signal: params.signal },
6433
7062
  onWarning: params.suppressWarnings ? void 0 : (message) => process.stderr.write(`[acpx] warning: ${message}\n`)
6434
7063
  });
6435
7064
  if (result.applied) params.record.acpx = applyModelSelection(params.record.acpx, requestedModel, result.response);
@@ -6510,8 +7139,9 @@ function emitPromptDisconnectNotice(snapshot, verbose) {
6510
7139
  if (!lastExit?.unexpectedDuringPrompt || !verbose) return;
6511
7140
  process.stderr.write("[acpx] agent disconnected during prompt (" + lastExit.reason + ", exit=" + lastExit.exitCode + ", signal=" + (lastExit.signal ?? "none") + ")\n");
6512
7141
  }
6513
- async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors) {
7142
+ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors, signal) {
6514
7143
  if (attempt < maxRetries && !hasSideEffects() && isRetryablePromptError(error)) {
7144
+ signal?.throwIfAborted();
6515
7145
  const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
6516
7146
  emitPromptRetryNotice({
6517
7147
  error,
@@ -6520,11 +7150,18 @@ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, su
6520
7150
  maxRetries,
6521
7151
  suppressSdkConsoleErrors
6522
7152
  });
6523
- await waitMs$1(delayMs);
7153
+ await setTimeout$1(delayMs, void 0, { signal }).catch((error) => {
7154
+ signal?.throwIfAborted();
7155
+ throw error;
7156
+ });
7157
+ signal?.throwIfAborted();
6524
7158
  return !hasSideEffects();
6525
7159
  }
6526
7160
  return false;
6527
7161
  }
7162
+ function isTurnCancellation(error, signal) {
7163
+ return signal?.aborted === true && error === signal.reason;
7164
+ }
6528
7165
  function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatter) {
6529
7166
  return {
6530
7167
  sessionRecordId,
@@ -6546,7 +7183,9 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
6546
7183
  sessionOptions: mergeSessionOptions(task.sessionOptions, options.sessionOptions),
6547
7184
  onClientAvailable: options.onClientAvailable,
6548
7185
  onClientClosed: options.onClientClosed,
7186
+ onClientCloseFailure: options.onClientCloseFailure,
6549
7187
  onPromptActive: options.onPromptActive,
7188
+ onPromptFinalizing: options.onPromptFinalizing,
6550
7189
  onPromptRequestWritten: () => {
6551
7190
  if (task.reportPromptStarted) task.send({
6552
7191
  type: "prompt_started",
@@ -6604,7 +7243,7 @@ async function waitForSessionTurn(options) {
6604
7243
  "SIGTERM",
6605
7244
  "SIGHUP"
6606
7245
  ];
6607
- for (const signal of signals) process.once(signal, onInterrupt);
7246
+ if (options.handleProcessInterrupts !== false) for (const signal of signals) process.once(signal, onInterrupt);
6608
7247
  const timeoutMs = options.timeoutMs;
6609
7248
  const timeout = timeoutMs != null && timeoutMs > 0 ? setTimeout(() => waiting.abort(new TimeoutError(timeoutMs)), timeoutMs) : void 0;
6610
7249
  const signal = options.waitSignal ? AbortSignal.any([waiting.signal, options.waitSignal]) : waiting.signal;
@@ -6615,18 +7254,22 @@ async function waitForSessionTurn(options) {
6615
7254
  ownership: await acquireSessionTurn(acpxRecordId, signal)
6616
7255
  };
6617
7256
  } catch (error) {
6618
- signal.throwIfAborted();
6619
- throw error;
7257
+ return throwTurnAcquisitionError(error, signal);
6620
7258
  } finally {
6621
7259
  clearTimeout(timeout);
6622
7260
  for (const signal of signals) process.off(signal, onInterrupt);
6623
7261
  }
6624
7262
  }
7263
+ function throwTurnAcquisitionError(error, signal) {
7264
+ if (signal.aborted && error instanceof Error && error.name === "AbortError" && error.cause === signal.reason) throw signal.reason;
7265
+ throw error;
7266
+ }
6625
7267
  async function runSessionPrompt(options) {
6626
7268
  let turn;
6627
7269
  try {
6628
7270
  turn = await waitForSessionTurn(options);
6629
7271
  } catch (error) {
7272
+ if (options.ownedSignal) throw error;
6630
7273
  if (options.waitSignal?.aborted && error === options.waitSignal.reason) {
6631
7274
  const record = await resolveSessionRecord(options.sessionRecordId);
6632
7275
  return {
@@ -6746,8 +7389,10 @@ async function runOwnedSessionPrompt(options) {
6746
7389
  const record = await measurePerf("session.resolve_prompt_record", async () => {
6747
7390
  return await resolveSessionRecord(options.sessionRecordId);
6748
7391
  });
7392
+ options.ownedSignal?.throwIfAborted();
6749
7393
  const { conversation, promptMessageId } = preparePromptConversation(record, options);
6750
7394
  await writeSessionRecord(record);
7395
+ options.ownedSignal?.throwIfAborted();
6751
7396
  output.setContext({ sessionId: record.acpxRecordId });
6752
7397
  const eventWriter = await measurePerf("session.events.open", async () => {
6753
7398
  return await SessionEventWriter.open(record);
@@ -6790,7 +7435,7 @@ async function runOwnedSessionPrompt(options) {
6790
7435
  if (options.verbose) process.stderr.write("[acpx] live session checkpoint failed: " + formatErrorMessage(error) + "\n");
6791
7436
  }
6792
7437
  });
6793
- let closeClientOnExit = options.client == null;
7438
+ let closeClientOnExit = options.client == null || options.ownedSignal !== void 0;
6794
7439
  const client = options.client ?? new AcpClient({
6795
7440
  agentCommand: record.agentCommand,
6796
7441
  agentArgv: record.agentArgv,
@@ -6816,6 +7461,7 @@ async function runOwnedSessionPrompt(options) {
6816
7461
  suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
6817
7462
  verbose: options.verbose
6818
7463
  });
7464
+ const closeOwnedClient = options.closeProvidedClient ?? ownDirectClient(client, options.ownedSignal);
6819
7465
  client.setEventHandlers({
6820
7466
  onAcpMessage: (direction, message) => {
6821
7467
  pendingMessages.push(message);
@@ -6852,24 +7498,23 @@ async function runOwnedSessionPrompt(options) {
6852
7498
  });
6853
7499
  let activeSessionIdForControl = record.acpSessionId;
6854
7500
  let notifiedClientAvailable = false;
7501
+ let controlRetirement;
6855
7502
  const activeController = {
6856
7503
  hasActivePrompt: () => client.hasActivePrompt(),
6857
7504
  requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
6858
- setSessionMode: async (modeId) => {
6859
- await client.setSessionMode(activeSessionIdForControl, modeId);
6860
- },
6861
- setSessionModel: async (modelId) => {
6862
- const models = advertisedModelState(record.acpx);
6863
- const response = await client.setSessionModel(activeSessionIdForControl, modelId, models);
6864
- record.acpx = applyModelSelection(record.acpx, modelId, response);
6865
- return response;
6866
- },
6867
- setSessionConfigOption: async (configId, value) => {
6868
- const modelConfigId = advertisedModelState(record.acpx)?.configId;
6869
- const response = await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
6870
- record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response, modelConfigId);
6871
- return response;
6872
- }
7505
+ ...createOwnedSessionControls({
7506
+ client,
7507
+ record,
7508
+ sessionId: () => activeSessionIdForControl,
7509
+ checkpoint: () => liveCheckpoint.checkpoint(),
7510
+ retire: () => {
7511
+ controlRetirement ??= client.close().catch((error) => {
7512
+ options.onClientCloseFailure?.();
7513
+ throw error;
7514
+ });
7515
+ return controlRetirement;
7516
+ }
7517
+ })
6873
7518
  };
6874
7519
  const flushConnectOutput = (loadError) => {
6875
7520
  bufferingConnectOutput = false;
@@ -6890,6 +7535,7 @@ async function runOwnedSessionPrompt(options) {
6890
7535
  verbose: options.verbose,
6891
7536
  suppressWarnings: options.suppressSdkConsoleErrors,
6892
7537
  activeController,
7538
+ authority: { signal: options.ownedSignal },
6893
7539
  onClientAvailable: (controller) => {
6894
7540
  options.onClientAvailable?.(controller);
6895
7541
  notifiedClientAvailable = true;
@@ -6933,15 +7579,17 @@ async function runOwnedSessionPrompt(options) {
6933
7579
  conversation,
6934
7580
  promptMessageId,
6935
7581
  onPromptRequestWritten: options.onPromptRequestWritten,
6936
- onPromptStarted: buildPromptStartedHook(attempt)
7582
+ onPromptStarted: buildPromptStartedHook(attempt),
7583
+ authority: { signal: options.waitSignal }
6937
7584
  });
6938
7585
  });
6939
7586
  emitPromptPerfMetric(promptStartedAt, options.verbose);
6940
7587
  return response;
6941
7588
  };
6942
7589
  const handlePromptFailure = async (error, attempt) => {
7590
+ if (isTurnCancellation(error, options.waitSignal)) throw error;
6943
7591
  const snapshot = client.getAgentLifecycleSnapshot();
6944
- if (snapshot.lastExit?.unexpectedDuringPrompt !== true && await preparePromptRetry(error, attempt, options.promptRetries ?? 0, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors)) return;
7592
+ if (snapshot.lastExit?.unexpectedDuringPrompt !== true && await preparePromptRetry(error, attempt, options.promptRetries ?? 0, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors, options.waitSignal)) return;
6945
7593
  await failRuntimePrompt(error, snapshot);
6946
7594
  };
6947
7595
  const failRuntimePrompt = async (error, snapshot) => {
@@ -6965,10 +7613,18 @@ async function runOwnedSessionPrompt(options) {
6965
7613
  };
6966
7614
  const runPromptWithRetries = async (sessionId) => {
6967
7615
  promptTurnActive = true;
6968
- for (let attempt = 0;; attempt++) try {
6969
- return await runPromptAttempt(sessionId, attempt);
7616
+ try {
7617
+ for (let attempt = 0;; attempt++) {
7618
+ options.waitSignal?.throwIfAborted();
7619
+ try {
7620
+ return await runPromptAttempt(sessionId, attempt);
7621
+ } catch (error) {
7622
+ await handlePromptFailure(error, attempt);
7623
+ }
7624
+ }
6970
7625
  } catch (error) {
6971
- await handlePromptFailure(error, attempt);
7626
+ if (isTurnCancellation(error, options.waitSignal)) return { stopReason: "cancelled" };
7627
+ throw error;
6972
7628
  }
6973
7629
  };
6974
7630
  const savePromptSuccess = async (response) => {
@@ -6986,15 +7642,19 @@ async function runOwnedSessionPrompt(options) {
6986
7642
  return response;
6987
7643
  };
6988
7644
  const runPrompt = async () => {
7645
+ options.ownedSignal?.throwIfAborted();
6989
7646
  const { sessionId: activeSessionId, resumed, loadError } = await connectForPrompt();
7647
+ options.ownedSignal?.throwIfAborted();
6990
7648
  await applyPromptModelIfAdvertised({
6991
7649
  client,
6992
7650
  sessionId: activeSessionId,
6993
7651
  requestedModel: options.sessionOptions?.model,
6994
7652
  record,
6995
7653
  timeoutMs: options.timeoutMs,
6996
- suppressWarnings: options.suppressSdkConsoleErrors
7654
+ suppressWarnings: options.suppressSdkConsoleErrors,
7655
+ signal: options.ownedSignal
6997
7656
  });
7657
+ options.ownedSignal?.throwIfAborted();
6998
7658
  output.setContext({ sessionId: record.acpxRecordId });
6999
7659
  await liveCheckpoint.checkpoint();
7000
7660
  const response = await savePromptSuccess(await runPromptWithRetries(activeSessionId));
@@ -7012,24 +7672,44 @@ async function runOwnedSessionPrompt(options) {
7012
7672
  record.lastUsedAt = isoNow$1();
7013
7673
  applyConversation(record, conversation);
7014
7674
  await flushPendingMessages(false).catch(() => {});
7015
- if (closeClientOnExit) await client.close();
7675
+ if (closeClientOnExit) await closeOwnedClient();
7016
7676
  };
7017
7677
  const runObservedPrompt = async () => {
7018
7678
  try {
7019
- return await runWithOptionalInterrupt({
7679
+ const result = await runWithOptionalInterrupt({
7020
7680
  handleProcessInterrupts: options.handleProcessInterrupts,
7021
7681
  run: runPrompt,
7022
7682
  handleInterrupt
7023
7683
  });
7684
+ options.ownedSignal?.throwIfAborted();
7685
+ return result;
7024
7686
  } catch (error) {
7025
- const matchedAcpError = acpErrors.match(error);
7026
- attachAcpErrorPayload(error, matchedAcpError);
7027
- markOutputAlreadyEmitted(error, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
7028
- throw error;
7687
+ const failure = directExecutionError(error, options.ownedSignal);
7688
+ const matchedAcpError = acpErrors.match(failure);
7689
+ attachAcpErrorPayload(failure, matchedAcpError);
7690
+ markOutputAlreadyEmitted(failure, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
7691
+ throw failure;
7029
7692
  }
7030
7693
  };
7031
7694
  const cleanupPrompt = async (attempt) => {
7695
+ const controlDrain = options.onPromptFinalizing?.();
7696
+ const unresolvedPrompt = client.hasUnresolvedPrompt();
7032
7697
  const steps = [
7698
+ async () => {
7699
+ if (unresolvedPrompt) await withTimeout(client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS), INTERRUPT_CANCEL_WAIT_MS).catch(() => {});
7700
+ },
7701
+ async () => {
7702
+ const journalFailed = attempt.status === "rejected" && attempt.error instanceof AcpxOperationalError && attempt.error.detailCode === "SESSION_JOURNAL_WRITE_FAILED";
7703
+ if (unresolvedPrompt || closeClientOnExit || journalFailed) try {
7704
+ await closeOwnedClient();
7705
+ } catch (error) {
7706
+ options.onClientCloseFailure?.();
7707
+ throw error;
7708
+ }
7709
+ },
7710
+ async () => {
7711
+ await controlDrain;
7712
+ },
7033
7713
  () => {
7034
7714
  const duration = stopTotalTimer();
7035
7715
  if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", duration)}\n`);
@@ -7038,10 +7718,6 @@ async function runOwnedSessionPrompt(options) {
7038
7718
  if (notifiedClientAvailable) options.onClientClosed?.();
7039
7719
  },
7040
7720
  () => client.clearEventHandlers(),
7041
- async () => {
7042
- const journalFailed = attempt.status === "rejected" && attempt.error instanceof AcpxOperationalError && attempt.error.detailCode === "SESSION_JOURNAL_WRITE_FAILED";
7043
- if (closeClientOnExit || journalFailed) await client.close();
7044
- },
7045
7721
  () => applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot()),
7046
7722
  () => applyConversation(record, conversation)
7047
7723
  ];
@@ -7063,11 +7739,13 @@ async function runOwnedSessionPrompt(options) {
7063
7739
  cleanup: cleanupPrompt
7064
7740
  });
7065
7741
  }
7066
- async function runOnce(options) {
7742
+ async function runOnce(options, control) {
7743
+ const authority = { signal: control?.signal };
7067
7744
  const output = options.outputFormatter;
7068
7745
  const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
7069
7746
  let promptTurnActive = false;
7070
7747
  let promptTurnHadSideEffects = false;
7748
+ let controlState = {};
7071
7749
  const acpErrors = new AcpErrorTracker();
7072
7750
  const client = new AcpClient({
7073
7751
  agentCommand: options.agentCommand,
@@ -7088,6 +7766,7 @@ async function runOnce(options) {
7088
7766
  acpErrors.observe(output, direction, message);
7089
7767
  },
7090
7768
  onSessionUpdate: (notification) => {
7769
+ if (notification.update.sessionUpdate === "config_option_update") controlState = applyConfigOptionsToState(controlState, notification.update.configOptions);
7091
7770
  if (promptTurnActive) promptTurnHadSideEffects = true;
7092
7771
  options.onSessionUpdate?.(notification);
7093
7772
  },
@@ -7101,10 +7780,12 @@ async function runOnce(options) {
7101
7780
  },
7102
7781
  sessionOptions: options.sessionOptions
7103
7782
  });
7783
+ const closeOwnedClient = ownDirectClient(client, control?.signal);
7104
7784
  const runExecPromptAttempt = async (sessionId) => {
7785
+ assertControlAuthority(authority);
7105
7786
  acpErrors.reset();
7106
7787
  return await measurePerf("runtime.exec.prompt", async () => {
7107
- return await withTimeout(client.prompt(sessionId, options.prompt), options.timeoutMs);
7788
+ return await withTimeout(client.prompt(sessionId, options.prompt, void 0, void 0, void 0, { signal: control?.signal }), options.timeoutMs);
7108
7789
  });
7109
7790
  };
7110
7791
  const runExecPromptWithRetries = async (sessionId) => {
@@ -7113,72 +7794,102 @@ async function runOnce(options) {
7113
7794
  for (let attempt = 0;; attempt++) try {
7114
7795
  return await runExecPromptAttempt(sessionId);
7115
7796
  } catch (error) {
7116
- if (await preparePromptRetry(error, attempt, maxRetries, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors)) continue;
7797
+ if (await preparePromptRetry(error, attempt, maxRetries, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors, control?.signal)) continue;
7117
7798
  promptTurnActive = false;
7118
7799
  throw error;
7119
7800
  }
7120
7801
  };
7121
7802
  try {
7122
- return await withInterrupt(async () => {
7123
- await measurePerf("runtime.exec.start", async () => {
7124
- await withTimeout(client.start(), options.timeoutMs);
7125
- });
7126
- const createdSession = await measurePerf("runtime.exec.create_session", async () => {
7127
- return await withTimeout(client.createSession(absolutePath(options.cwd)), options.timeoutMs);
7128
- });
7129
- const sessionId = createdSession.sessionId;
7130
- await applyRequestedModelIfAdvertised({
7131
- client,
7132
- sessionId,
7133
- requestedModel: options.sessionOptions?.model,
7134
- models: createdSession.models,
7135
- agentCommand: options.agentCommand,
7136
- timeoutMs: options.timeoutMs,
7137
- onWarning: options.suppressSdkConsoleErrors ? void 0 : (message) => process.stderr.write(`[acpx] warning: ${message}\n`)
7138
- });
7139
- for (const configOption of options.configOptions ?? []) await withTimeout(client.setSessionConfigOption(sessionId, configOption.configId, configOption.value), options.timeoutMs);
7140
- output.setContext({ sessionId });
7141
- const response = await runExecPromptWithRetries(sessionId);
7142
- promptTurnActive = false;
7143
- output.flush();
7144
- return toPromptResult(response.stopReason, sessionId, client, response._meta);
7145
- }, async () => {
7146
- await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
7147
- await client.close();
7803
+ return await runWithOptionalInterrupt({
7804
+ handleProcessInterrupts: control?.handleProcessInterrupts,
7805
+ run: async () => {
7806
+ assertControlAuthority(authority);
7807
+ await measurePerf("runtime.exec.start", async () => {
7808
+ await withTimeout(client.start(authority), options.timeoutMs);
7809
+ });
7810
+ assertControlAuthority(authority);
7811
+ const createdSession = await measurePerf("runtime.exec.create_session", async () => {
7812
+ return await withTimeout(client.createSession(absolutePath(options.cwd), authority), options.timeoutMs);
7813
+ });
7814
+ assertControlAuthority(authority);
7815
+ const sessionId = createdSession.sessionId;
7816
+ if (createdSession.models) applyAdvertisedModelState(controlState, createdSession.models);
7817
+ controlState = applyConfigOptionsToState(controlState, createdSession.configOptions ?? []);
7818
+ const modelApplication = await applyRequestedModelIfAdvertised({
7819
+ client,
7820
+ sessionId,
7821
+ requestedModel: options.sessionOptions?.model,
7822
+ models: createdSession.models,
7823
+ agentCommand: options.agentCommand,
7824
+ timeoutMs: options.timeoutMs,
7825
+ authority,
7826
+ onWarning: options.suppressSdkConsoleErrors ? void 0 : (message) => process.stderr.write(`[acpx] warning: ${message}\n`)
7827
+ });
7828
+ if (modelApplication.response) controlState = applyConfigOptionsToState(controlState, modelApplication.response.configOptions);
7829
+ for (const configOption of options.configOptions ?? []) {
7830
+ assertControlAuthority(authority);
7831
+ const response = await withTimeout(client.setSessionConfigOption(sessionId, configOption.configId, configOption.value, advertisedModelState(controlState), authority), options.timeoutMs);
7832
+ controlState = applyConfigOptionsToState(controlState, response.configOptions);
7833
+ }
7834
+ assertControlAuthority(authority);
7835
+ output.setContext({ sessionId });
7836
+ const response = await runExecPromptWithRetries(sessionId);
7837
+ promptTurnActive = false;
7838
+ return toPromptResult(response.stopReason, sessionId, client, response._meta);
7839
+ },
7840
+ handleInterrupt: async () => {
7841
+ await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
7842
+ await closeOwnedClient();
7843
+ }
7148
7844
  });
7149
7845
  } catch (error) {
7150
- const matchedAcpError = acpErrors.match(error);
7151
- attachAcpErrorPayload(error, matchedAcpError);
7152
- markOutputAlreadyEmitted(error, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
7153
- throw error;
7846
+ const failure = directExecutionError(error, authority.signal);
7847
+ const matchedAcpError = acpErrors.match(failure);
7848
+ attachAcpErrorPayload(failure, matchedAcpError);
7849
+ markOutputAlreadyEmitted(failure, matchedAcpError !== void 0 && shouldMarkAcpErrorsEmitted);
7850
+ throw failure;
7154
7851
  } finally {
7155
- await client.close();
7852
+ try {
7853
+ await closeOwnedClient();
7854
+ } finally {
7855
+ output.flush();
7856
+ }
7156
7857
  }
7157
7858
  }
7158
- async function sendSessionDirect(options) {
7159
- return await runSessionPrompt({
7160
- sessionRecordId: options.sessionId,
7161
- prompt: options.prompt,
7162
- mcpServers: options.mcpServers,
7163
- permissionMode: options.permissionMode,
7164
- resumePolicy: options.resumePolicy,
7165
- nonInteractivePermissions: options.nonInteractivePermissions,
7166
- permissionPolicy: options.permissionPolicy,
7167
- authCredentials: options.authCredentials,
7168
- authPolicy: options.authPolicy,
7169
- fs: options.fs,
7170
- terminal: options.terminal,
7171
- outputFormatter: options.outputFormatter,
7172
- errorEmissionPolicy: options.errorEmissionPolicy,
7173
- onAcpMessage: options.onAcpMessage,
7174
- onSessionUpdate: options.onSessionUpdate,
7175
- onClientOperation: options.onClientOperation,
7176
- onPermissionEscalation: options.onPermissionEscalation,
7177
- timeoutMs: options.timeoutMs,
7178
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
7179
- verbose: options.verbose,
7180
- client: options.client
7181
- });
7859
+ async function sendSessionDirect(options, control) {
7860
+ const closeProvidedClient = control && options.client ? ownDirectClient(options.client, control.signal) : void 0;
7861
+ try {
7862
+ control?.signal.throwIfAborted();
7863
+ return await runSessionPrompt({
7864
+ ownedSignal: control?.signal,
7865
+ closeProvidedClient,
7866
+ waitSignal: control?.signal,
7867
+ handleProcessInterrupts: control?.handleProcessInterrupts,
7868
+ sessionRecordId: options.sessionId,
7869
+ prompt: options.prompt,
7870
+ mcpServers: options.mcpServers,
7871
+ permissionMode: options.permissionMode,
7872
+ resumePolicy: options.resumePolicy,
7873
+ nonInteractivePermissions: options.nonInteractivePermissions,
7874
+ permissionPolicy: options.permissionPolicy,
7875
+ authCredentials: options.authCredentials,
7876
+ authPolicy: options.authPolicy,
7877
+ fs: options.fs,
7878
+ terminal: options.terminal,
7879
+ outputFormatter: options.outputFormatter,
7880
+ errorEmissionPolicy: options.errorEmissionPolicy,
7881
+ onAcpMessage: options.onAcpMessage,
7882
+ onSessionUpdate: options.onSessionUpdate,
7883
+ onClientOperation: options.onClientOperation,
7884
+ onPermissionEscalation: options.onPermissionEscalation,
7885
+ timeoutMs: options.timeoutMs,
7886
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
7887
+ verbose: options.verbose,
7888
+ client: options.client
7889
+ });
7890
+ } finally {
7891
+ await closeProvidedClient?.();
7892
+ }
7182
7893
  }
7183
7894
  //#endregion
7184
7895
  //#region src/session/execution/queue-owner-runtime.ts
@@ -7231,54 +7942,6 @@ function createQueueOwnerSharedClient(options, sessionRecord) {
7231
7942
  sessionOptions: mergeSessionOptions(options.sessionOptions, sessionOptionsFromRecord(sessionRecord))
7232
7943
  });
7233
7944
  }
7234
- function createQueueOwnerTurnController(options) {
7235
- return new QueueOwnerTurnController({
7236
- withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
7237
- setSessionModeFallback: async (modeId, timeoutMs) => {
7238
- await runSessionSetModeDirect({
7239
- sessionRecordId: options.sessionId,
7240
- modeId,
7241
- mcpServers: options.mcpServers,
7242
- nonInteractivePermissions: options.nonInteractivePermissions,
7243
- authCredentials: options.authCredentials,
7244
- authPolicy: options.authPolicy,
7245
- fs: options.fs,
7246
- terminal: options.terminal,
7247
- timeoutMs,
7248
- verbose: options.verbose
7249
- });
7250
- },
7251
- setSessionModelFallback: async (modelId, timeoutMs) => {
7252
- return (await runSessionSetModelDirect({
7253
- sessionRecordId: options.sessionId,
7254
- modelId,
7255
- mcpServers: options.mcpServers,
7256
- nonInteractivePermissions: options.nonInteractivePermissions,
7257
- authCredentials: options.authCredentials,
7258
- authPolicy: options.authPolicy,
7259
- fs: options.fs,
7260
- terminal: options.terminal,
7261
- timeoutMs,
7262
- verbose: options.verbose
7263
- })).response;
7264
- },
7265
- setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
7266
- return (await runSessionSetConfigOptionDirect({
7267
- sessionRecordId: options.sessionId,
7268
- configId,
7269
- value,
7270
- mcpServers: options.mcpServers,
7271
- nonInteractivePermissions: options.nonInteractivePermissions,
7272
- authCredentials: options.authCredentials,
7273
- authPolicy: options.authPolicy,
7274
- fs: options.fs,
7275
- terminal: options.terminal,
7276
- timeoutMs,
7277
- verbose: options.verbose
7278
- })).response;
7279
- }
7280
- });
7281
- }
7282
7945
  function logDeferredCancelFailure(error, verbose) {
7283
7946
  if (!verbose) return;
7284
7947
  process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
@@ -7322,16 +7985,17 @@ function createQueueOwnerShutdownController(params) {
7322
7985
  let shutdownPromise;
7323
7986
  const drainActiveTurn = async () => {
7324
7987
  const turn = activeTurn;
7325
- if (!turn) return;
7326
- params.turnController.requestCancel().catch((error) => {
7988
+ const pending = Promise.allSettled([turn, params.controls.drainAdmitted()]);
7989
+ if (turn) params.turnController.requestCancel().catch((error) => {
7327
7990
  logDeferredCancelFailure(error, params.verbose);
7328
7991
  });
7329
- if (!await settlesWithin(turn, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
7330
- await turn.catch(() => {});
7992
+ if (!await settlesWithin(pending, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
7993
+ await pending;
7331
7994
  };
7332
7995
  const request = () => {
7333
7996
  requested = true;
7334
7997
  params.stopHeartbeat();
7998
+ params.controls.stopAdmission();
7335
7999
  params.getOwner()?.beginShutdown();
7336
8000
  activeTurnShutdown ??= drainActiveTurn();
7337
8001
  };
@@ -7342,14 +8006,20 @@ function createQueueOwnerShutdownController(params) {
7342
8006
  request,
7343
8007
  setActiveTurn: (turn) => {
7344
8008
  activeTurn = turn;
8009
+ if (requested) activeTurnShutdown = Promise.all([activeTurnShutdown, drainActiveTurn()]).then(() => {});
7345
8010
  },
7346
8011
  clearActiveTurn: (turn) => {
7347
8012
  if (activeTurn === turn) activeTurn = void 0;
7348
8013
  },
8014
+ drain: () => {
8015
+ request();
8016
+ return activeTurnShutdown;
8017
+ },
7349
8018
  shutdown: () => {
7350
8019
  request();
7351
8020
  shutdownPromise ??= (async () => {
7352
8021
  await activeTurnShutdown;
8022
+ await params.getBackendClose()?.catch(() => {});
7353
8023
  await closeQueueOwnerRuntime({
7354
8024
  owner: params.getOwner(),
7355
8025
  turnController: params.turnController,
@@ -7368,20 +8038,50 @@ function applyRequestedShutdown(owner, shutdown) {
7368
8038
  function startQueueOwnerHeartbeat(params) {
7369
8039
  if (!params.enabled) return;
7370
8040
  return setInterval(() => {
7371
- refreshQueueOwnerLease(params.lease, { queueDepth: params.owner.queueDepth() }).catch(() => {});
8041
+ params.refresh(params.owner.queueDepth());
7372
8042
  }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
7373
8043
  }
8044
+ function createLeaseHeartbeat(lease, shutdown) {
8045
+ let guardFailure;
8046
+ return {
8047
+ refresh: async (queueDepth) => {
8048
+ if (shutdown.requested) return;
8049
+ await refreshQueueOwnerLease(lease, { queueDepth }).catch((error) => {
8050
+ if (error instanceof QueueLeaseGuardSettlementError) {
8051
+ guardFailure ??= error;
8052
+ shutdown.request();
8053
+ }
8054
+ });
8055
+ },
8056
+ throwIfFailed: () => {
8057
+ if (guardFailure) throw guardFailure;
8058
+ }
8059
+ };
8060
+ }
7374
8061
  async function runSessionQueueOwner(options) {
7375
8062
  const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
7376
8063
  path: options.mcpConfigPath,
7377
8064
  fingerprint: options.mcpConfigFingerprint
7378
8065
  });
7379
8066
  if (!lease) return;
8067
+ let runtimeFailure;
7380
8068
  try {
7381
8069
  await runQueueOwnerRuntime(options, lease);
7382
- } finally {
8070
+ } catch (error) {
8071
+ runtimeFailure = { error };
8072
+ }
8073
+ try {
7383
8074
  await releaseQueueOwnerLease(lease);
8075
+ } catch (error) {
8076
+ if (runtimeFailure) throw new AggregateError([runtimeFailure.error, error], "Queue owner shutdown failed", { cause: error });
8077
+ throw error;
7384
8078
  }
8079
+ if (runtimeFailure) throw runtimeFailure.error;
8080
+ }
8081
+ async function keepOwnerForPendingControls(controls, shutdown) {
8082
+ if (shutdown.requested || !controls.hasPending) return false;
8083
+ await controls.drainAdmitted();
8084
+ return !shutdown.requested;
7385
8085
  }
7386
8086
  async function runQueueOwnerRuntime(options, lease) {
7387
8087
  const sessionRecord = await resolveSessionRecord(options.sessionId);
@@ -7392,7 +8092,16 @@ async function runQueueOwnerRuntime(options, lease) {
7392
8092
  const maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth ?? 16));
7393
8093
  const taskPollTimeoutMs = ttlMs === 0 ? void 0 : ttlMs;
7394
8094
  const initialTaskPollTimeoutMs = taskPollTimeoutMs == null ? void 0 : Math.max(taskPollTimeoutMs, 1e3);
7395
- const turnController = createQueueOwnerTurnController(options);
8095
+ const turnController = new QueueOwnerTurnController();
8096
+ let activeControls;
8097
+ let backendCloseRequest;
8098
+ const controls = new QueueOwnerControlAdmission({ runIdle: async (invoke, controlOptions) => await runIdleOwnerControl({
8099
+ ...controlOptions,
8100
+ client: sharedClient,
8101
+ sessionId: options.sessionId,
8102
+ verbose: options.verbose,
8103
+ onRetirementFailure: () => shutdown.request()
8104
+ }, invoke) });
7396
8105
  const applyPendingCancel = async () => {
7397
8106
  return await turnController.applyPendingCancel();
7398
8107
  };
@@ -7402,23 +8111,34 @@ async function runQueueOwnerRuntime(options, lease) {
7402
8111
  });
7403
8112
  };
7404
8113
  const setActiveController = (controller) => {
8114
+ activeControls = controller;
7405
8115
  turnController.setActiveController(controller);
7406
8116
  scheduleApplyPendingCancel();
7407
8117
  };
7408
8118
  const clearActiveController = () => {
8119
+ activeControls = void 0;
7409
8120
  turnController.clearActiveController();
7410
8121
  };
7411
- const closeActiveBackendSession = async (timeoutMs) => {
7412
- const latestRecord = await resolveSessionRecord(options.sessionId);
7413
- if (!sharedClient.supportsCloseSession()) return false;
7414
- await withTimeout(sharedClient.closeSession(latestRecord.acpSessionId), timeoutMs);
7415
- return true;
8122
+ const closeActiveBackendSession = (timeoutMs) => {
8123
+ backendCloseRequest ??= Promise.resolve().then(async () => {
8124
+ await shutdown.drain();
8125
+ const latestRecord = await resolveSessionRecord(options.sessionId);
8126
+ if (!sharedClient.supportsCloseSession()) return false;
8127
+ await withTimeout(sharedClient.closeSession(latestRecord.acpSessionId), timeoutMs);
8128
+ return true;
8129
+ });
8130
+ return backendCloseRequest;
7416
8131
  };
7417
8132
  const runPromptTurn = async (run) => {
7418
8133
  const waitSignal = turnController.beginTurn();
8134
+ const { ticket, priorIdle } = controls.beginPrompt();
8135
+ if (shutdown.requested) turnController.requestCancel().catch(() => {});
7419
8136
  try {
7420
- return await run(waitSignal);
8137
+ await priorIdle;
8138
+ return await run(waitSignal, ticket);
7421
8139
  } finally {
8140
+ await controls.seal(ticket);
8141
+ controls.release(ticket);
7422
8142
  turnController.endTurn();
7423
8143
  }
7424
8144
  };
@@ -7431,6 +8151,8 @@ async function runQueueOwnerRuntime(options, lease) {
7431
8151
  }
7432
8152
  },
7433
8153
  turnController,
8154
+ controls,
8155
+ getBackendClose: () => backendCloseRequest,
7434
8156
  sharedClient,
7435
8157
  sessionId: options.sessionId,
7436
8158
  verbose: options.verbose
@@ -7438,6 +8160,7 @@ async function runQueueOwnerRuntime(options, lease) {
7438
8160
  const onSignal = () => {
7439
8161
  shutdown.request();
7440
8162
  };
8163
+ const heartbeat = createLeaseHeartbeat(lease, shutdown);
7441
8164
  process.once("SIGINT", onSignal);
7442
8165
  process.once("SIGTERM", onSignal);
7443
8166
  process.once("SIGHUP", onSignal);
@@ -7450,17 +8173,17 @@ async function runQueueOwnerRuntime(options, lease) {
7450
8173
  },
7451
8174
  closeSession: async (timeoutMs) => await closeActiveBackendSession(timeoutMs),
7452
8175
  setSessionMode: async (modeId, timeoutMs) => {
7453
- await turnController.setSessionMode(modeId, timeoutMs);
8176
+ await controls.run((context, deadline) => context.setSessionMode(modeId, deadline), timeoutMs);
7454
8177
  },
7455
- setSessionModel: async (modelId, timeoutMs) => await turnController.setSessionModel(modelId, timeoutMs),
8178
+ setSessionModel: async (modelId, timeoutMs) => await controls.run((context, deadline) => context.setSessionModel(modelId, deadline), timeoutMs, { key: "model" }),
7456
8179
  setSessionConfigOption: async (configId, value, timeoutMs) => {
7457
- return await turnController.setSessionConfigOption(configId, value, timeoutMs);
8180
+ return await controls.run((context, deadline) => context.setSessionConfigOption(configId, value, deadline), timeoutMs, { key: configId });
7458
8181
  }
7459
8182
  }, {
7460
8183
  maxQueueDepth,
7461
8184
  onQueueDepthChanged: (queueDepth) => {
7462
8185
  setPerfGauge("queue.owner.depth", queueDepth);
7463
- refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
8186
+ heartbeat.refresh(queueDepth);
7464
8187
  }
7465
8188
  });
7466
8189
  applyRequestedShutdown(owner, shutdown);
@@ -7470,19 +8193,22 @@ async function runQueueOwnerRuntime(options, lease) {
7470
8193
  maxQueueDepth,
7471
8194
  verbose: options.verbose
7472
8195
  });
7473
- await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
8196
+ await heartbeat.refresh(owner.queueDepth());
7474
8197
  heartbeatTimer = startQueueOwnerHeartbeat({
7475
8198
  enabled: !shutdown.requested,
7476
- lease,
8199
+ refresh: heartbeat.refresh,
7477
8200
  owner
7478
8201
  });
7479
8202
  let isFirstTask = true;
7480
8203
  while (true) {
7481
8204
  const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
7482
8205
  const task = await owner.nextTask(pollTimeoutMs);
7483
- if (!task) break;
8206
+ if (!task) {
8207
+ if (await keepOwnerForPendingControls(controls, shutdown)) continue;
8208
+ break;
8209
+ }
7484
8210
  isFirstTask = false;
7485
- const turnPromise = runPromptTurn(async (waitSignal) => {
8211
+ const turnPromise = runPromptTurn(async (waitSignal, ticket) => {
7486
8212
  try {
7487
8213
  await runQueuedTask(options.sessionId, task, {
7488
8214
  sharedClient,
@@ -7496,7 +8222,10 @@ async function runQueueOwnerRuntime(options, lease) {
7496
8222
  sessionOptions: options.sessionOptions,
7497
8223
  onClientAvailable: setActiveController,
7498
8224
  onClientClosed: clearActiveController,
8225
+ onClientCloseFailure: shutdown.request,
8226
+ onPromptFinalizing: () => controls.seal(ticket),
7499
8227
  onPromptActive: async () => {
8228
+ if (activeControls) controls.publish(ticket, activeControls);
7500
8229
  turnController.markPromptActive();
7501
8230
  await applyPendingCancel();
7502
8231
  },
@@ -7520,7 +8249,9 @@ async function runQueueOwnerRuntime(options, lease) {
7520
8249
  process.off("SIGTERM", onSignal);
7521
8250
  process.off("SIGHUP", onSignal);
7522
8251
  await shutdown.shutdown();
8252
+ await lease.updates;
7523
8253
  }
8254
+ heartbeat.throwIfFailed();
7524
8255
  }
7525
8256
  async function sendSession(options) {
7526
8257
  options.signal?.throwIfAborted();
@@ -7560,6 +8291,6 @@ async function sendSession(options) {
7560
8291
  }
7561
8292
  const queueOwnerRuntimeTestInternals = { queueOwnerExitIsFatal };
7562
8293
  //#endregion
7563
- export { normalizeQueueOwnerTtlMs as $, setDesiredModeId as A, recordClientOperation as B, withConnectedSession as C, applyInitialModelSelection as D, applyConfigOptionsToRecord as E, createInitialSessionRecord as F, AcpClient as G, recordSessionUpdate as H, reconcileAgentSessionId as I, RequestedModelUnsupportedError as J, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE as K, cloneSessionAcpxState as L, sessionOptionsFromRecord as M, applyConversation as N, applyModelSelection as O, applyLifecycleSnapshotToRecord as P, DEFAULT_QUEUE_OWNER_TTL_MS as Q, cloneSessionConversation as R, setSessionModel as S, applyConfigOptionSelection as T, trimConversationForRuntime as U, recordPromptSubmission as V, advertisedModelState as W, getAcpxVersion as X, isRequestedModelUnsupportedError as Y, permissionModeSatisfies as Z, cancelSessionPrompt as _, runQueuedTask as a, withInterrupt as at, setSessionConfigOption as b, LiveSessionCheckpoint as c, normalizeOutputError as ct, flushPerfMetricsCapture as d, listSessionEvents as et, installPerfMetricsCapture as f, listAgentSessions as g, ensureSession as h, runOnce as i, assertControlAuthority as it, persistSessionOptions as j, applyRequestedModelIfAdvertised as k, runPromptTurn as l, createSessionWithClient as m, runSessionQueueOwner as n, InterruptedError as nt, sendSessionDirect as o, withTimeout as ot, createSession as p, REQUESTED_MODEL_UNSUPPORTED_REASONS as q, sendSession as r, TimeoutError as rt, DISCARD_OUTPUT_FORMATTER as s, exitCodeForOutputErrorCode as st, queueOwnerRuntimeTestInternals as t, isLegacyZedCodexAcpInvocation as tt, buildQueueOwnerArgOverride as u, closeSession as v, connectAndLoadSession as w, setSessionMode as x, sessionControlTestInternals as y, createSessionConversation as z };
8294
+ export { getAcpxVersion as $, applyInitialModelSelection as A, cloneSessionAcpxState as B, setSessionModeOnOwner as C, connectAndLoadSession as D, withConnectedSession as E, sessionOptionsFromRecord as F, recordSessionUpdate as G, createSessionConversation as H, applyConversation as I, AcpClient as J, trimConversationForRuntime as K, applyLifecycleSnapshotToRecord as L, applyRequestedModelIfAdvertised as M, setDesiredModeId as N, applyConfigOptionSelection as O, persistSessionOptions as P, isRequestedModelUnsupportedError as Q, createInitialSessionRecord as R, setSessionMode as S, setSessionModelOnOwner as T, recordClientOperation as U, cloneSessionConversation as V, recordPromptSubmission as W, REQUESTED_MODEL_UNSUPPORTED_REASONS as X, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE as Y, RequestedModelUnsupportedError as Z, cancelSessionPrompt as _, runQueuedTask as a, InterruptedError as at, setSessionConfigOption as b, LiveSessionCheckpoint as c, withInterrupt as ct, flushPerfMetricsCapture as d, normalizeOutputError as dt, permissionModeSatisfies as et, installPerfMetricsCapture as f, listAgentSessions as g, ensureSession as h, runOnce as i, isLegacyZedCodexAcpInvocation as it, applyModelSelection as j, applyConfigOptionsToRecord as k, runPromptTurn as l, withTimeout as lt, createSessionWithClient as m, runSessionQueueOwner as n, normalizeQueueOwnerTtlMs as nt, sendSessionDirect as o, TimeoutError as ot, createSession as p, advertisedModelState as q, sendSession as r, listSessionEvents as rt, DISCARD_OUTPUT_FORMATTER as s, assertControlAuthority as st, queueOwnerRuntimeTestInternals as t, DEFAULT_QUEUE_OWNER_TTL_MS as tt, buildQueueOwnerArgOverride as u, exitCodeForOutputErrorCode as ut, closeSession as v, setSessionModel as w, setSessionConfigOptionOnOwner as x, sessionControlTestInternals as y, reconcileAgentSessionId as z };
7564
8295
 
7565
- //# sourceMappingURL=queue-owner-runtime-Dgv3e2Tp.js.map
8296
+ //# sourceMappingURL=queue-owner-runtime-CLwjMYNL.js.map