acpx 0.17.0 → 0.18.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 (37) hide show
  1. package/dist/{agent-registry-DU6mhZBL.js → agent-registry-DdKB-zia.js} +20 -17
  2. package/dist/agent-registry-DdKB-zia.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-CiyfEC7L.js → cli-8mVHeUXh.js} +5 -5
  6. package/dist/{cli-CiyfEC7L.js.map → cli-8mVHeUXh.js.map} +1 -1
  7. package/dist/cli.js +122 -94
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{client-C6NUFqr-.d.ts → client-DeNKZZcu.d.ts} +19 -13
  10. package/dist/client-DeNKZZcu.d.ts.map +1 -0
  11. package/dist/{flows-CTOdIk-m.js → flows-WOQNzCMC.js} +5 -5
  12. package/dist/{flows-CTOdIk-m.js.map → flows-WOQNzCMC.js.map} +1 -1
  13. package/dist/flows.d.ts +1 -1
  14. package/dist/flows.js +1 -1
  15. package/dist/{invocation-options-DbCcOAzM.js → invocation-options-B6d7TI-y.js} +7 -6
  16. package/dist/{invocation-options-DbCcOAzM.js.map → invocation-options-B6d7TI-y.js.map} +1 -1
  17. package/dist/{ipc-D3akHMEq.js → ipc-BJrvfNXr.js} +307 -90
  18. package/dist/ipc-BJrvfNXr.js.map +1 -0
  19. package/dist/{output-BPPjvJTI.js → output-CL18YmsG.js} +9 -6
  20. package/dist/output-CL18YmsG.js.map +1 -0
  21. package/dist/{queue-owner-runtime--IbbndGz.js → queue-owner-runtime-Cr0dWJuL.js} +1008 -463
  22. package/dist/queue-owner-runtime-Cr0dWJuL.js.map +1 -0
  23. package/dist/runtime.d.ts +129 -39
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +378 -176
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/{session-watch--UX5dh00.js → session-watch-B8Q_TsGf.js} +4 -4
  28. package/dist/{session-watch--UX5dh00.js.map → session-watch-B8Q_TsGf.js.map} +1 -1
  29. package/dist/{watch-BbBxO-kb.js → watch-Dp73ph8h.js} +2 -2
  30. package/dist/{watch-BbBxO-kb.js.map → watch-Dp73ph8h.js.map} +1 -1
  31. package/package.json +3 -3
  32. package/skills/acpx/SKILL.md +27 -7
  33. package/dist/agent-registry-DU6mhZBL.js.map +0 -1
  34. package/dist/client-C6NUFqr-.d.ts.map +0 -1
  35. package/dist/ipc-D3akHMEq.js.map +0 -1
  36. package/dist/output-BPPjvJTI.js.map +0 -1
  37. package/dist/queue-owner-runtime--IbbndGz.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-D3akHMEq.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-DU6mhZBL.js";
1
+ import { $ as setPerfGauge, A as isoNow$1, At as AcpxOperationalError, Bt as QueueConnectionError, C as promptToDisplayText, D as findGitRepositoryRoot, E as absolutePath, Ft as ClaudeAcpSessionCreateTimeoutError, Gt as SessionResumeRequiredError, H as normalizeAgentSessionId, Ht as SessionModeReplayError, I as resolveSessionRecord, It as CopilotAcpUnsupportedError, J as getPerfMetricsSnapshot, Kt as UnsupportedPromptContentError, L as writeSessionRecord, Lt as GeminiAcpStartupTimeoutError, Mt as AgentSpawnError, N as normalizeName, Nt as AgentStartupError, Pt as AuthPolicyError, Q as resetPerfMetrics, Rt as PermissionDeniedError, St as EXIT_CODES, Tt as OUTPUT_ERROR_ORIGINS, U as SESSION_JOURNAL_SCHEMA, Ut as SessionModelReplayError, V as extractAgentSessionId, Vt as SessionConfigOptionReplayError, W as SessionJournalReader, X as measurePerf, Y as incrementPerfCounter, Z as recordPerfDuration, _ as waitMs$1, _t as isProcessDefinitelyDead, a as trySetModelOnRunningOwner, b as getUnsupportedPromptContentMessage, bt as isAcpResourceNotFoundError, c as queueRequestExceedsLimit, dt as sessionEventActivePath, et as startPerfTimer, f as refreshQueueOwnerLease, ft as sessionEventLockPath, g as tryAcquireQueueOwnerLease, gt as isProcessAlive, h as terminateQueueOwnerForSession, i as trySetModeOnRunningOwner, it as isSessionUpdateNotification, jt as AgentDisconnectedError, k as findSessionByDirectoryWalk, l as parseQueueRequest, lt as defaultSessionEventLog, m as terminateProcess, n as tryCloseSessionOnRunningOwner, nt as isAcpJsonRpcMessage, o as trySubmitToRunningOwner, p as releaseQueueOwnerLease, pt as sessionEventSegmentPath, q as formatPerfMetric, r as trySetConfigOptionOnRunningOwner, rt as isAcpMessageObject, s as queueRequestByteLimit, t as tryCancelOnRunningOwner, ut as sessionBaseDir, v as QueueLeaseGuardSettlementError, vt as extractAcpError, w as textPrompt, wt as OUTPUT_ERROR_CODES, yt as formatUnknownErrorMessage, zt as PermissionPromptUnavailableError } from "./ipc-BJrvfNXr.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-DdKB-zia.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]);
@@ -485,21 +486,57 @@ function normalizeQueueOwnerTtlMs(ttlMs) {
485
486
  }
486
487
  //#endregion
487
488
  //#region src/permission-prompt.ts
489
+ let promptQueue = Promise.resolve();
488
490
  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`);
491
+ options.signal?.throwIfAborted();
492
+ const result = promptQueue.then(() => askPermission(options));
493
+ promptQueue = result.then(() => {}, () => {});
494
+ const signal = options.signal;
495
+ if (!signal) return await result;
496
+ let onAbort = () => {};
497
+ const aborted = new Promise((_, reject) => {
498
+ onAbort = () => reject(signal.reason);
499
+ signal.addEventListener("abort", onAbort, { once: true });
500
+ });
501
+ try {
502
+ return await Promise.race([result, aborted]);
503
+ } finally {
504
+ signal.removeEventListener("abort", onAbort);
505
+ }
506
+ }
507
+ async function askPermission(options) {
508
+ options.signal?.throwIfAborted();
509
+ if (!canPrompt()) return false;
510
+ writePromptDetails(options);
492
511
  const rl = readline.createInterface({
493
512
  input: process.stdin,
494
513
  output: process.stderr
495
514
  });
515
+ const questionController = new AbortController();
516
+ const signal = options.signal ? AbortSignal.any([options.signal, questionController.signal]) : questionController.signal;
517
+ let onClose = () => {};
518
+ const closed = new Promise((resolve) => {
519
+ onClose = () => resolve(void 0);
520
+ rl.once("close", onClose);
521
+ });
496
522
  try {
497
- const normalized = (await rl.question(options.prompt)).trim().toLowerCase();
523
+ const answer = await Promise.race([rl.question(options.prompt, { signal }), closed]);
524
+ options.signal?.throwIfAborted();
525
+ const normalized = answer?.trim().toLowerCase();
498
526
  return normalized === "y" || normalized === "yes";
499
527
  } finally {
528
+ rl.removeListener("close", onClose);
529
+ questionController.abort();
500
530
  rl.close();
501
531
  }
502
532
  }
533
+ function canPrompt() {
534
+ return process.stdin.isTTY && process.stderr.isTTY && !process.stdin.readableEnded && !process.stdin.destroyed;
535
+ }
536
+ function writePromptDetails(options) {
537
+ if (options.header) process.stderr.write(`\n${options.header}\n`);
538
+ if (options.details && options.details.trim().length > 0) process.stderr.write(`${options.details}\n`);
539
+ }
503
540
  //#endregion
504
541
  //#region src/filesystem.ts
505
542
  const WRITE_PREVIEW_MAX_LINES = 16;
@@ -515,11 +552,12 @@ function toWritePreview(content) {
515
552
  if (preview.length > WRITE_PREVIEW_MAX_CHARS) preview = `${preview.slice(0, 1197)}...`;
516
553
  return preview;
517
554
  }
518
- async function defaultConfirmWrite(filePath, preview) {
555
+ async function defaultConfirmWrite(filePath, preview, signal) {
519
556
  return await promptForPermission({
520
557
  header: `[permission] Allow write to ${filePath}?`,
521
558
  details: preview,
522
- prompt: "Allow write? (y/N) "
559
+ prompt: "Allow write? (y/N) ",
560
+ signal
523
561
  });
524
562
  }
525
563
  function canPromptForPermission$2() {
@@ -545,7 +583,8 @@ var FileSystemHandlers = class {
545
583
  this.permissionMode = permissionMode;
546
584
  this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
547
585
  }
548
- async readTextFile(params) {
586
+ async readTextFile(params, authority) {
587
+ assertControlAuthority(authority);
549
588
  const filePath = this.resolvePathWithinRoot(params.path);
550
589
  const summary = `read_text_file: ${filePath}`;
551
590
  this.emitOperation({
@@ -557,7 +596,8 @@ var FileSystemHandlers = class {
557
596
  });
558
597
  try {
559
598
  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)}`);
599
+ const content = await (await this.getWorkspace()).readText(filePath);
600
+ assertControlAuthority(authority);
561
601
  const sliced = this.sliceContent(content, params.line, params.limit);
562
602
  this.emitOperation({
563
603
  method: "fs/read_text_file",
@@ -579,7 +619,8 @@ var FileSystemHandlers = class {
579
619
  throw error;
580
620
  }
581
621
  }
582
- async writeTextFile(params) {
622
+ async writeTextFile(params, authority) {
623
+ assertControlAuthority(authority);
583
624
  const filePath = this.resolvePathWithinRoot(params.path);
584
625
  const preview = toWritePreview(params.content);
585
626
  const summary = `write_text_file: ${filePath}`;
@@ -591,9 +632,17 @@ var FileSystemHandlers = class {
591
632
  timestamp: nowIso$1()
592
633
  });
593
634
  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 });
635
+ const approved = await this.isWriteApproved(filePath, preview, authority?.signal);
636
+ assertControlAuthority(authority);
637
+ if (!approved) throw new PermissionDeniedError("Permission denied for fs/write_text_file");
638
+ const workspace = await this.getWorkspace();
639
+ const target = await workspace.resolve(filePath);
640
+ const file = await workspace.openWritable(target, {
641
+ mode: 438,
642
+ assertBeforeMutation: () => assertControlAuthority(authority)
643
+ });
596
644
  try {
645
+ assertControlAuthority(authority);
597
646
  await file.handle.writeFile(params.content, "utf8");
598
647
  } finally {
599
648
  await file.handle.close();
@@ -618,17 +667,17 @@ var FileSystemHandlers = class {
618
667
  throw error;
619
668
  }
620
669
  }
621
- async isWriteApproved(filePath, preview) {
670
+ async isWriteApproved(filePath, preview, signal) {
622
671
  if (this.permissionMode === "approve-all") return true;
623
672
  if (this.permissionMode === "deny-all") return false;
624
673
  if (this.usesDefaultConfirmWrite && this.nonInteractivePermissions === "fail" && !canPromptForPermission$2()) throw new PermissionPromptUnavailableError();
625
- return await this.confirmWrite(filePath, preview);
674
+ return await this.confirmWrite(filePath, preview, signal);
626
675
  }
627
676
  resolvePathWithinRoot(rawPath) {
628
677
  if (!path.isAbsolute(rawPath)) throw new Error(`Path must be absolute: ${rawPath}`);
629
678
  const resolved = path.resolve(rawPath);
630
679
  if (!isPathInside(this.rootDir, resolved)) throw new Error(`Path is outside allowed cwd subtree: ${resolved}`);
631
- return resolved;
680
+ return rawPath;
632
681
  }
633
682
  getWorkspace() {
634
683
  return this.workspace ??= root(this.rootDir, {
@@ -691,11 +740,11 @@ function pickOption(options, kinds) {
691
740
  const TOOL_KIND_TITLE_MATCHERS = [
692
741
  {
693
742
  kind: "read",
694
- needles: ["read", "cat"]
743
+ names: ["read", "cat"]
695
744
  },
696
745
  {
697
746
  kind: "search",
698
- needles: [
747
+ names: [
699
748
  "search",
700
749
  "find",
701
750
  "grep"
@@ -703,7 +752,7 @@ const TOOL_KIND_TITLE_MATCHERS = [
703
752
  },
704
753
  {
705
754
  kind: "edit",
706
- needles: [
755
+ names: [
707
756
  "write",
708
757
  "edit",
709
758
  "patch"
@@ -711,15 +760,15 @@ const TOOL_KIND_TITLE_MATCHERS = [
711
760
  },
712
761
  {
713
762
  kind: "delete",
714
- needles: ["delete", "remove"]
763
+ names: ["delete", "remove"]
715
764
  },
716
765
  {
717
766
  kind: "move",
718
- needles: ["move", "rename"]
767
+ names: ["move", "rename"]
719
768
  },
720
769
  {
721
770
  kind: "execute",
722
- needles: [
771
+ names: [
723
772
  "run",
724
773
  "execute",
725
774
  "bash"
@@ -727,7 +776,7 @@ const TOOL_KIND_TITLE_MATCHERS = [
727
776
  },
728
777
  {
729
778
  kind: "fetch",
730
- needles: [
779
+ names: [
731
780
  "fetch",
732
781
  "http",
733
782
  "url"
@@ -735,25 +784,25 @@ const TOOL_KIND_TITLE_MATCHERS = [
735
784
  },
736
785
  {
737
786
  kind: "think",
738
- needles: ["think"]
787
+ names: ["think"]
739
788
  }
740
789
  ];
741
790
  function inferToolKind(params) {
742
791
  if (params.toolCall.kind) return params.toolCall.kind;
743
792
  const title = params.toolCall.title?.trim().toLowerCase();
744
793
  if (!title) return;
745
- const head = title.split(":", 1)[0]?.trim();
794
+ const head = title.split(/[:\s]/, 1)[0];
746
795
  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;
796
+ return TOOL_KIND_TITLE_MATCHERS.find(({ names }) => names.includes(head))?.kind ?? "other";
751
797
  }
752
798
  function isAutoApprovedReadKind(kind) {
753
799
  return kind === "read" || kind === "search";
754
800
  }
755
- async function promptForToolPermission(params) {
756
- return await promptForPermission({ prompt: `\n[permission] Allow ${params.toolCall.title ?? "tool"} [${inferToolKind(params) ?? "other"}]? (y/N) ` });
801
+ async function promptForToolPermission(params, signal) {
802
+ return await promptForPermission({
803
+ prompt: `\n[permission] Allow ${params.toolCall.title ?? "tool"} [${inferToolKind(params) ?? "other"}]? (y/N) `,
804
+ signal
805
+ });
757
806
  }
758
807
  function canPromptForPermission$1() {
759
808
  return process.stdin.isTTY && process.stderr.isTTY;
@@ -848,24 +897,24 @@ function selectedOrFirst(options, allowOption) {
848
897
  function selectedOrCancelled(option) {
849
898
  return { response: option ? selected(option.optionId) : cancelled() };
850
899
  }
851
- async function resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption) {
852
- if (canPromptForPermission$1()) return resolveInteractivePromptResult(params, allowOption, rejectOption);
900
+ async function resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption, signal) {
901
+ if (canPromptForPermission$1()) return resolveInteractivePromptResult(params, allowOption, rejectOption, signal);
853
902
  const escalation = buildEscalationEvent(params, policyMatch.matchedRule);
854
903
  return {
855
904
  response: withPermissionMetadata(rejectOption ? selected(rejectOption.optionId) : cancelled(), { permissionEscalation: escalation }),
856
905
  escalation
857
906
  };
858
907
  }
859
- async function resolveInteractivePromptResult(params, allowOption, rejectOption) {
860
- const approved = await promptForToolPermission(params);
908
+ async function resolveInteractivePromptResult(params, allowOption, rejectOption, signal) {
909
+ const approved = await promptForToolPermission(params, signal);
861
910
  if (approved && allowOption) return { response: selected(allowOption.optionId) };
862
911
  if (!approved && rejectOption) return { response: selected(rejectOption.optionId) };
863
912
  return { response: cancelled() };
864
913
  }
865
- function resolvePolicyMatch(params, policyMatch, options, allowOption, rejectOption) {
914
+ function resolvePolicyMatch(params, policyMatch, options, allowOption, rejectOption, signal) {
866
915
  if (policyMatch?.action === "approve") return selectedOrFirst(options, allowOption);
867
916
  if (policyMatch?.action === "deny") return selectedOrCancelled(rejectOption);
868
- if (policyMatch?.action === "escalate") return resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption);
917
+ if (policyMatch?.action === "escalate") return resolveEscalatingPermissionRequest(params, policyMatch, allowOption, rejectOption, signal);
869
918
  }
870
919
  function resolveModeMatch(options, mode, allowOption, rejectOption) {
871
920
  if (mode === "approve-all") return selectedOrFirst(options, allowOption);
@@ -875,24 +924,35 @@ function resolveNonInteractivePermission(nonInteractivePolicy, rejectOption) {
875
924
  if (nonInteractivePolicy === "fail") throw new PermissionPromptUnavailableError();
876
925
  return selectedOrCancelled(rejectOption);
877
926
  }
878
- async function resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption) {
927
+ async function resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption, signal) {
879
928
  if (isAutoApprovedReadKind(inferToolKind(params)) && allowOption) return { response: selected(allowOption.optionId) };
880
929
  if (!canPromptForPermission$1()) return resolveNonInteractivePermission(nonInteractivePolicy, rejectOption);
881
- return resolveInteractivePromptResult(params, allowOption, rejectOption);
930
+ return resolveInteractivePromptResult(params, allowOption, rejectOption, signal);
882
931
  }
883
932
  function permissionModeSatisfies(actual, required) {
884
933
  return PERMISSION_MODE_RANK[actual] >= PERMISSION_MODE_RANK[required];
885
934
  }
886
- async function resolvePermissionRequestWithDetails(params, mode, nonInteractivePolicy = "deny", policy) {
935
+ async function resolvePermissionRequestWithDetails(params, mode, nonInteractivePolicy = "deny", policy, signal) {
936
+ try {
937
+ signal?.throwIfAborted();
938
+ const result = await resolvePermissionByPolicy(params, mode, nonInteractivePolicy, policy, signal);
939
+ signal?.throwIfAborted();
940
+ return result;
941
+ } catch (error) {
942
+ if (signal?.aborted) return { response: cancelled() };
943
+ throw error;
944
+ }
945
+ }
946
+ async function resolvePermissionByPolicy(params, mode, nonInteractivePolicy, policy, signal) {
887
947
  const options = params.options ?? [];
888
948
  if (options.length === 0) return { response: cancelled() };
889
949
  const allowOption = pickOption(options, ["allow_once", "allow_always"]);
890
950
  const rejectOption = pickOption(options, ["reject_once", "reject_always"]);
891
- const resolvedByPolicy = await resolvePolicyMatch(params, matchPermissionPolicy(params, policy), options, allowOption, rejectOption);
951
+ const resolvedByPolicy = await resolvePolicyMatch(params, matchPermissionPolicy(params, policy), options, allowOption, rejectOption, signal);
892
952
  if (resolvedByPolicy) return resolvedByPolicy;
893
953
  const resolvedByMode = resolveModeMatch(options, mode, allowOption, rejectOption);
894
954
  if (resolvedByMode) return resolvedByMode;
895
- return resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption);
955
+ return resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption, signal);
896
956
  }
897
957
  const DECISION_FALLBACK_ORDER = {
898
958
  allow_once: ["allow_once", "allow_always"],
@@ -1321,10 +1381,12 @@ const REQUESTED_MODEL_UNSUPPORTED_REASONS = ["missing-capability", "unadvertised
1321
1381
  var RequestedModelUnsupportedError = class extends Error {
1322
1382
  code = REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE;
1323
1383
  reason;
1324
- constructor(message, reason) {
1384
+ ambiguous;
1385
+ constructor(message, reason, details) {
1325
1386
  super(message);
1326
1387
  this.name = "RequestedModelUnsupportedError";
1327
1388
  this.reason = reason;
1389
+ if (details?.ambiguous) this.ambiguous = true;
1328
1390
  }
1329
1391
  };
1330
1392
  function isRequestedModelUnsupportedReason(value) {
@@ -1427,6 +1489,7 @@ function resolveRequestedModelId(params) {
1427
1489
  if (!params.models || !isCursorAcpCommandForModelAlias(params.agentCommand)) return params.requestedModel;
1428
1490
  if (params.models.availableModels.some((model) => model.modelId === params.requestedModel)) return params.requestedModel;
1429
1491
  const candidates = params.models.availableModels.map((model) => model.modelId).filter((modelId) => modelId.startsWith(`${params.requestedModel}[`));
1492
+ 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
1493
  return candidates.length === 1 ? candidates[0] : params.requestedModel;
1431
1494
  }
1432
1495
  function isCursorAcpCommandForModelAlias(agentCommand) {
@@ -1571,6 +1634,7 @@ function observeAcpStream(base, options) {
1571
1634
  const activePrompt = promptOwner ? options.bindPromptOwner(promptOwner) : void 0;
1572
1635
  const id = responseId(message);
1573
1636
  if (!(id !== void 0 && elicitationRequestIds.delete(id))) options.onMessage("outbound", message);
1637
+ if (activePrompt) options.assertPromptActive(activePrompt);
1574
1638
  const writer = base.writable.getWriter();
1575
1639
  try {
1576
1640
  await writer.write(message);
@@ -1756,8 +1820,11 @@ function trimToUtf8Boundary(buffer, limit) {
1756
1820
  if (start >= buffer.length) start = buffer.length - limit;
1757
1821
  return buffer.subarray(start);
1758
1822
  }
1759
- async function defaultConfirmExecute(commandLine) {
1760
- return await promptForPermission({ prompt: `\n[permission] Allow terminal command "${commandLine}"? (y/N) ` });
1823
+ async function defaultConfirmExecute(commandLine, signal) {
1824
+ return await promptForPermission({
1825
+ prompt: `\n[permission] Allow terminal command "${commandLine}"? (y/N) `,
1826
+ signal
1827
+ });
1761
1828
  }
1762
1829
  function canPromptForPermission() {
1763
1830
  return process.stdin.isTTY && process.stderr.isTTY;
@@ -1794,7 +1861,8 @@ var TerminalManager = class {
1794
1861
  this.permissionMode = permissionMode;
1795
1862
  this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
1796
1863
  }
1797
- async createTerminal(params) {
1864
+ async createTerminal(params, authority) {
1865
+ assertControlAuthority(authority);
1798
1866
  const commandLine = toCommandLine(params.command, params.args);
1799
1867
  const summary = `terminal/create: ${commandLine}`;
1800
1868
  this.emitOperation({
@@ -1804,10 +1872,12 @@ var TerminalManager = class {
1804
1872
  timestamp: nowIso()
1805
1873
  });
1806
1874
  try {
1807
- if (!await this.isExecuteApproved(commandLine)) throw new PermissionDeniedError("Permission denied for terminal/create");
1875
+ const approved = await this.isExecuteApproved(commandLine, authority?.signal);
1876
+ assertControlAuthority(authority);
1877
+ if (!approved) throw new PermissionDeniedError("Permission denied for terminal/create");
1808
1878
  const requestedLimit = Math.max(0, Math.round(params.outputByteLimit ?? DEFAULT_TERMINAL_OUTPUT_LIMIT_BYTES));
1809
1879
  const outputByteLimit = Math.min(requestedLimit, this.outputCeilingBytes ?? Number.POSITIVE_INFINITY);
1810
- const { proc, spawnCommand } = await spawnTerminalProcess(params, this.cwd);
1880
+ const { proc, spawnCommand } = await spawnTerminalProcess(params, this.cwd, authority);
1811
1881
  let resolveExit = () => {};
1812
1882
  const exitPromise = new Promise((resolve) => {
1813
1883
  resolveExit = resolve;
@@ -1852,6 +1922,15 @@ var TerminalManager = class {
1852
1922
  });
1853
1923
  const terminalId = randomUUID();
1854
1924
  this.terminals.set(terminalId, terminal);
1925
+ try {
1926
+ assertControlAuthority(authority);
1927
+ } catch (error) {
1928
+ await this.releaseTerminal({
1929
+ terminalId,
1930
+ sessionId: params.sessionId
1931
+ });
1932
+ throw error;
1933
+ }
1855
1934
  this.emitOperation({
1856
1935
  method: "terminal/create",
1857
1936
  status: "completed",
@@ -1990,11 +2069,11 @@ var TerminalManager = class {
1990
2069
  emitOperation(operation) {
1991
2070
  this.onOperation?.(operation);
1992
2071
  }
1993
- async isExecuteApproved(commandLine) {
2072
+ async isExecuteApproved(commandLine, signal) {
1994
2073
  if (this.permissionMode === "approve-all") return true;
1995
2074
  if (this.permissionMode === "deny-all") return false;
1996
2075
  if (this.usesDefaultConfirmExecute && this.nonInteractivePermissions === "fail" && !canPromptForPermission()) throw new PermissionPromptUnavailableError();
1997
- return await this.confirmExecute(commandLine);
2076
+ return await this.confirmExecute(commandLine, signal);
1998
2077
  }
1999
2078
  isRunning(terminal) {
2000
2079
  return terminal.exitCode === void 0 && terminal.signal === void 0;
@@ -2058,25 +2137,26 @@ var TerminalManager = class {
2058
2137
  while (hasLivePid(terminal.descendantPids)) await waitMs(25);
2059
2138
  }
2060
2139
  };
2061
- async function spawnTerminalProcess(params, defaultCwd) {
2140
+ async function spawnTerminalProcess(params, defaultCwd, authority) {
2062
2141
  const directCommand = buildTerminalSpawnCommand(params.command, params.args);
2063
2142
  try {
2064
2143
  return {
2065
- proc: await spawnAndWait(directCommand, params, defaultCwd),
2144
+ proc: await spawnAndWait(directCommand, params, defaultCwd, authority),
2066
2145
  spawnCommand: directCommand
2067
2146
  };
2068
2147
  } catch (error) {
2069
2148
  const fallbackCommand = params.args === void 0 && isNotFoundSpawnError(error) ? buildTerminalFallbackSpawnCommand(params.command, params.cwd ?? defaultCwd) : void 0;
2070
2149
  if (!fallbackCommand) throw error;
2071
2150
  return {
2072
- proc: await spawnAndWait(fallbackCommand, params, defaultCwd),
2151
+ proc: await spawnAndWait(fallbackCommand, params, defaultCwd, authority),
2073
2152
  spawnCommand: fallbackCommand
2074
2153
  };
2075
2154
  }
2076
2155
  }
2077
- async function spawnAndWait(spawnCommand, params, defaultCwd) {
2156
+ async function spawnAndWait(spawnCommand, params, defaultCwd, authority) {
2078
2157
  const spawnOptions = buildTerminalSpawnOptions(spawnCommand.command, params.cwd ?? defaultCwd, params.env);
2079
2158
  if (spawnCommand.killProcessGroup) spawnOptions.detached = true;
2159
+ assertControlAuthority(authority);
2080
2160
  const proc = spawn(spawnCommand.command, spawnCommand.args, spawnOptions);
2081
2161
  await waitForSpawn(proc);
2082
2162
  return proc;
@@ -2361,13 +2441,12 @@ var AcpClient = class {
2361
2441
  cancellingSessionIds = /* @__PURE__ */ new Set();
2362
2442
  permissionAbortControllers = /* @__PURE__ */ new Map();
2363
2443
  closing = false;
2444
+ closeEpoch = 0;
2364
2445
  agentStartedAt;
2365
2446
  lastAgentExit;
2366
2447
  lastKnownPid;
2367
2448
  promptPermissionFailures = /* @__PURE__ */ new Map();
2368
2449
  pendingConnectionRequests = /* @__PURE__ */ new Set();
2369
- modelConfigIds = /* @__PURE__ */ new Map();
2370
- legacyModelSessionIds = /* @__PURE__ */ new Set();
2371
2450
  constructor(options) {
2372
2451
  this.options = {
2373
2452
  ...options,
@@ -2457,8 +2536,11 @@ var AcpClient = class {
2457
2536
  this.filesystem.updatePermissionPolicy(this.options.permissionMode, this.options.nonInteractivePermissions);
2458
2537
  this.terminalManager.updatePermissionPolicy(this.options.permissionMode, this.options.nonInteractivePermissions);
2459
2538
  }
2539
+ hasLiveConnection() {
2540
+ return this.connection != null && !this.connection.signal.aborted && this.agent != null && isChildProcessRunning(this.agent);
2541
+ }
2460
2542
  hasReusableSession(sessionId) {
2461
- return this.connection != null && this.agent != null && isChildProcessRunning(this.agent) && this.loadedSessionId === sessionId;
2543
+ return this.hasLiveConnection() && this.loadedSessionId === sessionId;
2462
2544
  }
2463
2545
  hasActivePrompt(sessionId) {
2464
2546
  if (!this.activePrompt) return false;
@@ -2468,25 +2550,32 @@ var AcpClient = class {
2468
2550
  hasUnresolvedPrompt() {
2469
2551
  return this.activePrompt !== void 0;
2470
2552
  }
2471
- endPromptElicitation(sessionId) {
2472
- const active = this.activePrompt;
2473
- if (!active || active.sessionId !== sessionId) return;
2474
- active.elicitationHandler = void 0;
2475
- active.elicitationController.abort();
2553
+ abortSessionRequests(sessionId) {
2554
+ const owners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
2555
+ const fallback = this.takePermissionAbortController(sessionId);
2556
+ for (const owner of owners) {
2557
+ owner.elicitationHandler = void 0;
2558
+ owner.elicitationController.abort();
2559
+ owner.requestController.abort();
2560
+ }
2561
+ fallback?.abort();
2476
2562
  }
2477
2563
  async start() {
2478
- if (this.connection && this.agent && isChildProcessRunning(this.agent)) return;
2564
+ if (this.hasLiveConnection()) return;
2479
2565
  if (this.connection || this.agent) await this.close();
2566
+ const epoch = this.closeEpoch;
2480
2567
  const maxMessageBytes = readMaxAcpMessageBytes();
2481
2568
  const launch = await this.resolveAgentLaunchPlan();
2482
2569
  this.logAgentLaunch(launch);
2483
2570
  await this.ensureLaunchSupport(launch);
2484
2571
  const { child, process: startedProcess } = await this.spawnAgentProcess(launch);
2485
- this.agent = child;
2486
- this.closing = false;
2487
- this.agentStartedAt = startedProcess.startedAt;
2488
- this.lastAgentExit = void 0;
2489
- this.lastKnownPid = startedProcess.pid;
2572
+ if (this.closeEpoch === epoch) {
2573
+ this.agent = child;
2574
+ this.closing = false;
2575
+ this.agentStartedAt = startedProcess.startedAt;
2576
+ this.lastAgentExit = void 0;
2577
+ this.lastKnownPid = startedProcess.pid;
2578
+ }
2490
2579
  const startupStderr = [];
2491
2580
  child.stderr.on("data", (chunk) => {
2492
2581
  this.captureStartupStderr(startupStderr, chunk);
@@ -2502,6 +2591,7 @@ var AcpClient = class {
2502
2591
  startupFailure.dispose();
2503
2592
  throw error;
2504
2593
  }
2594
+ await this.stopIfClosedDuringLaunch(epoch, child, startupFailure);
2505
2595
  const input = Writable.toWeb(child.stdin);
2506
2596
  const output = Readable.toWeb(child.stdout);
2507
2597
  let connection;
@@ -2509,7 +2599,13 @@ var AcpClient = class {
2509
2599
  this.rejectPendingConnectionRequests(error);
2510
2600
  connection?.close(error);
2511
2601
  }));
2512
- connection = this.createConnection(stream, launch);
2602
+ const capabilities = resolveClientCapabilities({
2603
+ devinAcp: launch.devinAcp,
2604
+ fs: this.options.fs !== false,
2605
+ terminal: this.options.terminal !== false,
2606
+ elicitationModes: this.options.elicitationModes
2607
+ });
2608
+ connection = this.createConnection(stream, launch, capabilities);
2513
2609
  connection.signal.addEventListener("abort", () => {
2514
2610
  this.recordAgentExit(child, "connection_close", child.exitCode ?? null, child.signalCode ?? null);
2515
2611
  }, { once: true });
@@ -2518,7 +2614,8 @@ var AcpClient = class {
2518
2614
  connection,
2519
2615
  startupFailure,
2520
2616
  startupStderr,
2521
- launch
2617
+ launch,
2618
+ capabilities
2522
2619
  });
2523
2620
  }
2524
2621
  async resolveAgentLaunchPlan() {
@@ -2615,19 +2712,22 @@ var AcpClient = class {
2615
2712
  releaseExitNotification();
2616
2713
  }
2617
2714
  }
2618
- createConnection(stream, launch) {
2715
+ createConnection(stream, launch, capabilities) {
2619
2716
  const app = client({ name: "acpx" }).onNotification(methods.client.session.update, async ({ params }) => {
2620
2717
  await this.handleSessionUpdate(params);
2621
- }).onNotification(methods.client.elicitation.complete, async () => {}).onRequest(methods.client.session.requestPermission, async ({ params }) => {
2622
- return await this.handlePermissionRequest(params);
2718
+ }).onNotification(methods.client.elicitation.complete, async () => {}).onRequest(methods.client.session.requestPermission, async ({ params, signal }) => {
2719
+ return await this.handlePermissionRequest(params, signal);
2623
2720
  }).onRequest(methods.client.elicitation.create, async ({ params, requestId, signal }) => {
2624
2721
  return await this.handleElicitationRequest(params, requestId, signal);
2625
- }).onRequest(methods.client.fs.readTextFile, async ({ params }) => {
2626
- return await this.handleReadTextFile(params);
2627
- }).onRequest(methods.client.fs.writeTextFile, async ({ params }) => {
2628
- return await this.handleWriteTextFile(params);
2629
- }).onRequest(methods.client.terminal.create, async ({ params }) => {
2630
- return await this.handleCreateTerminal(params);
2722
+ });
2723
+ if (capabilities.fs?.readTextFile) app.onRequest(methods.client.fs.readTextFile, async ({ params, signal }) => {
2724
+ return await this.handleReadTextFile(params, signal);
2725
+ });
2726
+ if (capabilities.fs?.writeTextFile) app.onRequest(methods.client.fs.writeTextFile, async ({ params, signal }) => {
2727
+ return await this.handleWriteTextFile(params, signal);
2728
+ });
2729
+ if (capabilities.terminal) app.onRequest(methods.client.terminal.create, async ({ params, signal }) => {
2730
+ return await this.handleCreateTerminal(params, signal);
2631
2731
  }).onRequest(methods.client.terminal.output, async ({ params }) => {
2632
2732
  return await this.terminalManager.terminalOutput(params);
2633
2733
  }).onRequest(methods.client.terminal.waitForExit, async ({ params }) => {
@@ -2644,7 +2744,7 @@ var AcpClient = class {
2644
2744
  }
2645
2745
  async initializeAgentConnection(params) {
2646
2746
  try {
2647
- const initResult = await Promise.race([this.initializeProtocolConnection(params.connection, params.launch), params.startupFailure.promise]);
2747
+ const initResult = await Promise.race([this.initializeProtocolConnection(params.connection, params.launch, params.capabilities), params.startupFailure.promise]);
2648
2748
  params.startupFailure.dispose();
2649
2749
  this.connection = params.connection;
2650
2750
  this.initResult = initResult;
@@ -2655,15 +2755,10 @@ var AcpClient = class {
2655
2755
  await this.handleInitializeFailure(params, error);
2656
2756
  }
2657
2757
  }
2658
- async initializeProtocolConnection(connection, launch) {
2758
+ async initializeProtocolConnection(connection, launch, capabilities) {
2659
2759
  const initializePromise = connection.agent.request(methods.agent.initialize, {
2660
2760
  protocolVersion: PROTOCOL_VERSION,
2661
- clientCapabilities: resolveClientCapabilities({
2662
- devinAcp: launch.devinAcp,
2663
- fs: this.options.fs !== false,
2664
- terminal: this.options.terminal !== false,
2665
- elicitationModes: this.options.elicitationModes ?? []
2666
- }),
2761
+ clientCapabilities: capabilities,
2667
2762
  clientInfo: resolveClientInfo(launch.devinAcp)
2668
2763
  });
2669
2764
  const initialized = launch.geminiAcp ? await withTimeout(initializePromise, resolveGeminiAcpStartupTimeoutMs()) : await initializePromise;
@@ -2689,6 +2784,7 @@ var AcpClient = class {
2689
2784
  },
2690
2785
  suppressReplaySessionUpdates: () => this.suppressReplaySessionUpdateMessages,
2691
2786
  bindPromptOwner: (owner) => this.bindPromptOwner(owner),
2787
+ assertPromptActive: (active) => this.assertPromptActive(active),
2692
2788
  onPromptRequestWritten: (active, owner) => this.onPromptRequestWritten(active, owner)
2693
2789
  });
2694
2790
  }
@@ -2718,7 +2814,6 @@ var AcpClient = class {
2718
2814
  configOptions,
2719
2815
  response: result
2720
2816
  });
2721
- this.rememberSessionModels(result.sessionId, models);
2722
2817
  await this.captureAgentDescendants(this.agent);
2723
2818
  return {
2724
2819
  sessionId: result.sessionId,
@@ -2750,7 +2845,6 @@ var AcpClient = class {
2750
2845
  }
2751
2846
  this.loadedSessionId = sessionId;
2752
2847
  const result = toReconnectedSessionResult(response);
2753
- this.updateRememberedSessionModels(sessionId, result);
2754
2848
  await this.captureAgentDescendants(this.agent);
2755
2849
  return result;
2756
2850
  }
@@ -2764,7 +2858,6 @@ var AcpClient = class {
2764
2858
  }));
2765
2859
  this.loadedSessionId = sessionId;
2766
2860
  const result = toReconnectedSessionResult(response);
2767
- this.updateRememberedSessionModels(sessionId, result);
2768
2861
  await this.captureAgentDescendants(this.agent);
2769
2862
  return result;
2770
2863
  }
@@ -2781,28 +2874,24 @@ var AcpClient = class {
2781
2874
  this.suppressSessionUpdates = previous.suppressSessionUpdates;
2782
2875
  this.suppressReplaySessionUpdateMessages = previous.suppressReplaySessionUpdateMessages;
2783
2876
  }
2784
- async prompt(sessionId, prompt, onRequestWritten, onElicitation, onPermissionRequest) {
2877
+ async prompt(sessionId, prompt, onRequestWritten, onElicitation, onPermissionRequest, authority) {
2785
2878
  const connection = this.getConnection();
2786
2879
  const normalizedPrompt = this.normalizePromptForAgent(prompt);
2787
2880
  const restoreConsoleError = this.options.suppressSdkConsoleErrors ? installSdkConsoleErrorSuppression() : void 0;
2788
2881
  const previousActivePrompt = this.activePrompt;
2789
- const activePrompt = this.beginActivePrompt(sessionId, onRequestWritten, onElicitation, onPermissionRequest);
2790
- let promptPromise;
2882
+ const replacedOwners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
2883
+ const activePrompt = this.beginActivePrompt(sessionId, onRequestWritten, onElicitation, onPermissionRequest, authority);
2791
2884
  try {
2792
- promptPromise = this.runConnectionRequest(() => connection.agent.request(methods.agent.session.prompt, {
2885
+ const promptPromise = this.runConnectionRequest(() => connection.agent.request(methods.agent.session.prompt, {
2793
2886
  sessionId,
2794
2887
  prompt: normalizedPrompt
2795
- }));
2796
- } catch (error) {
2797
- this.clearActivePrompt(activePrompt);
2798
- restoreConsoleError?.();
2799
- throw error;
2800
- }
2801
- activePrompt.promise = promptPromise;
2802
- try {
2888
+ }), authority);
2889
+ activePrompt.promise = promptPromise;
2803
2890
  previousActivePrompt?.elicitationController?.abort();
2891
+ for (const owner of replacedOwners) owner.requestController.abort();
2804
2892
  return this.returnPromptResponseOrPermissionFailure(sessionId, await promptPromise);
2805
2893
  } catch (error) {
2894
+ if (activePrompt.admissionFailure) throw activePrompt.admissionFailure.error;
2806
2895
  this.throwPromptPermissionFailureIfPresent(sessionId);
2807
2896
  throw error;
2808
2897
  } finally {
@@ -2810,14 +2899,16 @@ var AcpClient = class {
2810
2899
  this.clearActivePrompt(activePrompt);
2811
2900
  }
2812
2901
  }
2813
- beginActivePrompt(sessionId, onRequestWritten, elicitationHandler, permissionHandler) {
2902
+ beginActivePrompt(sessionId, onRequestWritten, elicitationHandler, permissionHandler, authority) {
2814
2903
  this.cancellingSessionIds.delete(sessionId);
2815
2904
  const active = {
2816
2905
  sessionId,
2817
2906
  onRequestWritten,
2907
+ authority,
2818
2908
  elicitationHandler,
2819
2909
  permissionHandler,
2820
- elicitationController: new AbortController()
2910
+ elicitationController: new AbortController(),
2911
+ requestController: new AbortController()
2821
2912
  };
2822
2913
  this.activePrompt = active;
2823
2914
  this.pendingPromptOwners.push(active);
@@ -2830,6 +2921,14 @@ var AcpClient = class {
2830
2921
  if (active) active.requestId = owner.requestId;
2831
2922
  return active;
2832
2923
  }
2924
+ assertPromptActive(active) {
2925
+ try {
2926
+ assertControlAuthority(active.authority);
2927
+ } catch (error) {
2928
+ active.admissionFailure = { error };
2929
+ throw error;
2930
+ }
2931
+ }
2833
2932
  onPromptRequestWritten(active, owner) {
2834
2933
  if (active.requestId !== owner.requestId || active.sessionId !== owner.sessionId) return;
2835
2934
  try {
@@ -2848,6 +2947,7 @@ var AcpClient = class {
2848
2947
  this.promptPermissionFailures.delete(active.sessionId);
2849
2948
  }
2850
2949
  active.elicitationController.abort();
2950
+ active.requestController.abort();
2851
2951
  permissionController?.abort();
2852
2952
  }
2853
2953
  normalizePromptForAgent(prompt) {
@@ -2871,33 +2971,41 @@ var AcpClient = class {
2871
2971
  modeId
2872
2972
  }), authority, (error) => maybeWrapSessionControlError("session/set_mode", error, `for mode "${modeId}"`));
2873
2973
  }
2874
- async setSessionConfigOption(sessionId, configId, value, authority) {
2974
+ async setSessionConfigOption(sessionId, configId, value, models, authority) {
2975
+ assertControlAuthority(authority);
2976
+ const resolvedValue = models?.configId === configId ? this.resolveSessionModelId(value, models) : value;
2875
2977
  const connection = this.getConnection();
2876
2978
  return await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
2877
2979
  sessionId,
2878
2980
  configId,
2879
- value
2981
+ value: resolvedValue
2880
2982
  }), authority, (error) => maybeWrapSessionControlError("session/set_config_option", error, `for "${configId}"="${value}"`));
2881
2983
  }
2882
- async setSessionModel(sessionId, modelId, controlOverride, authority) {
2883
- const control = this.resolveModelControl(sessionId, controlOverride);
2884
- 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");
2885
- const resolvedModelId = resolveRequestedModelId({
2984
+ async setSessionModel(sessionId, modelId, models, authority) {
2985
+ assertControlAuthority(authority);
2986
+ 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");
2987
+ const resolvedModelId = this.resolveSessionModelId(modelId, models);
2988
+ return models.configId ? await this.setSessionModelThroughConfig(sessionId, resolvedModelId, models.configId, authority) : await this.setSessionModelThroughLegacyMethod(sessionId, resolvedModelId, authority);
2989
+ }
2990
+ resolveSessionModelId(modelId, models) {
2991
+ const params = {
2886
2992
  requestedModel: modelId,
2887
- models: controlOverride?.availableModels ? { availableModels: controlOverride.availableModels } : void 0,
2993
+ models,
2888
2994
  agentCommand: this.options.agentCommand
2995
+ };
2996
+ assertRequestedModelSupported({
2997
+ ...params,
2998
+ context: "apply"
2889
2999
  });
2890
- return control.kind === "config_option" ? await this.setSessionModelThroughConfig(sessionId, resolvedModelId, control.configId, authority) : await this.setSessionModelThroughLegacyMethod(sessionId, resolvedModelId, authority);
3000
+ return resolveRequestedModelId(params);
2891
3001
  }
2892
3002
  async setSessionModelThroughConfig(sessionId, modelId, configId, authority) {
2893
3003
  const connection = this.getConnection();
2894
- const response = await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
3004
+ return await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
2895
3005
  sessionId,
2896
3006
  configId,
2897
3007
  value: modelId
2898
3008
  }), authority, (error) => this.throwSessionModelError("session/set_config_option", modelId, error));
2899
- this.rememberSessionModels(sessionId, modelStateFromConfigOptions(response.configOptions));
2900
- return response;
2901
3009
  }
2902
3010
  async setSessionModelThroughLegacyMethod(sessionId, modelId, authority) {
2903
3011
  const connection = this.getConnection();
@@ -2913,39 +3021,10 @@ var AcpClient = class {
2913
3021
  const summary = acp ? formatSessionControlAcpSummary(acp) : error instanceof Error ? error.message : String(error);
2914
3022
  throw new Error(`Failed ${method} for model "${modelId}": ${summary}`, { cause: error });
2915
3023
  }
2916
- resolveModelControl(sessionId, controlOverride) {
2917
- if (controlOverride) return controlOverride.configId ? {
2918
- kind: "config_option",
2919
- configId: controlOverride.configId
2920
- } : { kind: "legacy_set_model" };
2921
- const configId = this.modelConfigIds.get(sessionId);
2922
- if (configId) return {
2923
- kind: "config_option",
2924
- configId
2925
- };
2926
- return this.legacyModelSessionIds.has(sessionId) ? { kind: "legacy_set_model" } : void 0;
2927
- }
2928
- rememberSessionModels(sessionId, models) {
2929
- if (!models) {
2930
- this.modelConfigIds.delete(sessionId);
2931
- this.legacyModelSessionIds.delete(sessionId);
2932
- return;
2933
- }
2934
- if (models.configId) {
2935
- this.modelConfigIds.set(sessionId, models.configId);
2936
- this.legacyModelSessionIds.delete(sessionId);
2937
- return;
2938
- }
2939
- this.modelConfigIds.delete(sessionId);
2940
- this.legacyModelSessionIds.add(sessionId);
2941
- }
2942
- updateRememberedSessionModels(sessionId, result) {
2943
- const explicitConfigRemoval = result.configOptionsPresent && this.modelConfigIds.has(sessionId);
2944
- if (result.models || result.legacyModelMetadataPresent || explicitConfigRemoval) this.rememberSessionModels(sessionId, result.models);
2945
- }
2946
3024
  async cancel(sessionId) {
2947
3025
  const connection = this.getConnection();
2948
- const active = this.activePrompt?.sessionId === sessionId ? this.activePrompt : void 0;
3026
+ const owners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
3027
+ const active = owners.at(-1);
2949
3028
  const cancellation = active?.cancelPromise ?? this.runConnectionRequest(() => connection.agent.notify(methods.agent.session.cancel, { sessionId })).catch((error) => {
2950
3029
  if (active?.cancelPromise === cancellation) active.cancelPromise = void 0;
2951
3030
  throw error;
@@ -2954,19 +3033,20 @@ var AcpClient = class {
2954
3033
  if (active) {
2955
3034
  active.cancelPromise = cancellation;
2956
3035
  this.cancellingSessionIds.add(sessionId);
2957
- active.elicitationController?.abort();
2958
3036
  } else this.cancellingSessionIds.delete(sessionId);
3037
+ for (const owner of owners) {
3038
+ owner.elicitationController.abort();
3039
+ owner.requestController.abort();
3040
+ }
2959
3041
  permissionController?.abort();
2960
3042
  await cancellation;
2961
3043
  }
2962
3044
  async closeSession(sessionId) {
2963
3045
  const connection = this.getConnection();
2964
3046
  this.cancellingSessionIds.add(sessionId);
2965
- if (this.activePrompt?.sessionId === sessionId) this.activePrompt.elicitationController?.abort();
3047
+ this.abortSessionRequests(sessionId);
2966
3048
  await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.close, { sessionId }));
2967
3049
  if (this.loadedSessionId === sessionId) this.loadedSessionId = void 0;
2968
- this.modelConfigIds.delete(sessionId);
2969
- this.legacyModelSessionIds.delete(sessionId);
2970
3050
  }
2971
3051
  async listSessions(params = {}) {
2972
3052
  const connection = this.getConnection();
@@ -3002,7 +3082,13 @@ var AcpClient = class {
3002
3082
  }
3003
3083
  async close() {
3004
3084
  this.closing = true;
3085
+ this.closeEpoch += 1;
3086
+ const permissionControllers = [...this.permissionAbortControllers.values()];
3087
+ this.permissionAbortControllers.clear();
3088
+ const owners = [...this.pendingPromptOwners];
3005
3089
  this.abortActiveElicitation();
3090
+ for (const owner of owners) owner.requestController.abort();
3091
+ for (const controller of permissionControllers) controller.abort();
3006
3092
  await this.terminalManager.shutdown();
3007
3093
  const agent = this.agent;
3008
3094
  if (agent) await this.terminateAgentProcess(agent);
@@ -3020,12 +3106,16 @@ var AcpClient = class {
3020
3106
  this.permissionAbortControllers.clear();
3021
3107
  this.promptPermissionFailures.clear();
3022
3108
  this.loadedSessionId = void 0;
3023
- this.modelConfigIds.clear();
3024
- this.legacyModelSessionIds.clear();
3025
3109
  this.initResult = void 0;
3026
3110
  this.connection = void 0;
3027
3111
  this.agent = void 0;
3028
3112
  }
3113
+ async stopIfClosedDuringLaunch(epoch, child, startupFailure) {
3114
+ if (this.closeEpoch === epoch) return;
3115
+ startupFailure.dispose();
3116
+ await this.terminateAgentProcess(child);
3117
+ throw new Error("ACP client was closed while the agent was starting");
3118
+ }
3029
3119
  abortActiveElicitation() {
3030
3120
  this.activePrompt?.elicitationController?.abort();
3031
3121
  }
@@ -3230,26 +3320,41 @@ var AcpClient = class {
3230
3320
  await connection.agent.request(methods.agent.authenticate, { methodId: selected.methodId });
3231
3321
  this.log(`authenticated with method ${selected.methodId} (${selected.source})`);
3232
3322
  }
3233
- async handlePermissionRequest(params) {
3234
- if (this.cancellingSessionIds.has(params.sessionId)) return cancelledPermissionResponse();
3235
- const hostResponse = await this.tryHandlePermissionRequestWithHost(params);
3236
- if (hostResponse) return hostResponse;
3237
- const { response, recorded } = await this.resolvePermissionRequestFromMode(params);
3238
- if (!recorded) {
3239
- const decision = classifyPermissionDecision(params, response);
3240
- this.recordPermissionDecision(decision);
3323
+ async handlePermissionRequest(params, requestSignal) {
3324
+ const owner = this.captureDelegatedRequestOwner(params.sessionId, requestSignal);
3325
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3326
+ if (!this.hasPermissionOwner(params.sessionId)) return this.finishPermissionRequest(params, owner, cancelledPermissionResponse());
3327
+ if (this.initResult?.agentInfo?.name === "antigravity-acp" && params.toolCall.toolCallId.startsWith("interaction_")) {
3328
+ const response = this.handleModePermissionError(params.sessionId, new PermissionPromptUnavailableError("Antigravity requested a user answer. acpx cannot answer Antigravity interaction questions; continue in an interactive client."));
3329
+ return this.finishPermissionRequest(params, owner, response);
3241
3330
  }
3242
- return response;
3331
+ const response = await this.tryHandlePermissionRequestWithHost(params, owner) ?? await this.resolvePermissionRequestFromMode(params, owner);
3332
+ return this.finishPermissionRequest(params, owner, response);
3333
+ }
3334
+ finishPermissionRequest(params, owner, response) {
3335
+ const result = this.isDelegatedRequestActive(owner) ? response : cancelledPermissionResponse();
3336
+ this.recordPermissionDecision(classifyPermissionDecision(params, result));
3337
+ return result;
3338
+ }
3339
+ hasPermissionOwner(sessionId) {
3340
+ return !this.pendingPromptOwners.some((owner) => owner.permissionHandler !== void 0) || this.pendingPromptOwners.some((owner) => owner.sessionId === sessionId);
3243
3341
  }
3244
3342
  async handleElicitationRequest(request, requestId, requestSignal) {
3245
3343
  const resolved = this.resolveElicitationOwner(request);
3246
3344
  if ("response" in resolved) return resolved.response;
3247
3345
  const { active, handler } = resolved.owner;
3248
- const signal = AbortSignal.any([requestSignal, active.elicitationController.signal]);
3249
- const outcome = await raceWithAbort(signal, Promise.resolve().then(async () => await handler(request, {
3250
- requestId,
3251
- signal
3252
- })).then((response) => ({
3346
+ const signal = AbortSignal.any([
3347
+ requestSignal,
3348
+ active.elicitationController.signal,
3349
+ active.authority?.signal
3350
+ ].filter((candidate) => candidate !== void 0));
3351
+ const outcome = await raceWithAbort(signal, Promise.resolve().then(async () => {
3352
+ signal.throwIfAborted();
3353
+ return await handler(request, {
3354
+ requestId,
3355
+ signal
3356
+ });
3357
+ }).then((response) => ({
3253
3358
  kind: "response",
3254
3359
  response
3255
3360
  }), (error) => ({
@@ -3284,62 +3389,65 @@ var AcpClient = class {
3284
3389
  isElicitationSessionCancelling(sessionId) {
3285
3390
  return this.closing || this.cancellingSessionIds.has(sessionId);
3286
3391
  }
3287
- resolvePermissionOwner(sessionId) {
3288
- const active = this.activePrompt;
3289
- const handler = active?.permissionHandler ?? this.options.onPermissionRequest;
3290
- if (!handler) return;
3291
- const signal = this.cancellationSignalForSession(sessionId);
3292
- if (!active?.permissionHandler) return {
3293
- handler,
3294
- signal
3295
- };
3392
+ captureDelegatedRequestOwner(sessionId, requestSignal) {
3393
+ const active = this.pendingPromptOwners.findLast((owner) => owner.sessionId === sessionId);
3394
+ const signal = AbortSignal.any([
3395
+ active?.requestController.signal ?? this.cancellationSignalForSession(sessionId),
3396
+ active?.authority?.signal,
3397
+ requestSignal
3398
+ ].filter((candidate) => candidate !== void 0));
3399
+ const epoch = this.closeEpoch;
3296
3400
  return {
3297
- handler,
3298
- signal: active.sessionId === sessionId ? AbortSignal.any([signal, active.elicitationController.signal]) : AbortSignal.abort()
3401
+ signal,
3402
+ permissionHandler: active?.permissionHandler ?? this.options.onPermissionRequest,
3403
+ assertActive: () => {
3404
+ if (this.closing || this.closeEpoch !== epoch || this.cancellingSessionIds.has(sessionId)) throw RequestError.requestCancelled();
3405
+ }
3299
3406
  };
3300
3407
  }
3301
- async tryHandlePermissionRequestWithHost(params) {
3302
- const owner = this.resolvePermissionOwner(params.sessionId);
3303
- if (!owner) return;
3304
- const { handler, signal } = owner;
3305
- if (signal.aborted) return this.hostPermissionDecisionResponse(params, signal, void 0);
3408
+ isDelegatedRequestActive(owner) {
3409
+ try {
3410
+ assertControlAuthority(owner);
3411
+ return true;
3412
+ } catch {
3413
+ return false;
3414
+ }
3415
+ }
3416
+ async tryHandlePermissionRequestWithHost(params, owner) {
3417
+ const handler = owner.permissionHandler;
3418
+ if (!handler) return;
3419
+ const { signal } = owner;
3420
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3306
3421
  try {
3307
3422
  const decision = await raceWithAbort(signal, handler({
3308
3423
  sessionId: params.sessionId,
3309
3424
  raw: params,
3310
3425
  inferredKind: inferToolKind(params)
3311
3426
  }, { signal }));
3312
- return this.hostPermissionDecisionResponse(params, signal, decision);
3427
+ return this.hostPermissionDecisionResponse(params, owner, decision);
3313
3428
  } catch (error) {
3314
- return this.hostPermissionErrorResponse(params, signal, error);
3429
+ return this.hostPermissionErrorResponse(owner, error);
3315
3430
  }
3316
3431
  }
3317
- hostPermissionDecisionResponse(params, signal, decision) {
3318
- if (signal.aborted || this.cancellingSessionIds.has(params.sessionId)) {
3319
- this.recordPermissionDecision("cancelled");
3320
- return cancelledPermissionResponse();
3321
- }
3432
+ hostPermissionDecisionResponse(params, owner, decision) {
3433
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3322
3434
  if (!decision) return;
3323
3435
  const response = decisionToResponse(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), decision);
3324
- this.recordPermissionDecision(classifyPermissionDecision(params, response));
3325
3436
  return decision.outcome === "cancel" ? response : this.explainPermissionRefusal(params, response);
3326
3437
  }
3327
- hostPermissionErrorResponse(params, signal, error) {
3328
- if (signal.aborted || this.cancellingSessionIds.has(params.sessionId)) {
3329
- this.recordPermissionDecision("cancelled");
3330
- return cancelledPermissionResponse();
3331
- }
3438
+ hostPermissionErrorResponse(owner, error) {
3439
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3332
3440
  this.log(`onPermissionRequest threw, falling through to mode-based resolver: ${error instanceof Error ? error.message : String(error)}`);
3333
3441
  }
3334
- async resolvePermissionRequestFromMode(params) {
3442
+ async resolvePermissionRequestFromMode(params, owner) {
3335
3443
  try {
3336
- const result = await resolvePermissionRequestWithDetails(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), this.options.permissionMode, this.options.nonInteractivePermissions ?? "deny", this.options.permissionPolicy);
3444
+ assertControlAuthority(owner);
3445
+ const result = await resolvePermissionRequestWithDetails(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), this.options.permissionMode, this.options.nonInteractivePermissions ?? "deny", this.options.permissionPolicy, owner.signal);
3446
+ assertControlAuthority(owner);
3337
3447
  this.emitPermissionEscalation(result.escalation);
3338
- return {
3339
- response: this.explainPermissionRefusal(params, result.response),
3340
- recorded: false
3341
- };
3448
+ return this.explainPermissionRefusal(params, result.response);
3342
3449
  } catch (error) {
3450
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3343
3451
  return this.handleModePermissionError(params.sessionId, error);
3344
3452
  }
3345
3453
  }
@@ -3362,11 +3470,7 @@ var AcpClient = class {
3362
3470
  handleModePermissionError(sessionId, error) {
3363
3471
  if (!(error instanceof PermissionPromptUnavailableError)) throw error;
3364
3472
  this.notePromptPermissionFailure(sessionId, error);
3365
- this.recordPermissionDecision("cancelled");
3366
- return {
3367
- response: cancelledPermissionResponse(),
3368
- recorded: true
3369
- };
3473
+ return cancelledPermissionResponse();
3370
3474
  }
3371
3475
  attachAgentLifecycleObservers(child, startedProcess, exitNotificationBarrier) {
3372
3476
  const onExit = (exitCode, signal) => {
@@ -3493,28 +3597,30 @@ var AcpClient = class {
3493
3597
  pending.reject(error);
3494
3598
  }
3495
3599
  }
3496
- async handleReadTextFile(params) {
3600
+ async handleReadTextFile(params, requestSignal) {
3497
3601
  try {
3498
- return await this.filesystem.readTextFile(params);
3602
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.filesystem.readTextFile(params, owner));
3499
3603
  } catch (error) {
3500
- this.recordPermissionError(params.sessionId, error);
3501
3604
  if (error instanceof FsSafeError && error.code === "not-found" || error instanceof Error && "code" in error && error.code === "ENOENT") throw RequestError.resourceNotFound(pathToFileURL(params.path).href);
3502
3605
  throw error;
3503
3606
  }
3504
3607
  }
3505
- async handleWriteTextFile(params) {
3506
- try {
3507
- return await this.filesystem.writeTextFile(params);
3508
- } catch (error) {
3509
- this.recordPermissionError(params.sessionId, error);
3510
- throw error;
3511
- }
3608
+ async handleWriteTextFile(params, requestSignal) {
3609
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.filesystem.writeTextFile(params, owner));
3610
+ }
3611
+ async handleCreateTerminal(params, requestSignal) {
3612
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.terminalManager.createTerminal(params, owner));
3512
3613
  }
3513
- async handleCreateTerminal(params) {
3614
+ async runDelegatedOperation(sessionId, requestSignal, run) {
3615
+ const owner = this.captureDelegatedRequestOwner(sessionId, requestSignal);
3514
3616
  try {
3515
- return await this.terminalManager.createTerminal(params);
3617
+ assertControlAuthority(owner);
3618
+ const result = await run(owner);
3619
+ assertControlAuthority(owner);
3620
+ return result;
3516
3621
  } catch (error) {
3517
- this.recordPermissionError(params.sessionId, error);
3622
+ if (!this.isDelegatedRequestActive(owner)) throw RequestError.requestCancelled();
3623
+ this.recordPermissionError(sessionId, error);
3518
3624
  throw error;
3519
3625
  }
3520
3626
  }
@@ -4011,20 +4117,6 @@ function recordPromptSubmission(conversation, prompt, timestamp = isoNow(), mess
4011
4117
  trimConversationForRuntime(conversation);
4012
4118
  return promptMessageId;
4013
4119
  }
4014
- function agentMessageHasObservedReply(message) {
4015
- return message.content.length > 0 || Object.keys(message.tool_results).length > 0;
4016
- }
4017
- function hasAgentReplyAfterPrompt(conversation, promptMessageId) {
4018
- let sawPrompt = false;
4019
- for (const message of conversation.messages) {
4020
- if (!sawPrompt) {
4021
- if (isUserMessage(message) && message.User.id === promptMessageId) sawPrompt = true;
4022
- continue;
4023
- }
4024
- if (isAgentMessage(message) && agentMessageHasObservedReply(message.Agent)) return true;
4025
- }
4026
- return false;
4027
- }
4028
4120
  function recordSessionUpdate(conversation, state, notification, timestamp = isoNow(), userMessageId) {
4029
4121
  const acpx = ensureAcpxState$1(state);
4030
4122
  const update = notification.update;
@@ -4430,14 +4522,42 @@ async function readLock(filePath) {
4430
4522
  throw error;
4431
4523
  }
4432
4524
  }
4433
- function lockPid(payload) {
4525
+ function lockPid(value) {
4526
+ if (!value || typeof value !== "object" || !("pid" in value)) return;
4527
+ const pid = value.pid;
4528
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
4529
+ }
4530
+ function parseLock(payload) {
4434
4531
  try {
4435
- const pid = JSON.parse(payload)?.pid;
4436
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
4532
+ return JSON.parse(payload);
4437
4533
  } catch {
4438
4534
  return;
4439
4535
  }
4440
4536
  }
4537
+ async function tryAcquireGuard(filePath) {
4538
+ try {
4539
+ return await acquireFileLock(filePath, {
4540
+ managerKey: "acpx.session-turn",
4541
+ lockPath: `${filePath}.guard`,
4542
+ staleMs: Infinity,
4543
+ timeoutMs: LOCK_RETRY_MS,
4544
+ retry: {
4545
+ retries: 8,
4546
+ minTimeout: 1,
4547
+ maxTimeout: 2,
4548
+ factor: 1,
4549
+ randomize: false
4550
+ },
4551
+ staleRecovery: "remove-if-unchanged",
4552
+ payload: () => ({ pid: process.pid }),
4553
+ shouldReclaim: ({ payload }) => isProcessDefinitelyDead(lockPid(payload)),
4554
+ shouldRemoveStaleLock: ({ payload }) => isProcessDefinitelyDead(lockPid(payload))
4555
+ });
4556
+ } catch (error) {
4557
+ if (error.code === "file_lock_timeout") return;
4558
+ throw error;
4559
+ }
4560
+ }
4441
4561
  async function removeObservedLock(filePath, observed) {
4442
4562
  const current = await readLock(filePath);
4443
4563
  if (!current) return true;
@@ -4449,64 +4569,105 @@ async function removeObservedLock(filePath, observed) {
4449
4569
  }
4450
4570
  async function recoverAbandonedLock(filePath) {
4451
4571
  const observed = await readLock(filePath);
4452
- if (!observed) return;
4453
- const pid = lockPid(observed.payload);
4454
- if (pid === process.pid || isProcessAlive(pid)) return;
4455
- if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return;
4456
- if (await removeObservedLock(filePath, observed)) incrementPerfCounter("session.events.stale_lock_recovered");
4572
+ if (!observed) return true;
4573
+ const pid = lockPid(parseLock(observed.payload));
4574
+ if (pid && !isProcessDefinitelyDead(pid)) return false;
4575
+ if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return false;
4576
+ if (await removeObservedLock(filePath, observed)) {
4577
+ incrementPerfCounter("session.events.stale_lock_recovered");
4578
+ return true;
4579
+ }
4580
+ return false;
4457
4581
  }
4458
4582
  async function publishLock(filePath, payload, signal) {
4459
- try {
4460
- await withTempFile({
4461
- rootDir: path.dirname(filePath),
4462
- prefix: "session-turn",
4463
- fileName: "lock"
4464
- }, async (temporaryPath) => {
4465
- await fs$1.writeFile(temporaryPath, payload, {
4583
+ await withTempFile({
4584
+ rootDir: path.dirname(filePath),
4585
+ prefix: "session-turn",
4586
+ fileName: "lock"
4587
+ }, async (temporaryPath) => {
4588
+ await fs$1.writeFile(temporaryPath, payload, {
4589
+ encoding: "utf8",
4590
+ flag: "wx",
4591
+ mode: 384
4592
+ });
4593
+ signal?.throwIfAborted();
4594
+ try {
4595
+ await fs$1.link(temporaryPath, filePath);
4596
+ } catch (error) {
4597
+ if (!isHardlinkFallbackError(error)) throw error;
4598
+ await fs$1.writeFile(filePath, payload, {
4466
4599
  encoding: "utf8",
4467
4600
  flag: "wx",
4468
4601
  mode: 384
4469
4602
  });
4470
- signal?.throwIfAborted();
4471
- try {
4472
- await fs$1.link(temporaryPath, filePath);
4473
- } catch (error) {
4474
- if (!isHardlinkFallbackError(error)) throw error;
4475
- await fs$1.writeFile(filePath, payload, {
4476
- encoding: "utf8",
4477
- flag: "wx",
4478
- mode: 384
4479
- });
4480
- }
4481
- });
4482
- } catch (error) {
4483
- await removePublishedLock(filePath, payload).catch(() => {});
4484
- throw error;
4485
- }
4603
+ }
4604
+ });
4486
4605
  }
4487
4606
  async function removePublishedLock(filePath, payload) {
4488
4607
  const current = await readLock(filePath);
4489
4608
  if (current?.payload === payload) await removeObservedLock(filePath, current);
4490
4609
  }
4491
- async function observePublishedLock(filePath, payload, signal) {
4610
+ async function rejectAcquisition(filePath, payload, guard, error) {
4611
+ const failures = [error];
4612
+ await removePublishedLock(filePath, payload).catch((cleanupError) => {
4613
+ failures.push(cleanupError);
4614
+ });
4615
+ await guard.release().catch((releaseError) => {
4616
+ failures.push(releaseError);
4617
+ });
4618
+ if (failures.length > 1) throw new AggregateError(failures, "Session turn acquisition and cleanup failed");
4619
+ throw error;
4620
+ }
4621
+ function turnReceipt(filePath, observed, guard) {
4622
+ let disposed = false;
4623
+ let disposal;
4624
+ return { [Symbol.asyncDispose]: async () => {
4625
+ if (disposed) return;
4626
+ const pending = disposal ??= (async () => {
4627
+ await removeObservedLock(filePath, observed);
4628
+ await guard.release();
4629
+ disposed = true;
4630
+ })();
4631
+ try {
4632
+ await pending;
4633
+ } finally {
4634
+ if (disposal === pending) disposal = void 0;
4635
+ }
4636
+ } };
4637
+ }
4638
+ async function tryPublishLock(filePath, payload, signal) {
4639
+ for (;;) try {
4640
+ await publishLock(filePath, payload, signal);
4641
+ return true;
4642
+ } catch (error) {
4643
+ if (error.code !== "EEXIST") throw error;
4644
+ if (!await recoverAbandonedLock(filePath)) return false;
4645
+ }
4646
+ }
4647
+ async function tryAcquireTurn(filePath, payload, signal) {
4648
+ const guard = await tryAcquireGuard(filePath);
4649
+ if (!guard) return;
4492
4650
  try {
4651
+ signal?.throwIfAborted();
4652
+ if (!await tryPublishLock(filePath, payload, signal)) {
4653
+ await guard.release();
4654
+ return;
4655
+ }
4493
4656
  const observed = await readLock(filePath);
4494
4657
  if (observed?.payload !== payload) throw new Error(`Session turn ownership changed before admission: ${filePath}`);
4495
4658
  signal?.throwIfAborted();
4496
- return { [Symbol.asyncDispose]: async () => {
4497
- await removeObservedLock(filePath, observed);
4498
- } };
4659
+ return turnReceipt(filePath, observed, guard);
4499
4660
  } catch (error) {
4500
- await removePublishedLock(filePath, payload).catch(() => {});
4501
- throw error;
4661
+ return await rejectAcquisition(filePath, payload, guard, error);
4502
4662
  }
4503
4663
  }
4504
4664
  async function acquireSessionTurn(recordId, signal) {
4505
- const filePath = sessionEventLockPath(recordId);
4506
- await fs$1.mkdir(path.dirname(filePath), {
4665
+ const requestedPath = sessionEventLockPath(recordId);
4666
+ await fs$1.mkdir(path.dirname(requestedPath), {
4507
4667
  recursive: true,
4508
4668
  mode: 448
4509
4669
  });
4670
+ const filePath = path.join(await fs$1.realpath(path.dirname(requestedPath)), path.basename(requestedPath));
4510
4671
  lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);
4511
4672
  const payload = `${JSON.stringify({
4512
4673
  pid: process.pid,
@@ -4514,16 +4675,10 @@ async function acquireSessionTurn(recordId, signal) {
4514
4675
  }, null, 2)}\n`;
4515
4676
  for (;;) {
4516
4677
  signal?.throwIfAborted();
4517
- try {
4518
- await publishLock(filePath, payload, signal);
4519
- break;
4520
- } catch (error) {
4521
- if (error.code !== "EEXIST") throw error;
4522
- await recoverAbandonedLock(filePath);
4523
- await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4524
- }
4678
+ const turn = await tryAcquireTurn(filePath, payload, signal);
4679
+ if (turn) return turn;
4680
+ await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4525
4681
  }
4526
- return await observePublishedLock(filePath, payload, signal);
4527
4682
  }
4528
4683
  //#endregion
4529
4684
  //#region src/session/queue/ipc-server.ts
@@ -4704,6 +4859,7 @@ var SessionQueueOwner = class SessionQueueOwner {
4704
4859
  return true;
4705
4860
  }
4706
4861
  handleControlRequest(options) {
4862
+ this.taskSockets.add(options.socket);
4707
4863
  writeQueueMessage(options.socket, {
4708
4864
  type: "accepted",
4709
4865
  requestId: options.requestId,
@@ -5016,7 +5172,7 @@ async function replayDesiredConfigOptions(params) {
5016
5172
  for (const [configId, value] of Object.entries(params.desiredConfigOptions)) {
5017
5173
  if (configId === replacingKey || acceptedConfigOptions && !acceptsSavedConfigValue(acceptedConfigOptions.find((option) => option.id === configId), value)) continue;
5018
5174
  try {
5019
- const response = await withTimeout(params.client.setSessionConfigOption(params.sessionId, configId, value, params.replay.authority), params.timeoutMs);
5175
+ const response = await withTimeout(params.client.setSessionConfigOption(params.sessionId, configId, value, advertisedModelState(params.record.acpx), params.replay.authority), params.timeoutMs);
5020
5176
  params.replay.acknowledged = true;
5021
5177
  params.record.acpx = applyConfigOptionSelection(params.record.acpx, configId, value, response);
5022
5178
  acceptedConfigOptions = response.configOptions;
@@ -5067,6 +5223,7 @@ async function connectAndLoadSession(options) {
5067
5223
  timeoutMs: options.timeoutMs
5068
5224
  });
5069
5225
  const { resumed, loadError, sessionId, createdFreshSession, pendingAgentSessionId, sessionModels } = loadState;
5226
+ applyReconnectedModelState(record, sessionModels, loadState.configOptionsPresent, loadState.legacyModelMetadataPresent, createdFreshSession);
5070
5227
  const preferenceReplay = await replaySessionPreferences({
5071
5228
  client,
5072
5229
  record,
@@ -5287,7 +5444,7 @@ function createActiveSessionController(params) {
5287
5444
  return response;
5288
5445
  },
5289
5446
  setSessionConfigOption: async (configId, value) => {
5290
- return await params.client.setSessionConfigOption(getActiveSessionId(), configId, value);
5447
+ return await params.client.setSessionConfigOption(getActiveSessionId(), configId, value, advertisedModelState(params.record.acpx));
5291
5448
  }
5292
5449
  };
5293
5450
  }
@@ -5410,14 +5567,38 @@ function toSessionMutationResult(result) {
5410
5567
  loadError: result.loadError
5411
5568
  };
5412
5569
  }
5570
+ async function withOwnedDirectSession(options) {
5571
+ const waiting = new AbortController();
5572
+ const timeoutMs = options.timeoutMs;
5573
+ const timer = timeoutMs != null && timeoutMs > 0 ? setTimeout(() => waiting.abort(new TimeoutError(timeoutMs)), timeoutMs) : void 0;
5574
+ let ownership;
5575
+ let recordId;
5576
+ try {
5577
+ recordId = (await options.loadRecord(options.sessionRecordId)).acpxRecordId;
5578
+ ownership = await acquireSessionTurn(recordId, waiting.signal);
5579
+ } catch (error) {
5580
+ waiting.signal.throwIfAborted();
5581
+ throw error;
5582
+ } finally {
5583
+ clearTimeout(timer);
5584
+ }
5585
+ try {
5586
+ return await withConnectedSession({
5587
+ ...options,
5588
+ sessionRecordId: recordId
5589
+ });
5590
+ } finally {
5591
+ await ownership[Symbol.asyncDispose]();
5592
+ }
5593
+ }
5413
5594
  async function runSessionSetModeDirect(options) {
5414
- return toSessionMutationResult(await withConnectedSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
5595
+ return toSessionMutationResult(await withOwnedDirectSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
5415
5596
  await withTimeout(client.setSessionMode(sessionId, options.modeId), options.timeoutMs);
5416
5597
  setDesiredModeId(record, options.modeId);
5417
5598
  })));
5418
5599
  }
5419
5600
  async function runSessionSetModelDirect(options) {
5420
- const result = await withConnectedSession(buildDirectConnectedSessionOptions({
5601
+ const result = await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5421
5602
  ...options,
5422
5603
  replacingConfigOption: { key: "model" }
5423
5604
  }, async ({ client, sessionId, record }) => {
@@ -5432,12 +5613,13 @@ async function runSessionSetModelDirect(options) {
5432
5613
  };
5433
5614
  }
5434
5615
  async function runSessionSetConfigOptionDirect(options) {
5435
- const result = await withConnectedSession(buildDirectConnectedSessionOptions({
5616
+ const result = await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5436
5617
  ...options,
5437
5618
  replacingConfigOption: { key: options.configId }
5438
5619
  }, async ({ client, sessionId, record }) => {
5439
- const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value), options.timeoutMs);
5440
- record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, response);
5620
+ const models = advertisedModelState(record.acpx);
5621
+ const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value, models), options.timeoutMs);
5622
+ record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, response, models?.configId);
5441
5623
  return response;
5442
5624
  }));
5443
5625
  return {
@@ -5456,16 +5638,31 @@ async function cancelSessionPrompt(options) {
5456
5638
  cancelled: cancelled === true
5457
5639
  };
5458
5640
  }
5459
- async function setSessionMode(options) {
5460
- if (await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose)) {
5461
- const record = await resolveSessionRecord(options.sessionId);
5641
+ /**
5642
+ * Dispatches the mode change to a running queue owner. Returns `undefined`
5643
+ * when no owner holds the session, leaving the direct-connection decision to
5644
+ * the caller.
5645
+ *
5646
+ * An owner advertising `persistsControlState` saves the accepted state itself.
5647
+ * Only the older caller-save path still reloads and rewrites the record here,
5648
+ * which is not serialized against another process doing the same.
5649
+ */
5650
+ async function setSessionModeOnOwner(options) {
5651
+ const submittedToOwner = await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose, options.assertDispatch);
5652
+ if (!submittedToOwner?.value) return;
5653
+ const record = await resolveSessionRecord(options.sessionId);
5654
+ if (!submittedToOwner.persistsControlState) {
5462
5655
  setDesiredModeId(record, options.modeId);
5463
5656
  await writeSessionRecord(record);
5464
- return {
5465
- record,
5466
- resumed: false
5467
- };
5468
5657
  }
5658
+ return {
5659
+ record,
5660
+ resumed: false
5661
+ };
5662
+ }
5663
+ async function setSessionMode(options) {
5664
+ const onOwner = await setSessionModeOnOwner(options);
5665
+ if (onOwner) return onOwner;
5469
5666
  return await runSessionSetModeDirect({
5470
5667
  sessionRecordId: options.sessionId,
5471
5668
  modeId: options.modeId,
@@ -5479,18 +5676,24 @@ async function setSessionMode(options) {
5479
5676
  verbose: options.verbose
5480
5677
  });
5481
5678
  }
5482
- async function setSessionModel(options) {
5483
- const submittedToOwner = await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose);
5484
- if (submittedToOwner) {
5485
- const record = await resolveSessionRecord(options.sessionId);
5486
- record.acpx = applyModelSelection(record.acpx, options.modelId, submittedToOwner.response);
5679
+ /** Owner-only counterpart of {@link setSessionMode} for the model control. */
5680
+ async function setSessionModelOnOwner(options) {
5681
+ const submittedToOwner = await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose, options.assertDispatch);
5682
+ if (!submittedToOwner) return;
5683
+ const record = await resolveSessionRecord(options.sessionId);
5684
+ if (!submittedToOwner.persistsControlState) {
5685
+ record.acpx = applyModelSelection(record.acpx, options.modelId, submittedToOwner.value.response);
5487
5686
  await writeSessionRecord(record);
5488
- return {
5489
- record,
5490
- response: submittedToOwner.response,
5491
- resumed: false
5492
- };
5493
5687
  }
5688
+ return {
5689
+ record,
5690
+ response: submittedToOwner.value.response,
5691
+ resumed: false
5692
+ };
5693
+ }
5694
+ async function setSessionModel(options) {
5695
+ const onOwner = await setSessionModelOnOwner(options);
5696
+ if (onOwner) return onOwner;
5494
5697
  return await runSessionSetModelDirect({
5495
5698
  sessionRecordId: options.sessionId,
5496
5699
  modelId: options.modelId,
@@ -5504,18 +5707,24 @@ async function setSessionModel(options) {
5504
5707
  verbose: options.verbose
5505
5708
  });
5506
5709
  }
5507
- async function setSessionConfigOption(options) {
5508
- const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose);
5509
- if (ownerResponse) {
5510
- const record = await resolveSessionRecord(options.sessionId);
5511
- record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, ownerResponse);
5710
+ /** Owner-only counterpart of {@link setSessionMode} for the config option control. */
5711
+ async function setSessionConfigOptionOnOwner(options) {
5712
+ const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose, options.assertDispatch);
5713
+ if (!ownerResponse) return;
5714
+ const record = await resolveSessionRecord(options.sessionId);
5715
+ if (!ownerResponse.persistsControlState) {
5716
+ record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, ownerResponse.value);
5512
5717
  await writeSessionRecord(record);
5513
- return {
5514
- record,
5515
- response: ownerResponse,
5516
- resumed: false
5517
- };
5518
5718
  }
5719
+ return {
5720
+ record,
5721
+ response: ownerResponse.value,
5722
+ resumed: false
5723
+ };
5724
+ }
5725
+ async function setSessionConfigOption(options) {
5726
+ const onOwner = await setSessionConfigOptionOnOwner(options);
5727
+ if (onOwner) return onOwner;
5519
5728
  return await runSessionSetConfigOptionDirect({
5520
5729
  sessionRecordId: options.sessionId,
5521
5730
  configId: options.configId,
@@ -5600,11 +5809,17 @@ async function closeSession(sessionId) {
5600
5809
  await tryCloseSessionOnRunningOwner({ sessionId: record.acpxRecordId }).catch(() => {});
5601
5810
  await terminateQueueOwnerForSession(record.acpxRecordId);
5602
5811
  if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) await terminateProcess(record.pid);
5603
- record.pid = void 0;
5604
- record.closed = true;
5605
- record.closedAt = isoNow$1();
5606
- await writeSessionRecord(record);
5607
- return record;
5812
+ const ownership = await acquireSessionTurn(record.acpxRecordId);
5813
+ try {
5814
+ const current = await resolveSessionRecord(record.acpxRecordId);
5815
+ current.pid = void 0;
5816
+ current.closed = true;
5817
+ current.closedAt = isoNow$1();
5818
+ await writeSessionRecord(current);
5819
+ return current;
5820
+ } finally {
5821
+ await ownership[Symbol.asyncDispose]();
5822
+ }
5608
5823
  }
5609
5824
  //#endregion
5610
5825
  //#region src/session/execution/session-management.ts
@@ -5925,16 +6140,182 @@ function installPerfMetricsCapture(options = {}) {
5925
6140
  }
5926
6141
  }
5927
6142
  //#endregion
6143
+ //#region src/session/queue/control-admission.ts
6144
+ var QueueControlDeadline = class {
6145
+ controller = new AbortController();
6146
+ admissionSignal;
6147
+ expiresAt;
6148
+ expired;
6149
+ timeoutError;
6150
+ rejectExpiry;
6151
+ timer;
6152
+ nativeSettled = false;
6153
+ admitted = false;
6154
+ constructor(timeoutMs, ownerSignal) {
6155
+ this.admissionSignal = ownerSignal ? AbortSignal.any([this.controller.signal, ownerSignal]) : this.controller.signal;
6156
+ this.expiresAt = timeoutMs != null && timeoutMs > 0 ? performance.now() + timeoutMs : void 0;
6157
+ this.timeoutError = this.expiresAt === void 0 ? void 0 : new TimeoutError(timeoutMs);
6158
+ this.expired = new Promise((_resolve, reject) => {
6159
+ this.rejectExpiry = reject;
6160
+ if (this.expiresAt !== void 0) this.timer = setTimeout(() => this.expire(), timeoutMs);
6161
+ });
6162
+ this.expired.catch(() => {});
6163
+ }
6164
+ get signal() {
6165
+ return this.admissionSignal;
6166
+ }
6167
+ get timeoutSignal() {
6168
+ return this.controller.signal;
6169
+ }
6170
+ get remainingMs() {
6171
+ this.assertActive();
6172
+ return this.expiresAt === void 0 || this.nativeSettled ? void 0 : Math.max(1, this.expiresAt - performance.now());
6173
+ }
6174
+ get nativeAdmitted() {
6175
+ return this.admitted;
6176
+ }
6177
+ get authority() {
6178
+ return {
6179
+ signal: this.signal,
6180
+ assertActive: () => {
6181
+ this.assertActive();
6182
+ this.admitted = true;
6183
+ }
6184
+ };
6185
+ }
6186
+ assertActive() {
6187
+ if (this.expiresAt !== void 0 && performance.now() >= this.expiresAt) this.expire();
6188
+ this.signal.throwIfAborted();
6189
+ }
6190
+ expire() {
6191
+ if (this.nativeSettled || this.timeoutSignal.aborted || !this.timeoutError) return;
6192
+ this.controller.abort(this.timeoutError);
6193
+ this.rejectExpiry(this.timeoutError);
6194
+ }
6195
+ responseSettled() {
6196
+ this.nativeSettled = true;
6197
+ clearTimeout(this.timer);
6198
+ this.timer = void 0;
6199
+ }
6200
+ isExpired(error) {
6201
+ return this.timeoutSignal.aborted && error === this.timeoutSignal.reason;
6202
+ }
6203
+ async wait(operation) {
6204
+ return await Promise.race([operation, this.expired]);
6205
+ }
6206
+ };
6207
+ function deferred() {
6208
+ let resolve;
6209
+ let reject;
6210
+ const promise = new Promise((accept, fail) => {
6211
+ resolve = accept;
6212
+ reject = fail;
6213
+ });
6214
+ promise.catch(() => {});
6215
+ return {
6216
+ promise,
6217
+ resolve,
6218
+ reject
6219
+ };
6220
+ }
6221
+ function drained(operation) {
6222
+ return operation.then(() => {}, () => {});
6223
+ }
6224
+ var QueueOwnerControlAdmission = class {
6225
+ options;
6226
+ idleTail = Promise.resolve();
6227
+ ticket;
6228
+ stopped = new AbortController();
6229
+ pending = 0;
6230
+ constructor(options) {
6231
+ this.options = options;
6232
+ }
6233
+ get hasPending() {
6234
+ return this.pending > 0;
6235
+ }
6236
+ beginPrompt() {
6237
+ const ticket = {
6238
+ ready: deferred(),
6239
+ released: deferred(),
6240
+ rawTail: Promise.resolve(),
6241
+ sealed: false
6242
+ };
6243
+ this.ticket = ticket;
6244
+ return {
6245
+ ticket,
6246
+ priorIdle: this.idleTail
6247
+ };
6248
+ }
6249
+ publish(ticket, controls) {
6250
+ if (!ticket.sealed) ticket.ready.resolve(controls);
6251
+ }
6252
+ seal(ticket) {
6253
+ ticket.sealed = true;
6254
+ ticket.ready.reject(new QueueConnectionError("Prompt ended before controls became ready", {
6255
+ detailCode: "QUEUE_CONTROL_REQUEST_FAILED",
6256
+ origin: "queue",
6257
+ retryable: true
6258
+ }));
6259
+ return ticket.rawTail;
6260
+ }
6261
+ release(ticket) {
6262
+ if (this.ticket === ticket) this.ticket = void 0;
6263
+ ticket.released.resolve();
6264
+ }
6265
+ stopAdmission() {
6266
+ this.stopped.abort(new QueueConnectionError("Queue owner is closing", {
6267
+ detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
6268
+ origin: "queue",
6269
+ retryable: true
6270
+ }));
6271
+ }
6272
+ async drainAdmitted() {
6273
+ await Promise.all([this.idleTail, this.ticket?.rawTail]);
6274
+ }
6275
+ async run(invoke, timeoutMs, replacingConfigOption) {
6276
+ const deadline = new QueueControlDeadline(timeoutMs, this.stopped.signal);
6277
+ this.pending += 1;
6278
+ const operation = this.enqueue(invoke, {
6279
+ deadline,
6280
+ replacingConfigOption
6281
+ });
6282
+ operation.finally(() => {
6283
+ this.pending -= 1;
6284
+ deadline.responseSettled();
6285
+ }).catch(() => {});
6286
+ return await deadline.wait(operation);
6287
+ }
6288
+ async enqueue(invoke, options) {
6289
+ options.deadline.assertActive();
6290
+ const ticket = this.ticket;
6291
+ if (ticket?.sealed) return ticket.released.promise.then(() => this.enqueue(invoke, options));
6292
+ if (ticket) {
6293
+ const operation = ticket.rawTail.then(async () => {
6294
+ const controls = await ticket.ready.promise.catch((error) => {
6295
+ options.deadline.assertActive();
6296
+ throw error;
6297
+ });
6298
+ options.deadline.assertActive();
6299
+ return await invoke(controls, options.deadline);
6300
+ });
6301
+ ticket.rawTail = drained(operation);
6302
+ return operation;
6303
+ }
6304
+ const operation = this.idleTail.then(async () => {
6305
+ options.deadline.assertActive();
6306
+ return await this.options.runIdle(invoke, options);
6307
+ });
6308
+ this.idleTail = drained(operation);
6309
+ return operation;
6310
+ }
6311
+ };
6312
+ //#endregion
5928
6313
  //#region src/session/queue/owner-turn-controller.ts
5929
6314
  var QueueOwnerTurnController = class {
5930
- options;
5931
6315
  state = "idle";
5932
6316
  pendingCancel = false;
5933
6317
  activeController;
5934
6318
  waitingTurn;
5935
- constructor(options) {
5936
- this.options = options;
5937
- }
5938
6319
  get lifecycleState() {
5939
6320
  return this.state;
5940
6321
  }
@@ -5966,20 +6347,9 @@ var QueueOwnerTurnController = class {
5966
6347
  clearActiveController() {
5967
6348
  this.activeController = void 0;
5968
6349
  }
5969
- assertCanHandleControlRequest() {
5970
- if (this.state === "closing") throw new QueueConnectionError("Queue owner is closing", {
5971
- detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
5972
- origin: "queue",
5973
- retryable: true
5974
- });
5975
- }
5976
6350
  async requestCancel() {
5977
6351
  const activeController = this.activeController;
5978
- if (activeController?.hasActivePrompt()) {
5979
- const cancelled = await activeController.requestCancelActivePrompt();
5980
- if (cancelled) this.pendingCancel = false;
5981
- return cancelled;
5982
- }
6352
+ if (activeController?.hasActivePrompt()) return await this.cancelActivePrompt(activeController);
5983
6353
  if (this.state === "starting" || this.state === "active") {
5984
6354
  this.pendingCancel = true;
5985
6355
  this.waitingTurn?.abort();
@@ -5990,32 +6360,138 @@ var QueueOwnerTurnController = class {
5990
6360
  async applyPendingCancel() {
5991
6361
  const activeController = this.activeController;
5992
6362
  if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) return false;
5993
- const cancelled = await activeController.requestCancelActivePrompt();
5994
- if (cancelled) this.pendingCancel = false;
6363
+ return await this.cancelActivePrompt(activeController);
6364
+ }
6365
+ async cancelActivePrompt(activeController) {
6366
+ const turn = this.waitingTurn;
6367
+ const cancellation = activeController.requestCancelActivePrompt();
6368
+ turn?.abort();
6369
+ const cancelled = await cancellation;
6370
+ if (cancelled && this.waitingTurn === turn) this.pendingCancel = false;
5995
6371
  return cancelled;
5996
6372
  }
5997
- async setSessionMode(modeId, timeoutMs) {
5998
- this.assertCanHandleControlRequest();
5999
- const activeController = this.activeController;
6000
- if (activeController) {
6001
- await this.options.withTimeout(async () => await activeController.setSessionMode(modeId), timeoutMs);
6002
- return;
6373
+ };
6374
+ //#endregion
6375
+ //#region src/session/execution/owned-controls.ts
6376
+ async function acceptControl(options, invoke, apply, deadline = new QueueControlDeadline()) {
6377
+ deadline.assertActive();
6378
+ const raw = invoke(deadline.authority).then(async (response) => {
6379
+ deadline.responseSettled();
6380
+ apply(response);
6381
+ await options.checkpoint();
6382
+ return response;
6383
+ }, (error) => {
6384
+ deadline.responseSettled();
6385
+ throw error;
6386
+ });
6387
+ try {
6388
+ return await deadline.wait(raw);
6389
+ } catch (error) {
6390
+ if (deadline.isExpired(error) && deadline.nativeAdmitted) {
6391
+ const [retired] = await Promise.allSettled([options.retire(), raw]);
6392
+ if (retired.status === "rejected") throw retired.reason;
6003
6393
  }
6004
- await this.options.setSessionModeFallback(modeId, timeoutMs);
6394
+ throw error;
6005
6395
  }
6006
- async setSessionModel(modelId, timeoutMs) {
6007
- this.assertCanHandleControlRequest();
6008
- const activeController = this.activeController;
6009
- if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionModel(modelId), timeoutMs);
6010
- return await this.options.setSessionModelFallback(modelId, timeoutMs);
6396
+ }
6397
+ function createOwnedSessionControls(options) {
6398
+ return {
6399
+ setSessionMode: async (modeId, deadline) => {
6400
+ await acceptControl(options, (authority) => options.client.setSessionMode(options.sessionId(), modeId, authority), () => setDesiredModeId(options.record, modeId), deadline);
6401
+ },
6402
+ setSessionModel: async (modelId, deadline) => {
6403
+ const models = advertisedModelState(options.record.acpx);
6404
+ return await acceptControl(options, (authority) => options.client.setSessionModel(options.sessionId(), modelId, models, authority), (response) => {
6405
+ options.record.acpx = applyModelSelection(options.record.acpx, modelId, response);
6406
+ }, deadline);
6407
+ },
6408
+ setSessionConfigOption: async (configId, value, deadline) => {
6409
+ const models = advertisedModelState(options.record.acpx);
6410
+ return await acceptControl(options, (authority) => options.client.setSessionConfigOption(options.sessionId(), configId, value, models, authority), (response) => {
6411
+ options.record.acpx = applyConfigOptionSelection(options.record.acpx, configId, value, response, models?.configId);
6412
+ }, deadline);
6413
+ }
6414
+ };
6415
+ }
6416
+ async function runIdleControlWithRecord(options, record, invoke) {
6417
+ const { client, deadline } = options;
6418
+ deadline.assertActive();
6419
+ let retirement;
6420
+ const retire = () => {
6421
+ retirement ??= client.close().catch((error) => {
6422
+ options.onRetirementFailure();
6423
+ throw error;
6424
+ });
6425
+ return retirement;
6426
+ };
6427
+ const onAbort = () => {
6428
+ retire().catch(() => {});
6429
+ };
6430
+ const checkpoint = async () => {
6431
+ record.lastUsedAt = isoNow$1();
6432
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
6433
+ await writeSessionRecord(record);
6434
+ };
6435
+ let sessionId = record.acpSessionId;
6436
+ const controls = createOwnedSessionControls({
6437
+ client,
6438
+ record,
6439
+ sessionId: () => sessionId,
6440
+ checkpoint,
6441
+ retire
6442
+ });
6443
+ client.setEventHandlers({ onSessionUpdate: (notification) => {
6444
+ record.acpx = recordSessionUpdate(record, record.acpx, notification);
6445
+ } });
6446
+ deadline.timeoutSignal.addEventListener("abort", onAbort, { once: true });
6447
+ let connected = false;
6448
+ let completed = false;
6449
+ try {
6450
+ await connectAndLoadSession({
6451
+ client,
6452
+ record,
6453
+ resumePolicy: "same-session-only",
6454
+ replacingConfigOption: options.replacingConfigOption,
6455
+ timeoutMs: deadline.remainingMs,
6456
+ authority: deadline.authority,
6457
+ verbose: options.verbose,
6458
+ activeController: {
6459
+ ...controls,
6460
+ hasActivePrompt: () => client.hasActivePrompt(),
6461
+ requestCancelActivePrompt: () => client.requestCancelActivePrompt()
6462
+ },
6463
+ onSessionIdResolved: (value) => {
6464
+ sessionId = value;
6465
+ }
6466
+ });
6467
+ connected = true;
6468
+ deadline.assertActive();
6469
+ const value = await invoke(controls, deadline);
6470
+ completed = true;
6471
+ return value;
6472
+ } finally {
6473
+ deadline.timeoutSignal.removeEventListener("abort", onAbort);
6474
+ try {
6475
+ if (!connected) await retire();
6476
+ await retirement;
6477
+ if (!completed || retirement) await checkpoint().catch(() => {});
6478
+ } finally {
6479
+ client.clearEventHandlers();
6480
+ }
6011
6481
  }
6012
- async setSessionConfigOption(configId, value, timeoutMs) {
6013
- this.assertCanHandleControlRequest();
6014
- const activeController = this.activeController;
6015
- if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionConfigOption(configId, value), timeoutMs);
6016
- return await this.options.setSessionConfigOptionFallback(configId, value, timeoutMs);
6482
+ }
6483
+ async function runIdleOwnerControl(options, invoke) {
6484
+ const { acpxRecordId } = await resolveSessionRecord(options.sessionId);
6485
+ const ownership = await acquireSessionTurn(acpxRecordId, options.deadline.signal).catch((error) => {
6486
+ options.deadline.assertActive();
6487
+ throw error;
6488
+ });
6489
+ try {
6490
+ return await runIdleControlWithRecord(options, await resolveSessionRecord(acpxRecordId), invoke);
6491
+ } finally {
6492
+ await ownership[Symbol.asyncDispose]();
6017
6493
  }
6018
- };
6494
+ }
6019
6495
  //#endregion
6020
6496
  //#region src/session/execution/queue-owner-process.ts
6021
6497
  function isNonEmptyStringArray(value) {
@@ -6197,20 +6673,27 @@ function responseMetaField(meta) {
6197
6673
  return meta === void 0 ? {} : { _meta: meta };
6198
6674
  }
6199
6675
  function recoveredSessionResult(response, conversation, promptMessageId) {
6200
- recordPromptResponseUsage(conversation, response?.usage, promptMessageId);
6676
+ recordPromptResponseUsage(conversation, response.usage, promptMessageId);
6201
6677
  return {
6202
- stopReason: "end_turn",
6678
+ stopReason: response.stopReason,
6203
6679
  source: "session",
6204
- ...responseMetaField(response?._meta)
6680
+ ...responseMetaField(response._meta)
6205
6681
  };
6206
6682
  }
6207
- function abortTimedOutRequests(error, client, sessionId) {
6208
- if (error instanceof TimeoutError) client.endPromptElicitation?.(sessionId);
6683
+ function abortTimedOutRequests(error, lifetime) {
6684
+ if (error instanceof TimeoutError) lifetime.abort(error);
6685
+ }
6686
+ function promptRequestAuthority(lifetime, authority) {
6687
+ return {
6688
+ signal: authority?.signal ? AbortSignal.any([authority.signal, lifetime.signal]) : lifetime.signal,
6689
+ assertActive: () => assertControlAuthority(authority)
6690
+ };
6209
6691
  }
6210
6692
  async function runPromptTurn(params) {
6211
6693
  let settledResponse;
6694
+ const lifetime = new AbortController();
6212
6695
  try {
6213
- const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestWritten, params.onElicitation, params.onPermissionRequest);
6696
+ const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestWritten, params.onElicitation, params.onPermissionRequest, promptRequestAuthority(lifetime, params.authority));
6214
6697
  promptPromise.then((response) => {
6215
6698
  settledResponse = response;
6216
6699
  }, () => {});
@@ -6227,14 +6710,16 @@ async function runPromptTurn(params) {
6227
6710
  ...responseMetaField(response._meta)
6228
6711
  };
6229
6712
  } catch (error) {
6230
- abortTimedOutRequests(error, params.client, params.sessionId);
6713
+ abortTimedOutRequests(error, lifetime);
6231
6714
  if (!(error instanceof TimeoutError) || !params.promptMessageId) throw error;
6232
6715
  await params.client.waitForSessionUpdatesIdle?.({
6233
6716
  idleMs: SESSION_REPLY_IDLE_MS,
6234
6717
  timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS
6235
6718
  }).catch(() => {});
6236
- if (hasAgentReplyAfterPrompt(params.conversation, params.promptMessageId)) return recoveredSessionResult(settledResponse, params.conversation, params.promptMessageId);
6719
+ if (settledResponse) return recoveredSessionResult(settledResponse, params.conversation, params.promptMessageId);
6237
6720
  throw error;
6721
+ } finally {
6722
+ lifetime.abort();
6238
6723
  }
6239
6724
  }
6240
6725
  //#endregion
@@ -6484,8 +6969,9 @@ function emitPromptDisconnectNotice(snapshot, verbose) {
6484
6969
  if (!lastExit?.unexpectedDuringPrompt || !verbose) return;
6485
6970
  process.stderr.write("[acpx] agent disconnected during prompt (" + lastExit.reason + ", exit=" + lastExit.exitCode + ", signal=" + (lastExit.signal ?? "none") + ")\n");
6486
6971
  }
6487
- async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors) {
6972
+ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors, signal) {
6488
6973
  if (attempt < maxRetries && !hasSideEffects() && isRetryablePromptError(error)) {
6974
+ signal?.throwIfAborted();
6489
6975
  const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
6490
6976
  emitPromptRetryNotice({
6491
6977
  error,
@@ -6494,11 +6980,18 @@ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, su
6494
6980
  maxRetries,
6495
6981
  suppressSdkConsoleErrors
6496
6982
  });
6497
- await waitMs$1(delayMs);
6983
+ await setTimeout$1(delayMs, void 0, { signal }).catch((error) => {
6984
+ signal?.throwIfAborted();
6985
+ throw error;
6986
+ });
6987
+ signal?.throwIfAborted();
6498
6988
  return !hasSideEffects();
6499
6989
  }
6500
6990
  return false;
6501
6991
  }
6992
+ function isTurnCancellation(error, signal) {
6993
+ return signal?.aborted === true && error === signal.reason;
6994
+ }
6502
6995
  function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatter) {
6503
6996
  return {
6504
6997
  sessionRecordId,
@@ -6520,7 +7013,9 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
6520
7013
  sessionOptions: mergeSessionOptions(task.sessionOptions, options.sessionOptions),
6521
7014
  onClientAvailable: options.onClientAvailable,
6522
7015
  onClientClosed: options.onClientClosed,
7016
+ onClientCloseFailure: options.onClientCloseFailure,
6523
7017
  onPromptActive: options.onPromptActive,
7018
+ onPromptFinalizing: options.onPromptFinalizing,
6524
7019
  onPromptRequestWritten: () => {
6525
7020
  if (task.reportPromptStarted) task.send({
6526
7021
  type: "prompt_started",
@@ -6826,24 +7321,23 @@ async function runOwnedSessionPrompt(options) {
6826
7321
  });
6827
7322
  let activeSessionIdForControl = record.acpSessionId;
6828
7323
  let notifiedClientAvailable = false;
7324
+ let controlRetirement;
6829
7325
  const activeController = {
6830
7326
  hasActivePrompt: () => client.hasActivePrompt(),
6831
7327
  requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
6832
- setSessionMode: async (modeId) => {
6833
- await client.setSessionMode(activeSessionIdForControl, modeId);
6834
- },
6835
- setSessionModel: async (modelId) => {
6836
- const models = advertisedModelState(record.acpx);
6837
- const response = await client.setSessionModel(activeSessionIdForControl, modelId, models);
6838
- record.acpx = applyModelSelection(record.acpx, modelId, response);
6839
- return response;
6840
- },
6841
- setSessionConfigOption: async (configId, value) => {
6842
- const modelConfigId = advertisedModelState(record.acpx)?.configId;
6843
- const response = await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
6844
- record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response, modelConfigId);
6845
- return response;
6846
- }
7328
+ ...createOwnedSessionControls({
7329
+ client,
7330
+ record,
7331
+ sessionId: () => activeSessionIdForControl,
7332
+ checkpoint: () => liveCheckpoint.checkpoint(),
7333
+ retire: () => {
7334
+ controlRetirement ??= client.close().catch((error) => {
7335
+ options.onClientCloseFailure?.();
7336
+ throw error;
7337
+ });
7338
+ return controlRetirement;
7339
+ }
7340
+ })
6847
7341
  };
6848
7342
  const flushConnectOutput = (loadError) => {
6849
7343
  bufferingConnectOutput = false;
@@ -6907,15 +7401,17 @@ async function runOwnedSessionPrompt(options) {
6907
7401
  conversation,
6908
7402
  promptMessageId,
6909
7403
  onPromptRequestWritten: options.onPromptRequestWritten,
6910
- onPromptStarted: buildPromptStartedHook(attempt)
7404
+ onPromptStarted: buildPromptStartedHook(attempt),
7405
+ authority: { signal: options.waitSignal }
6911
7406
  });
6912
7407
  });
6913
7408
  emitPromptPerfMetric(promptStartedAt, options.verbose);
6914
7409
  return response;
6915
7410
  };
6916
7411
  const handlePromptFailure = async (error, attempt) => {
7412
+ if (isTurnCancellation(error, options.waitSignal)) throw error;
6917
7413
  const snapshot = client.getAgentLifecycleSnapshot();
6918
- if (snapshot.lastExit?.unexpectedDuringPrompt !== true && await preparePromptRetry(error, attempt, options.promptRetries ?? 0, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors)) return;
7414
+ if (snapshot.lastExit?.unexpectedDuringPrompt !== true && await preparePromptRetry(error, attempt, options.promptRetries ?? 0, () => promptTurnHadSideEffects, options.suppressSdkConsoleErrors, options.waitSignal)) return;
6919
7415
  await failRuntimePrompt(error, snapshot);
6920
7416
  };
6921
7417
  const failRuntimePrompt = async (error, snapshot) => {
@@ -6939,10 +7435,18 @@ async function runOwnedSessionPrompt(options) {
6939
7435
  };
6940
7436
  const runPromptWithRetries = async (sessionId) => {
6941
7437
  promptTurnActive = true;
6942
- for (let attempt = 0;; attempt++) try {
6943
- return await runPromptAttempt(sessionId, attempt);
7438
+ try {
7439
+ for (let attempt = 0;; attempt++) {
7440
+ options.waitSignal?.throwIfAborted();
7441
+ try {
7442
+ return await runPromptAttempt(sessionId, attempt);
7443
+ } catch (error) {
7444
+ await handlePromptFailure(error, attempt);
7445
+ }
7446
+ }
6944
7447
  } catch (error) {
6945
- await handlePromptFailure(error, attempt);
7448
+ if (isTurnCancellation(error, options.waitSignal)) return { stopReason: "cancelled" };
7449
+ throw error;
6946
7450
  }
6947
7451
  };
6948
7452
  const savePromptSuccess = async (response) => {
@@ -7003,7 +7507,24 @@ async function runOwnedSessionPrompt(options) {
7003
7507
  }
7004
7508
  };
7005
7509
  const cleanupPrompt = async (attempt) => {
7510
+ const controlDrain = options.onPromptFinalizing?.();
7511
+ const unresolvedPrompt = client.hasUnresolvedPrompt();
7006
7512
  const steps = [
7513
+ async () => {
7514
+ if (unresolvedPrompt) await withTimeout(client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS), INTERRUPT_CANCEL_WAIT_MS).catch(() => {});
7515
+ },
7516
+ async () => {
7517
+ const journalFailed = attempt.status === "rejected" && attempt.error instanceof AcpxOperationalError && attempt.error.detailCode === "SESSION_JOURNAL_WRITE_FAILED";
7518
+ if (unresolvedPrompt || closeClientOnExit || journalFailed) try {
7519
+ await client.close();
7520
+ } catch (error) {
7521
+ options.onClientCloseFailure?.();
7522
+ throw error;
7523
+ }
7524
+ },
7525
+ async () => {
7526
+ await controlDrain;
7527
+ },
7007
7528
  () => {
7008
7529
  const duration = stopTotalTimer();
7009
7530
  if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", duration)}\n`);
@@ -7012,10 +7533,6 @@ async function runOwnedSessionPrompt(options) {
7012
7533
  if (notifiedClientAvailable) options.onClientClosed?.();
7013
7534
  },
7014
7535
  () => client.clearEventHandlers(),
7015
- async () => {
7016
- const journalFailed = attempt.status === "rejected" && attempt.error instanceof AcpxOperationalError && attempt.error.detailCode === "SESSION_JOURNAL_WRITE_FAILED";
7017
- if (closeClientOnExit || journalFailed) await client.close();
7018
- },
7019
7536
  () => applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot()),
7020
7537
  () => applyConversation(record, conversation)
7021
7538
  ];
@@ -7042,6 +7559,7 @@ async function runOnce(options) {
7042
7559
  const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
7043
7560
  let promptTurnActive = false;
7044
7561
  let promptTurnHadSideEffects = false;
7562
+ let controlState = {};
7045
7563
  const acpErrors = new AcpErrorTracker();
7046
7564
  const client = new AcpClient({
7047
7565
  agentCommand: options.agentCommand,
@@ -7062,6 +7580,7 @@ async function runOnce(options) {
7062
7580
  acpErrors.observe(output, direction, message);
7063
7581
  },
7064
7582
  onSessionUpdate: (notification) => {
7583
+ if (notification.update.sessionUpdate === "config_option_update") controlState = applyConfigOptionsToState(controlState, notification.update.configOptions);
7065
7584
  if (promptTurnActive) promptTurnHadSideEffects = true;
7066
7585
  options.onSessionUpdate?.(notification);
7067
7586
  },
@@ -7101,7 +7620,9 @@ async function runOnce(options) {
7101
7620
  return await withTimeout(client.createSession(absolutePath(options.cwd)), options.timeoutMs);
7102
7621
  });
7103
7622
  const sessionId = createdSession.sessionId;
7104
- await applyRequestedModelIfAdvertised({
7623
+ if (createdSession.models) applyAdvertisedModelState(controlState, createdSession.models);
7624
+ controlState = applyConfigOptionsToState(controlState, createdSession.configOptions ?? []);
7625
+ const modelApplication = await applyRequestedModelIfAdvertised({
7105
7626
  client,
7106
7627
  sessionId,
7107
7628
  requestedModel: options.sessionOptions?.model,
@@ -7110,7 +7631,11 @@ async function runOnce(options) {
7110
7631
  timeoutMs: options.timeoutMs,
7111
7632
  onWarning: options.suppressSdkConsoleErrors ? void 0 : (message) => process.stderr.write(`[acpx] warning: ${message}\n`)
7112
7633
  });
7113
- for (const configOption of options.configOptions ?? []) await withTimeout(client.setSessionConfigOption(sessionId, configOption.configId, configOption.value), options.timeoutMs);
7634
+ if (modelApplication.response) controlState = applyConfigOptionsToState(controlState, modelApplication.response.configOptions);
7635
+ for (const configOption of options.configOptions ?? []) {
7636
+ const response = await withTimeout(client.setSessionConfigOption(sessionId, configOption.configId, configOption.value, advertisedModelState(controlState)), options.timeoutMs);
7637
+ controlState = applyConfigOptionsToState(controlState, response.configOptions);
7638
+ }
7114
7639
  output.setContext({ sessionId });
7115
7640
  const response = await runExecPromptWithRetries(sessionId);
7116
7641
  promptTurnActive = false;
@@ -7205,54 +7730,6 @@ function createQueueOwnerSharedClient(options, sessionRecord) {
7205
7730
  sessionOptions: mergeSessionOptions(options.sessionOptions, sessionOptionsFromRecord(sessionRecord))
7206
7731
  });
7207
7732
  }
7208
- function createQueueOwnerTurnController(options) {
7209
- return new QueueOwnerTurnController({
7210
- withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
7211
- setSessionModeFallback: async (modeId, timeoutMs) => {
7212
- await runSessionSetModeDirect({
7213
- sessionRecordId: options.sessionId,
7214
- modeId,
7215
- mcpServers: options.mcpServers,
7216
- nonInteractivePermissions: options.nonInteractivePermissions,
7217
- authCredentials: options.authCredentials,
7218
- authPolicy: options.authPolicy,
7219
- fs: options.fs,
7220
- terminal: options.terminal,
7221
- timeoutMs,
7222
- verbose: options.verbose
7223
- });
7224
- },
7225
- setSessionModelFallback: async (modelId, timeoutMs) => {
7226
- return (await runSessionSetModelDirect({
7227
- sessionRecordId: options.sessionId,
7228
- modelId,
7229
- mcpServers: options.mcpServers,
7230
- nonInteractivePermissions: options.nonInteractivePermissions,
7231
- authCredentials: options.authCredentials,
7232
- authPolicy: options.authPolicy,
7233
- fs: options.fs,
7234
- terminal: options.terminal,
7235
- timeoutMs,
7236
- verbose: options.verbose
7237
- })).response;
7238
- },
7239
- setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
7240
- return (await runSessionSetConfigOptionDirect({
7241
- sessionRecordId: options.sessionId,
7242
- configId,
7243
- value,
7244
- mcpServers: options.mcpServers,
7245
- nonInteractivePermissions: options.nonInteractivePermissions,
7246
- authCredentials: options.authCredentials,
7247
- authPolicy: options.authPolicy,
7248
- fs: options.fs,
7249
- terminal: options.terminal,
7250
- timeoutMs,
7251
- verbose: options.verbose
7252
- })).response;
7253
- }
7254
- });
7255
- }
7256
7733
  function logDeferredCancelFailure(error, verbose) {
7257
7734
  if (!verbose) return;
7258
7735
  process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
@@ -7296,16 +7773,17 @@ function createQueueOwnerShutdownController(params) {
7296
7773
  let shutdownPromise;
7297
7774
  const drainActiveTurn = async () => {
7298
7775
  const turn = activeTurn;
7299
- if (!turn) return;
7300
- params.turnController.requestCancel().catch((error) => {
7776
+ const pending = Promise.allSettled([turn, params.controls.drainAdmitted()]);
7777
+ if (turn) params.turnController.requestCancel().catch((error) => {
7301
7778
  logDeferredCancelFailure(error, params.verbose);
7302
7779
  });
7303
- if (!await settlesWithin(turn, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
7304
- await turn.catch(() => {});
7780
+ if (!await settlesWithin(pending, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
7781
+ await pending;
7305
7782
  };
7306
7783
  const request = () => {
7307
7784
  requested = true;
7308
7785
  params.stopHeartbeat();
7786
+ params.controls.stopAdmission();
7309
7787
  params.getOwner()?.beginShutdown();
7310
7788
  activeTurnShutdown ??= drainActiveTurn();
7311
7789
  };
@@ -7316,14 +7794,20 @@ function createQueueOwnerShutdownController(params) {
7316
7794
  request,
7317
7795
  setActiveTurn: (turn) => {
7318
7796
  activeTurn = turn;
7797
+ if (requested) activeTurnShutdown = Promise.all([activeTurnShutdown, drainActiveTurn()]).then(() => {});
7319
7798
  },
7320
7799
  clearActiveTurn: (turn) => {
7321
7800
  if (activeTurn === turn) activeTurn = void 0;
7322
7801
  },
7802
+ drain: () => {
7803
+ request();
7804
+ return activeTurnShutdown;
7805
+ },
7323
7806
  shutdown: () => {
7324
7807
  request();
7325
7808
  shutdownPromise ??= (async () => {
7326
7809
  await activeTurnShutdown;
7810
+ await params.getBackendClose()?.catch(() => {});
7327
7811
  await closeQueueOwnerRuntime({
7328
7812
  owner: params.getOwner(),
7329
7813
  turnController: params.turnController,
@@ -7342,20 +7826,50 @@ function applyRequestedShutdown(owner, shutdown) {
7342
7826
  function startQueueOwnerHeartbeat(params) {
7343
7827
  if (!params.enabled) return;
7344
7828
  return setInterval(() => {
7345
- refreshQueueOwnerLease(params.lease, { queueDepth: params.owner.queueDepth() }).catch(() => {});
7829
+ params.refresh(params.owner.queueDepth());
7346
7830
  }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
7347
7831
  }
7832
+ function createLeaseHeartbeat(lease, shutdown) {
7833
+ let guardFailure;
7834
+ return {
7835
+ refresh: async (queueDepth) => {
7836
+ if (shutdown.requested) return;
7837
+ await refreshQueueOwnerLease(lease, { queueDepth }).catch((error) => {
7838
+ if (error instanceof QueueLeaseGuardSettlementError) {
7839
+ guardFailure ??= error;
7840
+ shutdown.request();
7841
+ }
7842
+ });
7843
+ },
7844
+ throwIfFailed: () => {
7845
+ if (guardFailure) throw guardFailure;
7846
+ }
7847
+ };
7848
+ }
7348
7849
  async function runSessionQueueOwner(options) {
7349
7850
  const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
7350
7851
  path: options.mcpConfigPath,
7351
7852
  fingerprint: options.mcpConfigFingerprint
7352
7853
  });
7353
7854
  if (!lease) return;
7855
+ let runtimeFailure;
7354
7856
  try {
7355
7857
  await runQueueOwnerRuntime(options, lease);
7356
- } finally {
7858
+ } catch (error) {
7859
+ runtimeFailure = { error };
7860
+ }
7861
+ try {
7357
7862
  await releaseQueueOwnerLease(lease);
7863
+ } catch (error) {
7864
+ if (runtimeFailure) throw new AggregateError([runtimeFailure.error, error], "Queue owner shutdown failed", { cause: error });
7865
+ throw error;
7358
7866
  }
7867
+ if (runtimeFailure) throw runtimeFailure.error;
7868
+ }
7869
+ async function keepOwnerForPendingControls(controls, shutdown) {
7870
+ if (shutdown.requested || !controls.hasPending) return false;
7871
+ await controls.drainAdmitted();
7872
+ return !shutdown.requested;
7359
7873
  }
7360
7874
  async function runQueueOwnerRuntime(options, lease) {
7361
7875
  const sessionRecord = await resolveSessionRecord(options.sessionId);
@@ -7366,7 +7880,16 @@ async function runQueueOwnerRuntime(options, lease) {
7366
7880
  const maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth ?? 16));
7367
7881
  const taskPollTimeoutMs = ttlMs === 0 ? void 0 : ttlMs;
7368
7882
  const initialTaskPollTimeoutMs = taskPollTimeoutMs == null ? void 0 : Math.max(taskPollTimeoutMs, 1e3);
7369
- const turnController = createQueueOwnerTurnController(options);
7883
+ const turnController = new QueueOwnerTurnController();
7884
+ let activeControls;
7885
+ let backendCloseRequest;
7886
+ const controls = new QueueOwnerControlAdmission({ runIdle: async (invoke, controlOptions) => await runIdleOwnerControl({
7887
+ ...controlOptions,
7888
+ client: sharedClient,
7889
+ sessionId: options.sessionId,
7890
+ verbose: options.verbose,
7891
+ onRetirementFailure: () => shutdown.request()
7892
+ }, invoke) });
7370
7893
  const applyPendingCancel = async () => {
7371
7894
  return await turnController.applyPendingCancel();
7372
7895
  };
@@ -7376,23 +7899,34 @@ async function runQueueOwnerRuntime(options, lease) {
7376
7899
  });
7377
7900
  };
7378
7901
  const setActiveController = (controller) => {
7902
+ activeControls = controller;
7379
7903
  turnController.setActiveController(controller);
7380
7904
  scheduleApplyPendingCancel();
7381
7905
  };
7382
7906
  const clearActiveController = () => {
7907
+ activeControls = void 0;
7383
7908
  turnController.clearActiveController();
7384
7909
  };
7385
- const closeActiveBackendSession = async (timeoutMs) => {
7386
- const latestRecord = await resolveSessionRecord(options.sessionId);
7387
- if (!sharedClient.supportsCloseSession()) return false;
7388
- await withTimeout(sharedClient.closeSession(latestRecord.acpSessionId), timeoutMs);
7389
- return true;
7910
+ const closeActiveBackendSession = (timeoutMs) => {
7911
+ backendCloseRequest ??= Promise.resolve().then(async () => {
7912
+ await shutdown.drain();
7913
+ const latestRecord = await resolveSessionRecord(options.sessionId);
7914
+ if (!sharedClient.supportsCloseSession()) return false;
7915
+ await withTimeout(sharedClient.closeSession(latestRecord.acpSessionId), timeoutMs);
7916
+ return true;
7917
+ });
7918
+ return backendCloseRequest;
7390
7919
  };
7391
7920
  const runPromptTurn = async (run) => {
7392
7921
  const waitSignal = turnController.beginTurn();
7922
+ const { ticket, priorIdle } = controls.beginPrompt();
7923
+ if (shutdown.requested) turnController.requestCancel().catch(() => {});
7393
7924
  try {
7394
- return await run(waitSignal);
7925
+ await priorIdle;
7926
+ return await run(waitSignal, ticket);
7395
7927
  } finally {
7928
+ await controls.seal(ticket);
7929
+ controls.release(ticket);
7396
7930
  turnController.endTurn();
7397
7931
  }
7398
7932
  };
@@ -7405,6 +7939,8 @@ async function runQueueOwnerRuntime(options, lease) {
7405
7939
  }
7406
7940
  },
7407
7941
  turnController,
7942
+ controls,
7943
+ getBackendClose: () => backendCloseRequest,
7408
7944
  sharedClient,
7409
7945
  sessionId: options.sessionId,
7410
7946
  verbose: options.verbose
@@ -7412,6 +7948,7 @@ async function runQueueOwnerRuntime(options, lease) {
7412
7948
  const onSignal = () => {
7413
7949
  shutdown.request();
7414
7950
  };
7951
+ const heartbeat = createLeaseHeartbeat(lease, shutdown);
7415
7952
  process.once("SIGINT", onSignal);
7416
7953
  process.once("SIGTERM", onSignal);
7417
7954
  process.once("SIGHUP", onSignal);
@@ -7424,17 +7961,17 @@ async function runQueueOwnerRuntime(options, lease) {
7424
7961
  },
7425
7962
  closeSession: async (timeoutMs) => await closeActiveBackendSession(timeoutMs),
7426
7963
  setSessionMode: async (modeId, timeoutMs) => {
7427
- await turnController.setSessionMode(modeId, timeoutMs);
7964
+ await controls.run((context, deadline) => context.setSessionMode(modeId, deadline), timeoutMs);
7428
7965
  },
7429
- setSessionModel: async (modelId, timeoutMs) => await turnController.setSessionModel(modelId, timeoutMs),
7966
+ setSessionModel: async (modelId, timeoutMs) => await controls.run((context, deadline) => context.setSessionModel(modelId, deadline), timeoutMs, { key: "model" }),
7430
7967
  setSessionConfigOption: async (configId, value, timeoutMs) => {
7431
- return await turnController.setSessionConfigOption(configId, value, timeoutMs);
7968
+ return await controls.run((context, deadline) => context.setSessionConfigOption(configId, value, deadline), timeoutMs, { key: configId });
7432
7969
  }
7433
7970
  }, {
7434
7971
  maxQueueDepth,
7435
7972
  onQueueDepthChanged: (queueDepth) => {
7436
7973
  setPerfGauge("queue.owner.depth", queueDepth);
7437
- refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
7974
+ heartbeat.refresh(queueDepth);
7438
7975
  }
7439
7976
  });
7440
7977
  applyRequestedShutdown(owner, shutdown);
@@ -7444,19 +7981,22 @@ async function runQueueOwnerRuntime(options, lease) {
7444
7981
  maxQueueDepth,
7445
7982
  verbose: options.verbose
7446
7983
  });
7447
- await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
7984
+ await heartbeat.refresh(owner.queueDepth());
7448
7985
  heartbeatTimer = startQueueOwnerHeartbeat({
7449
7986
  enabled: !shutdown.requested,
7450
- lease,
7987
+ refresh: heartbeat.refresh,
7451
7988
  owner
7452
7989
  });
7453
7990
  let isFirstTask = true;
7454
7991
  while (true) {
7455
7992
  const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
7456
7993
  const task = await owner.nextTask(pollTimeoutMs);
7457
- if (!task) break;
7994
+ if (!task) {
7995
+ if (await keepOwnerForPendingControls(controls, shutdown)) continue;
7996
+ break;
7997
+ }
7458
7998
  isFirstTask = false;
7459
- const turnPromise = runPromptTurn(async (waitSignal) => {
7999
+ const turnPromise = runPromptTurn(async (waitSignal, ticket) => {
7460
8000
  try {
7461
8001
  await runQueuedTask(options.sessionId, task, {
7462
8002
  sharedClient,
@@ -7470,7 +8010,10 @@ async function runQueueOwnerRuntime(options, lease) {
7470
8010
  sessionOptions: options.sessionOptions,
7471
8011
  onClientAvailable: setActiveController,
7472
8012
  onClientClosed: clearActiveController,
8013
+ onClientCloseFailure: shutdown.request,
8014
+ onPromptFinalizing: () => controls.seal(ticket),
7473
8015
  onPromptActive: async () => {
8016
+ if (activeControls) controls.publish(ticket, activeControls);
7474
8017
  turnController.markPromptActive();
7475
8018
  await applyPendingCancel();
7476
8019
  },
@@ -7494,7 +8037,9 @@ async function runQueueOwnerRuntime(options, lease) {
7494
8037
  process.off("SIGTERM", onSignal);
7495
8038
  process.off("SIGHUP", onSignal);
7496
8039
  await shutdown.shutdown();
8040
+ await lease.updates;
7497
8041
  }
8042
+ heartbeat.throwIfFailed();
7498
8043
  }
7499
8044
  async function sendSession(options) {
7500
8045
  options.signal?.throwIfAborted();
@@ -7534,6 +8079,6 @@ async function sendSession(options) {
7534
8079
  }
7535
8080
  const queueOwnerRuntimeTestInternals = { queueOwnerExitIsFatal };
7536
8081
  //#endregion
7537
- 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 };
8082
+ 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 };
7538
8083
 
7539
- //# sourceMappingURL=queue-owner-runtime--IbbndGz.js.map
8084
+ //# sourceMappingURL=queue-owner-runtime-Cr0dWJuL.js.map