acpx 0.17.1 → 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 (36) hide show
  1. package/dist/{agent-registry-evUoJINz.js → agent-registry-DdKB-zia.js} +16 -17
  2. package/dist/{agent-registry-evUoJINz.js.map → agent-registry-DdKB-zia.js.map} +1 -1
  3. package/dist/agent-registry.d.ts.map +1 -1
  4. package/dist/agent-registry.js +1 -1
  5. package/dist/{cli-B2ycKzaO.js → cli-8mVHeUXh.js} +5 -5
  6. package/dist/{cli-B2ycKzaO.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-CkbBoSGd.d.ts → client-DeNKZZcu.d.ts} +14 -12
  10. package/dist/client-DeNKZZcu.d.ts.map +1 -0
  11. package/dist/{flows-xGyhmoHO.js → flows-WOQNzCMC.js} +5 -5
  12. package/dist/{flows-xGyhmoHO.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-eZEBh4vd.js → invocation-options-B6d7TI-y.js} +7 -6
  16. package/dist/{invocation-options-eZEBh4vd.js.map → invocation-options-B6d7TI-y.js.map} +1 -1
  17. package/dist/{ipc-D2rstexT.js → ipc-BJrvfNXr.js} +305 -88
  18. package/dist/ipc-BJrvfNXr.js.map +1 -0
  19. package/dist/{output-D2xL6wdp.js → output-CL18YmsG.js} +9 -6
  20. package/dist/output-CL18YmsG.js.map +1 -0
  21. package/dist/{queue-owner-runtime-Dgv3e2Tp.js → queue-owner-runtime-Cr0dWJuL.js} +952 -433
  22. package/dist/queue-owner-runtime-Cr0dWJuL.js.map +1 -0
  23. package/dist/runtime.d.ts +39 -3
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +272 -154
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/{session-watch-CTe9Bnbc.js → session-watch-B8Q_TsGf.js} +4 -4
  28. package/dist/{session-watch-CTe9Bnbc.js.map → session-watch-B8Q_TsGf.js.map} +1 -1
  29. package/dist/{watch-DxuXEc8n.js → watch-Dp73ph8h.js} +2 -2
  30. package/dist/{watch-DxuXEc8n.js.map → watch-Dp73ph8h.js.map} +1 -1
  31. package/package.json +1 -1
  32. package/skills/acpx/SKILL.md +26 -7
  33. package/dist/client-CkbBoSGd.d.ts.map +0 -1
  34. package/dist/ipc-D2rstexT.js.map +0 -1
  35. package/dist/output-D2xL6wdp.js.map +0 -1
  36. package/dist/queue-owner-runtime-Dgv3e2Tp.js.map +0 -1
@@ -1,5 +1,5 @@
1
- import { $ as startPerfTimer, At as AgentSpawnError, B as extractAgentSessionId, Bt as SessionModeReplayError, C as textPrompt, Ct as OUTPUT_ERROR_ORIGINS, E as findGitRepositoryRoot, F as resolveSessionRecord, Ft as GeminiAcpStartupTimeoutError, H as SESSION_JOURNAL_SCHEMA, I as writeSessionRecord, It as PermissionDeniedError, J as incrementPerfCounter, K as formatPerfMetric, Lt as PermissionPromptUnavailableError, M as normalizeName, Mt as AuthPolicyError, Nt as ClaudeAcpSessionCreateTimeoutError, O as findSessionByDirectoryWalk, Ot as AcpxOperationalError, Pt as CopilotAcpUnsupportedError, Q as setPerfGauge, Rt as QueueConnectionError, S as promptToDisplayText, St as OUTPUT_ERROR_CODES, T as absolutePath, U as SessionJournalReader, Ut as SessionResumeRequiredError, V as normalizeAgentSessionId, Vt as SessionModelReplayError, Wt as UnsupportedPromptContentError, X as recordPerfDuration, Y as measurePerf, Z as resetPerfMetrics, _ as waitMs$1, _t as formatUnknownErrorMessage, a as trySetModelOnRunningOwner, bt as EXIT_CODES, c as queueRequestExceedsLimit, ct as defaultSessionEventLog, dt as sessionEventLockPath, f as refreshQueueOwnerLease, ft as sessionEventSegmentPath, g as tryAcquireQueueOwnerLease, gt as extractAcpError, h as terminateQueueOwnerForSession, ht as isProcessAlive, i as trySetModeOnRunningOwner, jt as AgentStartupError, k as isoNow$1, kt as AgentDisconnectedError, l as parseQueueRequest, lt as sessionBaseDir, m as terminateProcess, n as tryCloseSessionOnRunningOwner, nt as isAcpMessageObject, o as trySubmitToRunningOwner, p as releaseQueueOwnerLease, q as getPerfMetricsSnapshot, r as trySetConfigOptionOnRunningOwner, rt as isSessionUpdateNotification, s as queueRequestByteLimit, t as tryCancelOnRunningOwner, tt as isAcpJsonRpcMessage, ut as sessionEventActivePath, vt as isAcpResourceNotFoundError, y as getUnsupportedPromptContentMessage, zt as SessionConfigOptionReplayError } from "./ipc-D2rstexT.js";
2
- import { A as resolveAgentCommandParts, C as asAbsoluteCwd, E as isoNow$2, F as waitForSpawn, M as runTimedExecFile, N as splitCommandLine, P as waitForChildExit, S as PROCESS_HELPER_TIMEOUT_MS, T as isChildProcessRunning, _ as buildSpawnCommandOptions, b as readWindowsEnvValue, f as resolveBuiltInAgentLaunch, g as buildAgentSpawnCommand, j as resolveAgentSessionCwd, k as requireAgentStdio, v as buildTerminalShellSpawnCommand, w as basenameToken, x as resolveWindowsExecutablePath, y as buildTerminalSpawnCommand } from "./agent-registry-evUoJINz.js";
1
+ import { $ as 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) {
@@ -1757,8 +1820,11 @@ function trimToUtf8Boundary(buffer, limit) {
1757
1820
  if (start >= buffer.length) start = buffer.length - limit;
1758
1821
  return buffer.subarray(start);
1759
1822
  }
1760
- async function defaultConfirmExecute(commandLine) {
1761
- 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
+ });
1762
1828
  }
1763
1829
  function canPromptForPermission() {
1764
1830
  return process.stdin.isTTY && process.stderr.isTTY;
@@ -1795,7 +1861,8 @@ var TerminalManager = class {
1795
1861
  this.permissionMode = permissionMode;
1796
1862
  this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
1797
1863
  }
1798
- async createTerminal(params) {
1864
+ async createTerminal(params, authority) {
1865
+ assertControlAuthority(authority);
1799
1866
  const commandLine = toCommandLine(params.command, params.args);
1800
1867
  const summary = `terminal/create: ${commandLine}`;
1801
1868
  this.emitOperation({
@@ -1805,10 +1872,12 @@ var TerminalManager = class {
1805
1872
  timestamp: nowIso()
1806
1873
  });
1807
1874
  try {
1808
- 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");
1809
1878
  const requestedLimit = Math.max(0, Math.round(params.outputByteLimit ?? DEFAULT_TERMINAL_OUTPUT_LIMIT_BYTES));
1810
1879
  const outputByteLimit = Math.min(requestedLimit, this.outputCeilingBytes ?? Number.POSITIVE_INFINITY);
1811
- const { proc, spawnCommand } = await spawnTerminalProcess(params, this.cwd);
1880
+ const { proc, spawnCommand } = await spawnTerminalProcess(params, this.cwd, authority);
1812
1881
  let resolveExit = () => {};
1813
1882
  const exitPromise = new Promise((resolve) => {
1814
1883
  resolveExit = resolve;
@@ -1853,6 +1922,15 @@ var TerminalManager = class {
1853
1922
  });
1854
1923
  const terminalId = randomUUID();
1855
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
+ }
1856
1934
  this.emitOperation({
1857
1935
  method: "terminal/create",
1858
1936
  status: "completed",
@@ -1991,11 +2069,11 @@ var TerminalManager = class {
1991
2069
  emitOperation(operation) {
1992
2070
  this.onOperation?.(operation);
1993
2071
  }
1994
- async isExecuteApproved(commandLine) {
2072
+ async isExecuteApproved(commandLine, signal) {
1995
2073
  if (this.permissionMode === "approve-all") return true;
1996
2074
  if (this.permissionMode === "deny-all") return false;
1997
2075
  if (this.usesDefaultConfirmExecute && this.nonInteractivePermissions === "fail" && !canPromptForPermission()) throw new PermissionPromptUnavailableError();
1998
- return await this.confirmExecute(commandLine);
2076
+ return await this.confirmExecute(commandLine, signal);
1999
2077
  }
2000
2078
  isRunning(terminal) {
2001
2079
  return terminal.exitCode === void 0 && terminal.signal === void 0;
@@ -2059,25 +2137,26 @@ var TerminalManager = class {
2059
2137
  while (hasLivePid(terminal.descendantPids)) await waitMs(25);
2060
2138
  }
2061
2139
  };
2062
- async function spawnTerminalProcess(params, defaultCwd) {
2140
+ async function spawnTerminalProcess(params, defaultCwd, authority) {
2063
2141
  const directCommand = buildTerminalSpawnCommand(params.command, params.args);
2064
2142
  try {
2065
2143
  return {
2066
- proc: await spawnAndWait(directCommand, params, defaultCwd),
2144
+ proc: await spawnAndWait(directCommand, params, defaultCwd, authority),
2067
2145
  spawnCommand: directCommand
2068
2146
  };
2069
2147
  } catch (error) {
2070
2148
  const fallbackCommand = params.args === void 0 && isNotFoundSpawnError(error) ? buildTerminalFallbackSpawnCommand(params.command, params.cwd ?? defaultCwd) : void 0;
2071
2149
  if (!fallbackCommand) throw error;
2072
2150
  return {
2073
- proc: await spawnAndWait(fallbackCommand, params, defaultCwd),
2151
+ proc: await spawnAndWait(fallbackCommand, params, defaultCwd, authority),
2074
2152
  spawnCommand: fallbackCommand
2075
2153
  };
2076
2154
  }
2077
2155
  }
2078
- async function spawnAndWait(spawnCommand, params, defaultCwd) {
2156
+ async function spawnAndWait(spawnCommand, params, defaultCwd, authority) {
2079
2157
  const spawnOptions = buildTerminalSpawnOptions(spawnCommand.command, params.cwd ?? defaultCwd, params.env);
2080
2158
  if (spawnCommand.killProcessGroup) spawnOptions.detached = true;
2159
+ assertControlAuthority(authority);
2081
2160
  const proc = spawn(spawnCommand.command, spawnCommand.args, spawnOptions);
2082
2161
  await waitForSpawn(proc);
2083
2162
  return proc;
@@ -2368,8 +2447,6 @@ var AcpClient = class {
2368
2447
  lastKnownPid;
2369
2448
  promptPermissionFailures = /* @__PURE__ */ new Map();
2370
2449
  pendingConnectionRequests = /* @__PURE__ */ new Set();
2371
- modelConfigIds = /* @__PURE__ */ new Map();
2372
- legacyModelSessionIds = /* @__PURE__ */ new Set();
2373
2450
  constructor(options) {
2374
2451
  this.options = {
2375
2452
  ...options,
@@ -2473,11 +2550,15 @@ var AcpClient = class {
2473
2550
  hasUnresolvedPrompt() {
2474
2551
  return this.activePrompt !== void 0;
2475
2552
  }
2476
- endPromptElicitation(sessionId) {
2477
- const active = this.activePrompt;
2478
- if (!active || active.sessionId !== sessionId) return;
2479
- active.elicitationHandler = void 0;
2480
- 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();
2481
2562
  }
2482
2563
  async start() {
2483
2564
  if (this.hasLiveConnection()) return;
@@ -2634,19 +2715,19 @@ var AcpClient = class {
2634
2715
  createConnection(stream, launch, capabilities) {
2635
2716
  const app = client({ name: "acpx" }).onNotification(methods.client.session.update, async ({ params }) => {
2636
2717
  await this.handleSessionUpdate(params);
2637
- }).onNotification(methods.client.elicitation.complete, async () => {}).onRequest(methods.client.session.requestPermission, async ({ params }) => {
2638
- 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);
2639
2720
  }).onRequest(methods.client.elicitation.create, async ({ params, requestId, signal }) => {
2640
2721
  return await this.handleElicitationRequest(params, requestId, signal);
2641
2722
  });
2642
- if (capabilities.fs?.readTextFile) app.onRequest(methods.client.fs.readTextFile, async ({ params }) => {
2643
- return await this.handleReadTextFile(params);
2723
+ if (capabilities.fs?.readTextFile) app.onRequest(methods.client.fs.readTextFile, async ({ params, signal }) => {
2724
+ return await this.handleReadTextFile(params, signal);
2644
2725
  });
2645
- if (capabilities.fs?.writeTextFile) app.onRequest(methods.client.fs.writeTextFile, async ({ params }) => {
2646
- return await this.handleWriteTextFile(params);
2726
+ if (capabilities.fs?.writeTextFile) app.onRequest(methods.client.fs.writeTextFile, async ({ params, signal }) => {
2727
+ return await this.handleWriteTextFile(params, signal);
2647
2728
  });
2648
- if (capabilities.terminal) app.onRequest(methods.client.terminal.create, async ({ params }) => {
2649
- return await this.handleCreateTerminal(params);
2729
+ if (capabilities.terminal) app.onRequest(methods.client.terminal.create, async ({ params, signal }) => {
2730
+ return await this.handleCreateTerminal(params, signal);
2650
2731
  }).onRequest(methods.client.terminal.output, async ({ params }) => {
2651
2732
  return await this.terminalManager.terminalOutput(params);
2652
2733
  }).onRequest(methods.client.terminal.waitForExit, async ({ params }) => {
@@ -2733,7 +2814,6 @@ var AcpClient = class {
2733
2814
  configOptions,
2734
2815
  response: result
2735
2816
  });
2736
- this.rememberSessionModels(result.sessionId, models);
2737
2817
  await this.captureAgentDescendants(this.agent);
2738
2818
  return {
2739
2819
  sessionId: result.sessionId,
@@ -2765,7 +2845,6 @@ var AcpClient = class {
2765
2845
  }
2766
2846
  this.loadedSessionId = sessionId;
2767
2847
  const result = toReconnectedSessionResult(response);
2768
- this.updateRememberedSessionModels(sessionId, result);
2769
2848
  await this.captureAgentDescendants(this.agent);
2770
2849
  return result;
2771
2850
  }
@@ -2779,7 +2858,6 @@ var AcpClient = class {
2779
2858
  }));
2780
2859
  this.loadedSessionId = sessionId;
2781
2860
  const result = toReconnectedSessionResult(response);
2782
- this.updateRememberedSessionModels(sessionId, result);
2783
2861
  await this.captureAgentDescendants(this.agent);
2784
2862
  return result;
2785
2863
  }
@@ -2801,6 +2879,7 @@ var AcpClient = class {
2801
2879
  const normalizedPrompt = this.normalizePromptForAgent(prompt);
2802
2880
  const restoreConsoleError = this.options.suppressSdkConsoleErrors ? installSdkConsoleErrorSuppression() : void 0;
2803
2881
  const previousActivePrompt = this.activePrompt;
2882
+ const replacedOwners = this.pendingPromptOwners.filter((owner) => owner.sessionId === sessionId);
2804
2883
  const activePrompt = this.beginActivePrompt(sessionId, onRequestWritten, onElicitation, onPermissionRequest, authority);
2805
2884
  try {
2806
2885
  const promptPromise = this.runConnectionRequest(() => connection.agent.request(methods.agent.session.prompt, {
@@ -2809,6 +2888,7 @@ var AcpClient = class {
2809
2888
  }), authority);
2810
2889
  activePrompt.promise = promptPromise;
2811
2890
  previousActivePrompt?.elicitationController?.abort();
2891
+ for (const owner of replacedOwners) owner.requestController.abort();
2812
2892
  return this.returnPromptResponseOrPermissionFailure(sessionId, await promptPromise);
2813
2893
  } catch (error) {
2814
2894
  if (activePrompt.admissionFailure) throw activePrompt.admissionFailure.error;
@@ -2827,7 +2907,8 @@ var AcpClient = class {
2827
2907
  authority,
2828
2908
  elicitationHandler,
2829
2909
  permissionHandler,
2830
- elicitationController: new AbortController()
2910
+ elicitationController: new AbortController(),
2911
+ requestController: new AbortController()
2831
2912
  };
2832
2913
  this.activePrompt = active;
2833
2914
  this.pendingPromptOwners.push(active);
@@ -2866,6 +2947,7 @@ var AcpClient = class {
2866
2947
  this.promptPermissionFailures.delete(active.sessionId);
2867
2948
  }
2868
2949
  active.elicitationController.abort();
2950
+ active.requestController.abort();
2869
2951
  permissionController?.abort();
2870
2952
  }
2871
2953
  normalizePromptForAgent(prompt) {
@@ -2889,33 +2971,41 @@ var AcpClient = class {
2889
2971
  modeId
2890
2972
  }), authority, (error) => maybeWrapSessionControlError("session/set_mode", error, `for mode "${modeId}"`));
2891
2973
  }
2892
- 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;
2893
2977
  const connection = this.getConnection();
2894
2978
  return await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
2895
2979
  sessionId,
2896
2980
  configId,
2897
- value
2981
+ value: resolvedValue
2898
2982
  }), authority, (error) => maybeWrapSessionControlError("session/set_config_option", error, `for "${configId}"="${value}"`));
2899
2983
  }
2900
- async setSessionModel(sessionId, modelId, controlOverride, authority) {
2901
- const control = this.resolveModelControl(sessionId, controlOverride);
2902
- if (!control) throw new RequestedModelUnsupportedError(`Cannot set model "${modelId}": the ACP session did not advertise a model config option or legacy session/set_model support.`, "missing-capability");
2903
- const resolvedModelId = resolveRequestedModelId({
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 = {
2904
2992
  requestedModel: modelId,
2905
- models: controlOverride?.availableModels ? { availableModels: controlOverride.availableModels } : void 0,
2993
+ models,
2906
2994
  agentCommand: this.options.agentCommand
2995
+ };
2996
+ assertRequestedModelSupported({
2997
+ ...params,
2998
+ context: "apply"
2907
2999
  });
2908
- return control.kind === "config_option" ? await this.setSessionModelThroughConfig(sessionId, resolvedModelId, control.configId, authority) : await this.setSessionModelThroughLegacyMethod(sessionId, resolvedModelId, authority);
3000
+ return resolveRequestedModelId(params);
2909
3001
  }
2910
3002
  async setSessionModelThroughConfig(sessionId, modelId, configId, authority) {
2911
3003
  const connection = this.getConnection();
2912
- const response = await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
3004
+ return await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.setConfigOption, {
2913
3005
  sessionId,
2914
3006
  configId,
2915
3007
  value: modelId
2916
3008
  }), authority, (error) => this.throwSessionModelError("session/set_config_option", modelId, error));
2917
- this.rememberSessionModels(sessionId, modelStateFromConfigOptions(response.configOptions));
2918
- return response;
2919
3009
  }
2920
3010
  async setSessionModelThroughLegacyMethod(sessionId, modelId, authority) {
2921
3011
  const connection = this.getConnection();
@@ -2931,39 +3021,10 @@ var AcpClient = class {
2931
3021
  const summary = acp ? formatSessionControlAcpSummary(acp) : error instanceof Error ? error.message : String(error);
2932
3022
  throw new Error(`Failed ${method} for model "${modelId}": ${summary}`, { cause: error });
2933
3023
  }
2934
- resolveModelControl(sessionId, controlOverride) {
2935
- if (controlOverride) return controlOverride.configId ? {
2936
- kind: "config_option",
2937
- configId: controlOverride.configId
2938
- } : { kind: "legacy_set_model" };
2939
- const configId = this.modelConfigIds.get(sessionId);
2940
- if (configId) return {
2941
- kind: "config_option",
2942
- configId
2943
- };
2944
- return this.legacyModelSessionIds.has(sessionId) ? { kind: "legacy_set_model" } : void 0;
2945
- }
2946
- rememberSessionModels(sessionId, models) {
2947
- if (!models) {
2948
- this.modelConfigIds.delete(sessionId);
2949
- this.legacyModelSessionIds.delete(sessionId);
2950
- return;
2951
- }
2952
- if (models.configId) {
2953
- this.modelConfigIds.set(sessionId, models.configId);
2954
- this.legacyModelSessionIds.delete(sessionId);
2955
- return;
2956
- }
2957
- this.modelConfigIds.delete(sessionId);
2958
- this.legacyModelSessionIds.add(sessionId);
2959
- }
2960
- updateRememberedSessionModels(sessionId, result) {
2961
- const explicitConfigRemoval = result.configOptionsPresent && this.modelConfigIds.has(sessionId);
2962
- if (result.models || result.legacyModelMetadataPresent || explicitConfigRemoval) this.rememberSessionModels(sessionId, result.models);
2963
- }
2964
3024
  async cancel(sessionId) {
2965
3025
  const connection = this.getConnection();
2966
- 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);
2967
3028
  const cancellation = active?.cancelPromise ?? this.runConnectionRequest(() => connection.agent.notify(methods.agent.session.cancel, { sessionId })).catch((error) => {
2968
3029
  if (active?.cancelPromise === cancellation) active.cancelPromise = void 0;
2969
3030
  throw error;
@@ -2972,19 +3033,20 @@ var AcpClient = class {
2972
3033
  if (active) {
2973
3034
  active.cancelPromise = cancellation;
2974
3035
  this.cancellingSessionIds.add(sessionId);
2975
- active.elicitationController?.abort();
2976
3036
  } else this.cancellingSessionIds.delete(sessionId);
3037
+ for (const owner of owners) {
3038
+ owner.elicitationController.abort();
3039
+ owner.requestController.abort();
3040
+ }
2977
3041
  permissionController?.abort();
2978
3042
  await cancellation;
2979
3043
  }
2980
3044
  async closeSession(sessionId) {
2981
3045
  const connection = this.getConnection();
2982
3046
  this.cancellingSessionIds.add(sessionId);
2983
- if (this.activePrompt?.sessionId === sessionId) this.activePrompt.elicitationController?.abort();
3047
+ this.abortSessionRequests(sessionId);
2984
3048
  await this.runConnectionRequest(() => connection.agent.request(methods.agent.session.close, { sessionId }));
2985
3049
  if (this.loadedSessionId === sessionId) this.loadedSessionId = void 0;
2986
- this.modelConfigIds.delete(sessionId);
2987
- this.legacyModelSessionIds.delete(sessionId);
2988
3050
  }
2989
3051
  async listSessions(params = {}) {
2990
3052
  const connection = this.getConnection();
@@ -3021,7 +3083,12 @@ var AcpClient = class {
3021
3083
  async close() {
3022
3084
  this.closing = true;
3023
3085
  this.closeEpoch += 1;
3086
+ const permissionControllers = [...this.permissionAbortControllers.values()];
3087
+ this.permissionAbortControllers.clear();
3088
+ const owners = [...this.pendingPromptOwners];
3024
3089
  this.abortActiveElicitation();
3090
+ for (const owner of owners) owner.requestController.abort();
3091
+ for (const controller of permissionControllers) controller.abort();
3025
3092
  await this.terminalManager.shutdown();
3026
3093
  const agent = this.agent;
3027
3094
  if (agent) await this.terminateAgentProcess(agent);
@@ -3039,8 +3106,6 @@ var AcpClient = class {
3039
3106
  this.permissionAbortControllers.clear();
3040
3107
  this.promptPermissionFailures.clear();
3041
3108
  this.loadedSessionId = void 0;
3042
- this.modelConfigIds.clear();
3043
- this.legacyModelSessionIds.clear();
3044
3109
  this.initResult = void 0;
3045
3110
  this.connection = void 0;
3046
3111
  this.agent = void 0;
@@ -3255,27 +3320,41 @@ var AcpClient = class {
3255
3320
  await connection.agent.request(methods.agent.authenticate, { methodId: selected.methodId });
3256
3321
  this.log(`authenticated with method ${selected.methodId} (${selected.source})`);
3257
3322
  }
3258
- async handlePermissionRequest(params) {
3259
- if (this.cancellingSessionIds.has(params.sessionId)) return cancelledPermissionResponse();
3260
- if (this.initResult?.agentInfo?.name === "antigravity-acp" && params.toolCall.toolCallId.startsWith("interaction_")) return this.handleModePermissionError(params.sessionId, new PermissionPromptUnavailableError("Antigravity requested a user answer. acpx cannot answer Antigravity interaction questions; continue in an interactive client.")).response;
3261
- const hostResponse = await this.tryHandlePermissionRequestWithHost(params);
3262
- if (hostResponse) return hostResponse;
3263
- const { response, recorded } = await this.resolvePermissionRequestFromMode(params);
3264
- if (!recorded) {
3265
- const decision = classifyPermissionDecision(params, response);
3266
- this.recordPermissionDecision(decision);
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);
3267
3330
  }
3268
- 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);
3269
3341
  }
3270
3342
  async handleElicitationRequest(request, requestId, requestSignal) {
3271
3343
  const resolved = this.resolveElicitationOwner(request);
3272
3344
  if ("response" in resolved) return resolved.response;
3273
3345
  const { active, handler } = resolved.owner;
3274
- const signal = AbortSignal.any([requestSignal, active.elicitationController.signal]);
3275
- const outcome = await raceWithAbort(signal, Promise.resolve().then(async () => await handler(request, {
3276
- requestId,
3277
- signal
3278
- })).then((response) => ({
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) => ({
3279
3358
  kind: "response",
3280
3359
  response
3281
3360
  }), (error) => ({
@@ -3310,62 +3389,65 @@ var AcpClient = class {
3310
3389
  isElicitationSessionCancelling(sessionId) {
3311
3390
  return this.closing || this.cancellingSessionIds.has(sessionId);
3312
3391
  }
3313
- resolvePermissionOwner(sessionId) {
3314
- const active = this.activePrompt;
3315
- const handler = active?.permissionHandler ?? this.options.onPermissionRequest;
3316
- if (!handler) return;
3317
- const signal = this.cancellationSignalForSession(sessionId);
3318
- if (!active?.permissionHandler) return {
3319
- handler,
3320
- signal
3321
- };
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;
3322
3400
  return {
3323
- handler,
3324
- 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
+ }
3325
3406
  };
3326
3407
  }
3327
- async tryHandlePermissionRequestWithHost(params) {
3328
- const owner = this.resolvePermissionOwner(params.sessionId);
3329
- if (!owner) return;
3330
- const { handler, signal } = owner;
3331
- if (signal.aborted) return this.hostPermissionDecisionResponse(params, signal, void 0);
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();
3332
3421
  try {
3333
3422
  const decision = await raceWithAbort(signal, handler({
3334
3423
  sessionId: params.sessionId,
3335
3424
  raw: params,
3336
3425
  inferredKind: inferToolKind(params)
3337
3426
  }, { signal }));
3338
- return this.hostPermissionDecisionResponse(params, signal, decision);
3427
+ return this.hostPermissionDecisionResponse(params, owner, decision);
3339
3428
  } catch (error) {
3340
- return this.hostPermissionErrorResponse(params, signal, error);
3429
+ return this.hostPermissionErrorResponse(owner, error);
3341
3430
  }
3342
3431
  }
3343
- hostPermissionDecisionResponse(params, signal, decision) {
3344
- if (signal.aborted || this.cancellingSessionIds.has(params.sessionId)) {
3345
- this.recordPermissionDecision("cancelled");
3346
- return cancelledPermissionResponse();
3347
- }
3432
+ hostPermissionDecisionResponse(params, owner, decision) {
3433
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3348
3434
  if (!decision) return;
3349
3435
  const response = decisionToResponse(preferCodexPermissionRefusal(params, this.initResult?.agentInfo?.name), decision);
3350
- this.recordPermissionDecision(classifyPermissionDecision(params, response));
3351
3436
  return decision.outcome === "cancel" ? response : this.explainPermissionRefusal(params, response);
3352
3437
  }
3353
- hostPermissionErrorResponse(params, signal, error) {
3354
- if (signal.aborted || this.cancellingSessionIds.has(params.sessionId)) {
3355
- this.recordPermissionDecision("cancelled");
3356
- return cancelledPermissionResponse();
3357
- }
3438
+ hostPermissionErrorResponse(owner, error) {
3439
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3358
3440
  this.log(`onPermissionRequest threw, falling through to mode-based resolver: ${error instanceof Error ? error.message : String(error)}`);
3359
3441
  }
3360
- async resolvePermissionRequestFromMode(params) {
3442
+ async resolvePermissionRequestFromMode(params, owner) {
3361
3443
  try {
3362
- 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);
3363
3447
  this.emitPermissionEscalation(result.escalation);
3364
- return {
3365
- response: this.explainPermissionRefusal(params, result.response),
3366
- recorded: false
3367
- };
3448
+ return this.explainPermissionRefusal(params, result.response);
3368
3449
  } catch (error) {
3450
+ if (!this.isDelegatedRequestActive(owner)) return cancelledPermissionResponse();
3369
3451
  return this.handleModePermissionError(params.sessionId, error);
3370
3452
  }
3371
3453
  }
@@ -3388,11 +3470,7 @@ var AcpClient = class {
3388
3470
  handleModePermissionError(sessionId, error) {
3389
3471
  if (!(error instanceof PermissionPromptUnavailableError)) throw error;
3390
3472
  this.notePromptPermissionFailure(sessionId, error);
3391
- this.recordPermissionDecision("cancelled");
3392
- return {
3393
- response: cancelledPermissionResponse(),
3394
- recorded: true
3395
- };
3473
+ return cancelledPermissionResponse();
3396
3474
  }
3397
3475
  attachAgentLifecycleObservers(child, startedProcess, exitNotificationBarrier) {
3398
3476
  const onExit = (exitCode, signal) => {
@@ -3519,28 +3597,30 @@ var AcpClient = class {
3519
3597
  pending.reject(error);
3520
3598
  }
3521
3599
  }
3522
- async handleReadTextFile(params) {
3600
+ async handleReadTextFile(params, requestSignal) {
3523
3601
  try {
3524
- return await this.filesystem.readTextFile(params);
3602
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.filesystem.readTextFile(params, owner));
3525
3603
  } catch (error) {
3526
- this.recordPermissionError(params.sessionId, error);
3527
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);
3528
3605
  throw error;
3529
3606
  }
3530
3607
  }
3531
- async handleWriteTextFile(params) {
3532
- try {
3533
- return await this.filesystem.writeTextFile(params);
3534
- } catch (error) {
3535
- this.recordPermissionError(params.sessionId, error);
3536
- throw error;
3537
- }
3608
+ async handleWriteTextFile(params, requestSignal) {
3609
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.filesystem.writeTextFile(params, owner));
3538
3610
  }
3539
- async handleCreateTerminal(params) {
3611
+ async handleCreateTerminal(params, requestSignal) {
3612
+ return await this.runDelegatedOperation(params.sessionId, requestSignal, (owner) => this.terminalManager.createTerminal(params, owner));
3613
+ }
3614
+ async runDelegatedOperation(sessionId, requestSignal, run) {
3615
+ const owner = this.captureDelegatedRequestOwner(sessionId, requestSignal);
3540
3616
  try {
3541
- return await this.terminalManager.createTerminal(params);
3617
+ assertControlAuthority(owner);
3618
+ const result = await run(owner);
3619
+ assertControlAuthority(owner);
3620
+ return result;
3542
3621
  } catch (error) {
3543
- this.recordPermissionError(params.sessionId, error);
3622
+ if (!this.isDelegatedRequestActive(owner)) throw RequestError.requestCancelled();
3623
+ this.recordPermissionError(sessionId, error);
3544
3624
  throw error;
3545
3625
  }
3546
3626
  }
@@ -4037,20 +4117,6 @@ function recordPromptSubmission(conversation, prompt, timestamp = isoNow(), mess
4037
4117
  trimConversationForRuntime(conversation);
4038
4118
  return promptMessageId;
4039
4119
  }
4040
- function agentMessageHasObservedReply(message) {
4041
- return message.content.length > 0 || Object.keys(message.tool_results).length > 0;
4042
- }
4043
- function hasAgentReplyAfterPrompt(conversation, promptMessageId) {
4044
- let sawPrompt = false;
4045
- for (const message of conversation.messages) {
4046
- if (!sawPrompt) {
4047
- if (isUserMessage(message) && message.User.id === promptMessageId) sawPrompt = true;
4048
- continue;
4049
- }
4050
- if (isAgentMessage(message) && agentMessageHasObservedReply(message.Agent)) return true;
4051
- }
4052
- return false;
4053
- }
4054
4120
  function recordSessionUpdate(conversation, state, notification, timestamp = isoNow(), userMessageId) {
4055
4121
  const acpx = ensureAcpxState$1(state);
4056
4122
  const update = notification.update;
@@ -4456,14 +4522,42 @@ async function readLock(filePath) {
4456
4522
  throw error;
4457
4523
  }
4458
4524
  }
4459
- 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) {
4460
4531
  try {
4461
- const pid = JSON.parse(payload)?.pid;
4462
- return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : void 0;
4532
+ return JSON.parse(payload);
4463
4533
  } catch {
4464
4534
  return;
4465
4535
  }
4466
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
+ }
4467
4561
  async function removeObservedLock(filePath, observed) {
4468
4562
  const current = await readLock(filePath);
4469
4563
  if (!current) return true;
@@ -4475,64 +4569,105 @@ async function removeObservedLock(filePath, observed) {
4475
4569
  }
4476
4570
  async function recoverAbandonedLock(filePath) {
4477
4571
  const observed = await readLock(filePath);
4478
- if (!observed) return;
4479
- const pid = lockPid(observed.payload);
4480
- if (pid === process.pid || isProcessAlive(pid)) return;
4481
- if (!pid && Date.now() - Number(observed.stat.mtimeMs) <= INCOMPLETE_RESERVATION_GRACE_MS) return;
4482
- if (await removeObservedLock(filePath, observed)) incrementPerfCounter("session.events.stale_lock_recovered");
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;
4483
4581
  }
4484
4582
  async function publishLock(filePath, payload, signal) {
4485
- try {
4486
- await withTempFile({
4487
- rootDir: path.dirname(filePath),
4488
- prefix: "session-turn",
4489
- fileName: "lock"
4490
- }, async (temporaryPath) => {
4491
- await fs$1.writeFile(temporaryPath, payload, {
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, {
4492
4599
  encoding: "utf8",
4493
4600
  flag: "wx",
4494
4601
  mode: 384
4495
4602
  });
4496
- signal?.throwIfAborted();
4497
- try {
4498
- await fs$1.link(temporaryPath, filePath);
4499
- } catch (error) {
4500
- if (!isHardlinkFallbackError(error)) throw error;
4501
- await fs$1.writeFile(filePath, payload, {
4502
- encoding: "utf8",
4503
- flag: "wx",
4504
- mode: 384
4505
- });
4506
- }
4507
- });
4508
- } catch (error) {
4509
- await removePublishedLock(filePath, payload).catch(() => {});
4510
- throw error;
4511
- }
4603
+ }
4604
+ });
4512
4605
  }
4513
4606
  async function removePublishedLock(filePath, payload) {
4514
4607
  const current = await readLock(filePath);
4515
4608
  if (current?.payload === payload) await removeObservedLock(filePath, current);
4516
4609
  }
4517
- 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;
4518
4650
  try {
4651
+ signal?.throwIfAborted();
4652
+ if (!await tryPublishLock(filePath, payload, signal)) {
4653
+ await guard.release();
4654
+ return;
4655
+ }
4519
4656
  const observed = await readLock(filePath);
4520
4657
  if (observed?.payload !== payload) throw new Error(`Session turn ownership changed before admission: ${filePath}`);
4521
4658
  signal?.throwIfAborted();
4522
- return { [Symbol.asyncDispose]: async () => {
4523
- await removeObservedLock(filePath, observed);
4524
- } };
4659
+ return turnReceipt(filePath, observed, guard);
4525
4660
  } catch (error) {
4526
- await removePublishedLock(filePath, payload).catch(() => {});
4527
- throw error;
4661
+ return await rejectAcquisition(filePath, payload, guard, error);
4528
4662
  }
4529
4663
  }
4530
4664
  async function acquireSessionTurn(recordId, signal) {
4531
- const filePath = sessionEventLockPath(recordId);
4532
- await fs$1.mkdir(path.dirname(filePath), {
4665
+ const requestedPath = sessionEventLockPath(recordId);
4666
+ await fs$1.mkdir(path.dirname(requestedPath), {
4533
4667
  recursive: true,
4534
4668
  mode: 448
4535
4669
  });
4670
+ const filePath = path.join(await fs$1.realpath(path.dirname(requestedPath)), path.basename(requestedPath));
4536
4671
  lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);
4537
4672
  const payload = `${JSON.stringify({
4538
4673
  pid: process.pid,
@@ -4540,16 +4675,10 @@ async function acquireSessionTurn(recordId, signal) {
4540
4675
  }, null, 2)}\n`;
4541
4676
  for (;;) {
4542
4677
  signal?.throwIfAborted();
4543
- try {
4544
- await publishLock(filePath, payload, signal);
4545
- break;
4546
- } catch (error) {
4547
- if (error.code !== "EEXIST") throw error;
4548
- await recoverAbandonedLock(filePath);
4549
- await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4550
- }
4678
+ const turn = await tryAcquireTurn(filePath, payload, signal);
4679
+ if (turn) return turn;
4680
+ await setTimeout$1(LOCK_RETRY_MS, void 0, { signal });
4551
4681
  }
4552
- return await observePublishedLock(filePath, payload, signal);
4553
4682
  }
4554
4683
  //#endregion
4555
4684
  //#region src/session/queue/ipc-server.ts
@@ -4730,6 +4859,7 @@ var SessionQueueOwner = class SessionQueueOwner {
4730
4859
  return true;
4731
4860
  }
4732
4861
  handleControlRequest(options) {
4862
+ this.taskSockets.add(options.socket);
4733
4863
  writeQueueMessage(options.socket, {
4734
4864
  type: "accepted",
4735
4865
  requestId: options.requestId,
@@ -5042,7 +5172,7 @@ async function replayDesiredConfigOptions(params) {
5042
5172
  for (const [configId, value] of Object.entries(params.desiredConfigOptions)) {
5043
5173
  if (configId === replacingKey || acceptedConfigOptions && !acceptsSavedConfigValue(acceptedConfigOptions.find((option) => option.id === configId), value)) continue;
5044
5174
  try {
5045
- 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);
5046
5176
  params.replay.acknowledged = true;
5047
5177
  params.record.acpx = applyConfigOptionSelection(params.record.acpx, configId, value, response);
5048
5178
  acceptedConfigOptions = response.configOptions;
@@ -5093,6 +5223,7 @@ async function connectAndLoadSession(options) {
5093
5223
  timeoutMs: options.timeoutMs
5094
5224
  });
5095
5225
  const { resumed, loadError, sessionId, createdFreshSession, pendingAgentSessionId, sessionModels } = loadState;
5226
+ applyReconnectedModelState(record, sessionModels, loadState.configOptionsPresent, loadState.legacyModelMetadataPresent, createdFreshSession);
5096
5227
  const preferenceReplay = await replaySessionPreferences({
5097
5228
  client,
5098
5229
  record,
@@ -5313,7 +5444,7 @@ function createActiveSessionController(params) {
5313
5444
  return response;
5314
5445
  },
5315
5446
  setSessionConfigOption: async (configId, value) => {
5316
- return await params.client.setSessionConfigOption(getActiveSessionId(), configId, value);
5447
+ return await params.client.setSessionConfigOption(getActiveSessionId(), configId, value, advertisedModelState(params.record.acpx));
5317
5448
  }
5318
5449
  };
5319
5450
  }
@@ -5436,14 +5567,38 @@ function toSessionMutationResult(result) {
5436
5567
  loadError: result.loadError
5437
5568
  };
5438
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
+ }
5439
5594
  async function runSessionSetModeDirect(options) {
5440
- return toSessionMutationResult(await withConnectedSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
5595
+ return toSessionMutationResult(await withOwnedDirectSession(buildDirectConnectedSessionOptions(options, async ({ client, sessionId, record }) => {
5441
5596
  await withTimeout(client.setSessionMode(sessionId, options.modeId), options.timeoutMs);
5442
5597
  setDesiredModeId(record, options.modeId);
5443
5598
  })));
5444
5599
  }
5445
5600
  async function runSessionSetModelDirect(options) {
5446
- const result = await withConnectedSession(buildDirectConnectedSessionOptions({
5601
+ const result = await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5447
5602
  ...options,
5448
5603
  replacingConfigOption: { key: "model" }
5449
5604
  }, async ({ client, sessionId, record }) => {
@@ -5458,12 +5613,13 @@ async function runSessionSetModelDirect(options) {
5458
5613
  };
5459
5614
  }
5460
5615
  async function runSessionSetConfigOptionDirect(options) {
5461
- const result = await withConnectedSession(buildDirectConnectedSessionOptions({
5616
+ const result = await withOwnedDirectSession(buildDirectConnectedSessionOptions({
5462
5617
  ...options,
5463
5618
  replacingConfigOption: { key: options.configId }
5464
5619
  }, async ({ client, sessionId, record }) => {
5465
- const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value), options.timeoutMs);
5466
- record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, response);
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);
5467
5623
  return response;
5468
5624
  }));
5469
5625
  return {
@@ -5482,16 +5638,31 @@ async function cancelSessionPrompt(options) {
5482
5638
  cancelled: cancelled === true
5483
5639
  };
5484
5640
  }
5485
- async function setSessionMode(options) {
5486
- if (await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose)) {
5487
- 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) {
5488
5655
  setDesiredModeId(record, options.modeId);
5489
5656
  await writeSessionRecord(record);
5490
- return {
5491
- record,
5492
- resumed: false
5493
- };
5494
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;
5495
5666
  return await runSessionSetModeDirect({
5496
5667
  sessionRecordId: options.sessionId,
5497
5668
  modeId: options.modeId,
@@ -5505,18 +5676,24 @@ async function setSessionMode(options) {
5505
5676
  verbose: options.verbose
5506
5677
  });
5507
5678
  }
5508
- async function setSessionModel(options) {
5509
- const submittedToOwner = await trySetModelOnRunningOwner(options.sessionId, options.modelId, options.timeoutMs, options.verbose);
5510
- if (submittedToOwner) {
5511
- const record = await resolveSessionRecord(options.sessionId);
5512
- record.acpx = applyModelSelection(record.acpx, options.modelId, submittedToOwner.response);
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);
5513
5686
  await writeSessionRecord(record);
5514
- return {
5515
- record,
5516
- response: submittedToOwner.response,
5517
- resumed: false
5518
- };
5519
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;
5520
5697
  return await runSessionSetModelDirect({
5521
5698
  sessionRecordId: options.sessionId,
5522
5699
  modelId: options.modelId,
@@ -5530,18 +5707,24 @@ async function setSessionModel(options) {
5530
5707
  verbose: options.verbose
5531
5708
  });
5532
5709
  }
5533
- async function setSessionConfigOption(options) {
5534
- const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose);
5535
- if (ownerResponse) {
5536
- const record = await resolveSessionRecord(options.sessionId);
5537
- record.acpx = applyConfigOptionSelection(record.acpx, options.configId, options.value, ownerResponse);
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);
5538
5717
  await writeSessionRecord(record);
5539
- return {
5540
- record,
5541
- response: ownerResponse,
5542
- resumed: false
5543
- };
5544
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;
5545
5728
  return await runSessionSetConfigOptionDirect({
5546
5729
  sessionRecordId: options.sessionId,
5547
5730
  configId: options.configId,
@@ -5626,11 +5809,17 @@ async function closeSession(sessionId) {
5626
5809
  await tryCloseSessionOnRunningOwner({ sessionId: record.acpxRecordId }).catch(() => {});
5627
5810
  await terminateQueueOwnerForSession(record.acpxRecordId);
5628
5811
  if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) await terminateProcess(record.pid);
5629
- record.pid = void 0;
5630
- record.closed = true;
5631
- record.closedAt = isoNow$1();
5632
- await writeSessionRecord(record);
5633
- return record;
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
+ }
5634
5823
  }
5635
5824
  //#endregion
5636
5825
  //#region src/session/execution/session-management.ts
@@ -5951,16 +6140,182 @@ function installPerfMetricsCapture(options = {}) {
5951
6140
  }
5952
6141
  }
5953
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
5954
6313
  //#region src/session/queue/owner-turn-controller.ts
5955
6314
  var QueueOwnerTurnController = class {
5956
- options;
5957
6315
  state = "idle";
5958
6316
  pendingCancel = false;
5959
6317
  activeController;
5960
6318
  waitingTurn;
5961
- constructor(options) {
5962
- this.options = options;
5963
- }
5964
6319
  get lifecycleState() {
5965
6320
  return this.state;
5966
6321
  }
@@ -5992,20 +6347,9 @@ var QueueOwnerTurnController = class {
5992
6347
  clearActiveController() {
5993
6348
  this.activeController = void 0;
5994
6349
  }
5995
- assertCanHandleControlRequest() {
5996
- if (this.state === "closing") throw new QueueConnectionError("Queue owner is closing", {
5997
- detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
5998
- origin: "queue",
5999
- retryable: true
6000
- });
6001
- }
6002
6350
  async requestCancel() {
6003
6351
  const activeController = this.activeController;
6004
- if (activeController?.hasActivePrompt()) {
6005
- const cancelled = await activeController.requestCancelActivePrompt();
6006
- if (cancelled) this.pendingCancel = false;
6007
- return cancelled;
6008
- }
6352
+ if (activeController?.hasActivePrompt()) return await this.cancelActivePrompt(activeController);
6009
6353
  if (this.state === "starting" || this.state === "active") {
6010
6354
  this.pendingCancel = true;
6011
6355
  this.waitingTurn?.abort();
@@ -6016,32 +6360,138 @@ var QueueOwnerTurnController = class {
6016
6360
  async applyPendingCancel() {
6017
6361
  const activeController = this.activeController;
6018
6362
  if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) return false;
6019
- const cancelled = await activeController.requestCancelActivePrompt();
6020
- 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;
6021
6371
  return cancelled;
6022
6372
  }
6023
- async setSessionMode(modeId, timeoutMs) {
6024
- this.assertCanHandleControlRequest();
6025
- const activeController = this.activeController;
6026
- if (activeController) {
6027
- await this.options.withTimeout(async () => await activeController.setSessionMode(modeId), timeoutMs);
6028
- return;
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;
6029
6393
  }
6030
- await this.options.setSessionModeFallback(modeId, timeoutMs);
6394
+ throw error;
6031
6395
  }
6032
- async setSessionModel(modelId, timeoutMs) {
6033
- this.assertCanHandleControlRequest();
6034
- const activeController = this.activeController;
6035
- if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionModel(modelId), timeoutMs);
6036
- return await this.options.setSessionModelFallback(modelId, timeoutMs);
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
+ }
6037
6481
  }
6038
- async setSessionConfigOption(configId, value, timeoutMs) {
6039
- this.assertCanHandleControlRequest();
6040
- const activeController = this.activeController;
6041
- if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionConfigOption(configId, value), timeoutMs);
6042
- return await this.options.setSessionConfigOptionFallback(configId, value, timeoutMs);
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]();
6043
6493
  }
6044
- };
6494
+ }
6045
6495
  //#endregion
6046
6496
  //#region src/session/execution/queue-owner-process.ts
6047
6497
  function isNonEmptyStringArray(value) {
@@ -6223,20 +6673,27 @@ function responseMetaField(meta) {
6223
6673
  return meta === void 0 ? {} : { _meta: meta };
6224
6674
  }
6225
6675
  function recoveredSessionResult(response, conversation, promptMessageId) {
6226
- recordPromptResponseUsage(conversation, response?.usage, promptMessageId);
6676
+ recordPromptResponseUsage(conversation, response.usage, promptMessageId);
6227
6677
  return {
6228
- stopReason: "end_turn",
6678
+ stopReason: response.stopReason,
6229
6679
  source: "session",
6230
- ...responseMetaField(response?._meta)
6680
+ ...responseMetaField(response._meta)
6231
6681
  };
6232
6682
  }
6233
- function abortTimedOutRequests(error, client, sessionId) {
6234
- 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
+ };
6235
6691
  }
6236
6692
  async function runPromptTurn(params) {
6237
6693
  let settledResponse;
6694
+ const lifetime = new AbortController();
6238
6695
  try {
6239
- const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestWritten, params.onElicitation, params.onPermissionRequest, params.authority);
6696
+ const promptPromise = params.client.prompt(params.sessionId, params.prompt, params.onPromptRequestWritten, params.onElicitation, params.onPermissionRequest, promptRequestAuthority(lifetime, params.authority));
6240
6697
  promptPromise.then((response) => {
6241
6698
  settledResponse = response;
6242
6699
  }, () => {});
@@ -6253,14 +6710,16 @@ async function runPromptTurn(params) {
6253
6710
  ...responseMetaField(response._meta)
6254
6711
  };
6255
6712
  } catch (error) {
6256
- abortTimedOutRequests(error, params.client, params.sessionId);
6713
+ abortTimedOutRequests(error, lifetime);
6257
6714
  if (!(error instanceof TimeoutError) || !params.promptMessageId) throw error;
6258
6715
  await params.client.waitForSessionUpdatesIdle?.({
6259
6716
  idleMs: SESSION_REPLY_IDLE_MS,
6260
6717
  timeoutMs: SESSION_REPLY_DRAIN_TIMEOUT_MS
6261
6718
  }).catch(() => {});
6262
- if (hasAgentReplyAfterPrompt(params.conversation, params.promptMessageId)) return recoveredSessionResult(settledResponse, params.conversation, params.promptMessageId);
6719
+ if (settledResponse) return recoveredSessionResult(settledResponse, params.conversation, params.promptMessageId);
6263
6720
  throw error;
6721
+ } finally {
6722
+ lifetime.abort();
6264
6723
  }
6265
6724
  }
6266
6725
  //#endregion
@@ -6510,8 +6969,9 @@ function emitPromptDisconnectNotice(snapshot, verbose) {
6510
6969
  if (!lastExit?.unexpectedDuringPrompt || !verbose) return;
6511
6970
  process.stderr.write("[acpx] agent disconnected during prompt (" + lastExit.reason + ", exit=" + lastExit.exitCode + ", signal=" + (lastExit.signal ?? "none") + ")\n");
6512
6971
  }
6513
- async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors) {
6972
+ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, suppressSdkConsoleErrors, signal) {
6514
6973
  if (attempt < maxRetries && !hasSideEffects() && isRetryablePromptError(error)) {
6974
+ signal?.throwIfAborted();
6515
6975
  const delayMs = Math.min(1e3 * 2 ** attempt, 1e4);
6516
6976
  emitPromptRetryNotice({
6517
6977
  error,
@@ -6520,11 +6980,18 @@ async function preparePromptRetry(error, attempt, maxRetries, hasSideEffects, su
6520
6980
  maxRetries,
6521
6981
  suppressSdkConsoleErrors
6522
6982
  });
6523
- await waitMs$1(delayMs);
6983
+ await setTimeout$1(delayMs, void 0, { signal }).catch((error) => {
6984
+ signal?.throwIfAborted();
6985
+ throw error;
6986
+ });
6987
+ signal?.throwIfAborted();
6524
6988
  return !hasSideEffects();
6525
6989
  }
6526
6990
  return false;
6527
6991
  }
6992
+ function isTurnCancellation(error, signal) {
6993
+ return signal?.aborted === true && error === signal.reason;
6994
+ }
6528
6995
  function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatter) {
6529
6996
  return {
6530
6997
  sessionRecordId,
@@ -6546,7 +7013,9 @@ function buildQueuedTaskRunOptions(sessionRecordId, task, options, outputFormatt
6546
7013
  sessionOptions: mergeSessionOptions(task.sessionOptions, options.sessionOptions),
6547
7014
  onClientAvailable: options.onClientAvailable,
6548
7015
  onClientClosed: options.onClientClosed,
7016
+ onClientCloseFailure: options.onClientCloseFailure,
6549
7017
  onPromptActive: options.onPromptActive,
7018
+ onPromptFinalizing: options.onPromptFinalizing,
6550
7019
  onPromptRequestWritten: () => {
6551
7020
  if (task.reportPromptStarted) task.send({
6552
7021
  type: "prompt_started",
@@ -6852,24 +7321,23 @@ async function runOwnedSessionPrompt(options) {
6852
7321
  });
6853
7322
  let activeSessionIdForControl = record.acpSessionId;
6854
7323
  let notifiedClientAvailable = false;
7324
+ let controlRetirement;
6855
7325
  const activeController = {
6856
7326
  hasActivePrompt: () => client.hasActivePrompt(),
6857
7327
  requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
6858
- setSessionMode: async (modeId) => {
6859
- await client.setSessionMode(activeSessionIdForControl, modeId);
6860
- },
6861
- setSessionModel: async (modelId) => {
6862
- const models = advertisedModelState(record.acpx);
6863
- const response = await client.setSessionModel(activeSessionIdForControl, modelId, models);
6864
- record.acpx = applyModelSelection(record.acpx, modelId, response);
6865
- return response;
6866
- },
6867
- setSessionConfigOption: async (configId, value) => {
6868
- const modelConfigId = advertisedModelState(record.acpx)?.configId;
6869
- const response = await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
6870
- record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response, modelConfigId);
6871
- return response;
6872
- }
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
+ })
6873
7341
  };
6874
7342
  const flushConnectOutput = (loadError) => {
6875
7343
  bufferingConnectOutput = false;
@@ -6933,15 +7401,17 @@ async function runOwnedSessionPrompt(options) {
6933
7401
  conversation,
6934
7402
  promptMessageId,
6935
7403
  onPromptRequestWritten: options.onPromptRequestWritten,
6936
- onPromptStarted: buildPromptStartedHook(attempt)
7404
+ onPromptStarted: buildPromptStartedHook(attempt),
7405
+ authority: { signal: options.waitSignal }
6937
7406
  });
6938
7407
  });
6939
7408
  emitPromptPerfMetric(promptStartedAt, options.verbose);
6940
7409
  return response;
6941
7410
  };
6942
7411
  const handlePromptFailure = async (error, attempt) => {
7412
+ if (isTurnCancellation(error, options.waitSignal)) throw error;
6943
7413
  const snapshot = client.getAgentLifecycleSnapshot();
6944
- 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;
6945
7415
  await failRuntimePrompt(error, snapshot);
6946
7416
  };
6947
7417
  const failRuntimePrompt = async (error, snapshot) => {
@@ -6965,10 +7435,18 @@ async function runOwnedSessionPrompt(options) {
6965
7435
  };
6966
7436
  const runPromptWithRetries = async (sessionId) => {
6967
7437
  promptTurnActive = true;
6968
- for (let attempt = 0;; attempt++) try {
6969
- 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
+ }
6970
7447
  } catch (error) {
6971
- await handlePromptFailure(error, attempt);
7448
+ if (isTurnCancellation(error, options.waitSignal)) return { stopReason: "cancelled" };
7449
+ throw error;
6972
7450
  }
6973
7451
  };
6974
7452
  const savePromptSuccess = async (response) => {
@@ -7029,7 +7507,24 @@ async function runOwnedSessionPrompt(options) {
7029
7507
  }
7030
7508
  };
7031
7509
  const cleanupPrompt = async (attempt) => {
7510
+ const controlDrain = options.onPromptFinalizing?.();
7511
+ const unresolvedPrompt = client.hasUnresolvedPrompt();
7032
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
+ },
7033
7528
  () => {
7034
7529
  const duration = stopTotalTimer();
7035
7530
  if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", duration)}\n`);
@@ -7038,10 +7533,6 @@ async function runOwnedSessionPrompt(options) {
7038
7533
  if (notifiedClientAvailable) options.onClientClosed?.();
7039
7534
  },
7040
7535
  () => client.clearEventHandlers(),
7041
- async () => {
7042
- const journalFailed = attempt.status === "rejected" && attempt.error instanceof AcpxOperationalError && attempt.error.detailCode === "SESSION_JOURNAL_WRITE_FAILED";
7043
- if (closeClientOnExit || journalFailed) await client.close();
7044
- },
7045
7536
  () => applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot()),
7046
7537
  () => applyConversation(record, conversation)
7047
7538
  ];
@@ -7068,6 +7559,7 @@ async function runOnce(options) {
7068
7559
  const shouldMarkAcpErrorsEmitted = rendersAcpErrors(options.errorEmissionPolicy);
7069
7560
  let promptTurnActive = false;
7070
7561
  let promptTurnHadSideEffects = false;
7562
+ let controlState = {};
7071
7563
  const acpErrors = new AcpErrorTracker();
7072
7564
  const client = new AcpClient({
7073
7565
  agentCommand: options.agentCommand,
@@ -7088,6 +7580,7 @@ async function runOnce(options) {
7088
7580
  acpErrors.observe(output, direction, message);
7089
7581
  },
7090
7582
  onSessionUpdate: (notification) => {
7583
+ if (notification.update.sessionUpdate === "config_option_update") controlState = applyConfigOptionsToState(controlState, notification.update.configOptions);
7091
7584
  if (promptTurnActive) promptTurnHadSideEffects = true;
7092
7585
  options.onSessionUpdate?.(notification);
7093
7586
  },
@@ -7127,7 +7620,9 @@ async function runOnce(options) {
7127
7620
  return await withTimeout(client.createSession(absolutePath(options.cwd)), options.timeoutMs);
7128
7621
  });
7129
7622
  const sessionId = createdSession.sessionId;
7130
- await applyRequestedModelIfAdvertised({
7623
+ if (createdSession.models) applyAdvertisedModelState(controlState, createdSession.models);
7624
+ controlState = applyConfigOptionsToState(controlState, createdSession.configOptions ?? []);
7625
+ const modelApplication = await applyRequestedModelIfAdvertised({
7131
7626
  client,
7132
7627
  sessionId,
7133
7628
  requestedModel: options.sessionOptions?.model,
@@ -7136,7 +7631,11 @@ async function runOnce(options) {
7136
7631
  timeoutMs: options.timeoutMs,
7137
7632
  onWarning: options.suppressSdkConsoleErrors ? void 0 : (message) => process.stderr.write(`[acpx] warning: ${message}\n`)
7138
7633
  });
7139
- 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
+ }
7140
7639
  output.setContext({ sessionId });
7141
7640
  const response = await runExecPromptWithRetries(sessionId);
7142
7641
  promptTurnActive = false;
@@ -7231,54 +7730,6 @@ function createQueueOwnerSharedClient(options, sessionRecord) {
7231
7730
  sessionOptions: mergeSessionOptions(options.sessionOptions, sessionOptionsFromRecord(sessionRecord))
7232
7731
  });
7233
7732
  }
7234
- function createQueueOwnerTurnController(options) {
7235
- return new QueueOwnerTurnController({
7236
- withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
7237
- setSessionModeFallback: async (modeId, timeoutMs) => {
7238
- await runSessionSetModeDirect({
7239
- sessionRecordId: options.sessionId,
7240
- modeId,
7241
- mcpServers: options.mcpServers,
7242
- nonInteractivePermissions: options.nonInteractivePermissions,
7243
- authCredentials: options.authCredentials,
7244
- authPolicy: options.authPolicy,
7245
- fs: options.fs,
7246
- terminal: options.terminal,
7247
- timeoutMs,
7248
- verbose: options.verbose
7249
- });
7250
- },
7251
- setSessionModelFallback: async (modelId, timeoutMs) => {
7252
- return (await runSessionSetModelDirect({
7253
- sessionRecordId: options.sessionId,
7254
- modelId,
7255
- mcpServers: options.mcpServers,
7256
- nonInteractivePermissions: options.nonInteractivePermissions,
7257
- authCredentials: options.authCredentials,
7258
- authPolicy: options.authPolicy,
7259
- fs: options.fs,
7260
- terminal: options.terminal,
7261
- timeoutMs,
7262
- verbose: options.verbose
7263
- })).response;
7264
- },
7265
- setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
7266
- return (await runSessionSetConfigOptionDirect({
7267
- sessionRecordId: options.sessionId,
7268
- configId,
7269
- value,
7270
- mcpServers: options.mcpServers,
7271
- nonInteractivePermissions: options.nonInteractivePermissions,
7272
- authCredentials: options.authCredentials,
7273
- authPolicy: options.authPolicy,
7274
- fs: options.fs,
7275
- terminal: options.terminal,
7276
- timeoutMs,
7277
- verbose: options.verbose
7278
- })).response;
7279
- }
7280
- });
7281
- }
7282
7733
  function logDeferredCancelFailure(error, verbose) {
7283
7734
  if (!verbose) return;
7284
7735
  process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
@@ -7322,16 +7773,17 @@ function createQueueOwnerShutdownController(params) {
7322
7773
  let shutdownPromise;
7323
7774
  const drainActiveTurn = async () => {
7324
7775
  const turn = activeTurn;
7325
- if (!turn) return;
7326
- params.turnController.requestCancel().catch((error) => {
7776
+ const pending = Promise.allSettled([turn, params.controls.drainAdmitted()]);
7777
+ if (turn) params.turnController.requestCancel().catch((error) => {
7327
7778
  logDeferredCancelFailure(error, params.verbose);
7328
7779
  });
7329
- if (!await settlesWithin(turn, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
7330
- await turn.catch(() => {});
7780
+ if (!await settlesWithin(pending, QUEUE_OWNER_ACTIVE_TURN_CANCEL_GRACE_MS)) await params.sharedClient.close().catch(() => {});
7781
+ await pending;
7331
7782
  };
7332
7783
  const request = () => {
7333
7784
  requested = true;
7334
7785
  params.stopHeartbeat();
7786
+ params.controls.stopAdmission();
7335
7787
  params.getOwner()?.beginShutdown();
7336
7788
  activeTurnShutdown ??= drainActiveTurn();
7337
7789
  };
@@ -7342,14 +7794,20 @@ function createQueueOwnerShutdownController(params) {
7342
7794
  request,
7343
7795
  setActiveTurn: (turn) => {
7344
7796
  activeTurn = turn;
7797
+ if (requested) activeTurnShutdown = Promise.all([activeTurnShutdown, drainActiveTurn()]).then(() => {});
7345
7798
  },
7346
7799
  clearActiveTurn: (turn) => {
7347
7800
  if (activeTurn === turn) activeTurn = void 0;
7348
7801
  },
7802
+ drain: () => {
7803
+ request();
7804
+ return activeTurnShutdown;
7805
+ },
7349
7806
  shutdown: () => {
7350
7807
  request();
7351
7808
  shutdownPromise ??= (async () => {
7352
7809
  await activeTurnShutdown;
7810
+ await params.getBackendClose()?.catch(() => {});
7353
7811
  await closeQueueOwnerRuntime({
7354
7812
  owner: params.getOwner(),
7355
7813
  turnController: params.turnController,
@@ -7368,20 +7826,50 @@ function applyRequestedShutdown(owner, shutdown) {
7368
7826
  function startQueueOwnerHeartbeat(params) {
7369
7827
  if (!params.enabled) return;
7370
7828
  return setInterval(() => {
7371
- refreshQueueOwnerLease(params.lease, { queueDepth: params.owner.queueDepth() }).catch(() => {});
7829
+ params.refresh(params.owner.queueDepth());
7372
7830
  }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
7373
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
+ }
7374
7849
  async function runSessionQueueOwner(options) {
7375
7850
  const lease = await tryAcquireQueueOwnerLease(options.sessionId, {
7376
7851
  path: options.mcpConfigPath,
7377
7852
  fingerprint: options.mcpConfigFingerprint
7378
7853
  });
7379
7854
  if (!lease) return;
7855
+ let runtimeFailure;
7380
7856
  try {
7381
7857
  await runQueueOwnerRuntime(options, lease);
7382
- } finally {
7858
+ } catch (error) {
7859
+ runtimeFailure = { error };
7860
+ }
7861
+ try {
7383
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;
7384
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;
7385
7873
  }
7386
7874
  async function runQueueOwnerRuntime(options, lease) {
7387
7875
  const sessionRecord = await resolveSessionRecord(options.sessionId);
@@ -7392,7 +7880,16 @@ async function runQueueOwnerRuntime(options, lease) {
7392
7880
  const maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth ?? 16));
7393
7881
  const taskPollTimeoutMs = ttlMs === 0 ? void 0 : ttlMs;
7394
7882
  const initialTaskPollTimeoutMs = taskPollTimeoutMs == null ? void 0 : Math.max(taskPollTimeoutMs, 1e3);
7395
- 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) });
7396
7893
  const applyPendingCancel = async () => {
7397
7894
  return await turnController.applyPendingCancel();
7398
7895
  };
@@ -7402,23 +7899,34 @@ async function runQueueOwnerRuntime(options, lease) {
7402
7899
  });
7403
7900
  };
7404
7901
  const setActiveController = (controller) => {
7902
+ activeControls = controller;
7405
7903
  turnController.setActiveController(controller);
7406
7904
  scheduleApplyPendingCancel();
7407
7905
  };
7408
7906
  const clearActiveController = () => {
7907
+ activeControls = void 0;
7409
7908
  turnController.clearActiveController();
7410
7909
  };
7411
- const closeActiveBackendSession = async (timeoutMs) => {
7412
- const latestRecord = await resolveSessionRecord(options.sessionId);
7413
- if (!sharedClient.supportsCloseSession()) return false;
7414
- await withTimeout(sharedClient.closeSession(latestRecord.acpSessionId), timeoutMs);
7415
- return true;
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;
7416
7919
  };
7417
7920
  const runPromptTurn = async (run) => {
7418
7921
  const waitSignal = turnController.beginTurn();
7922
+ const { ticket, priorIdle } = controls.beginPrompt();
7923
+ if (shutdown.requested) turnController.requestCancel().catch(() => {});
7419
7924
  try {
7420
- return await run(waitSignal);
7925
+ await priorIdle;
7926
+ return await run(waitSignal, ticket);
7421
7927
  } finally {
7928
+ await controls.seal(ticket);
7929
+ controls.release(ticket);
7422
7930
  turnController.endTurn();
7423
7931
  }
7424
7932
  };
@@ -7431,6 +7939,8 @@ async function runQueueOwnerRuntime(options, lease) {
7431
7939
  }
7432
7940
  },
7433
7941
  turnController,
7942
+ controls,
7943
+ getBackendClose: () => backendCloseRequest,
7434
7944
  sharedClient,
7435
7945
  sessionId: options.sessionId,
7436
7946
  verbose: options.verbose
@@ -7438,6 +7948,7 @@ async function runQueueOwnerRuntime(options, lease) {
7438
7948
  const onSignal = () => {
7439
7949
  shutdown.request();
7440
7950
  };
7951
+ const heartbeat = createLeaseHeartbeat(lease, shutdown);
7441
7952
  process.once("SIGINT", onSignal);
7442
7953
  process.once("SIGTERM", onSignal);
7443
7954
  process.once("SIGHUP", onSignal);
@@ -7450,17 +7961,17 @@ async function runQueueOwnerRuntime(options, lease) {
7450
7961
  },
7451
7962
  closeSession: async (timeoutMs) => await closeActiveBackendSession(timeoutMs),
7452
7963
  setSessionMode: async (modeId, timeoutMs) => {
7453
- await turnController.setSessionMode(modeId, timeoutMs);
7964
+ await controls.run((context, deadline) => context.setSessionMode(modeId, deadline), timeoutMs);
7454
7965
  },
7455
- 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" }),
7456
7967
  setSessionConfigOption: async (configId, value, timeoutMs) => {
7457
- return await turnController.setSessionConfigOption(configId, value, timeoutMs);
7968
+ return await controls.run((context, deadline) => context.setSessionConfigOption(configId, value, deadline), timeoutMs, { key: configId });
7458
7969
  }
7459
7970
  }, {
7460
7971
  maxQueueDepth,
7461
7972
  onQueueDepthChanged: (queueDepth) => {
7462
7973
  setPerfGauge("queue.owner.depth", queueDepth);
7463
- refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
7974
+ heartbeat.refresh(queueDepth);
7464
7975
  }
7465
7976
  });
7466
7977
  applyRequestedShutdown(owner, shutdown);
@@ -7470,19 +7981,22 @@ async function runQueueOwnerRuntime(options, lease) {
7470
7981
  maxQueueDepth,
7471
7982
  verbose: options.verbose
7472
7983
  });
7473
- await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
7984
+ await heartbeat.refresh(owner.queueDepth());
7474
7985
  heartbeatTimer = startQueueOwnerHeartbeat({
7475
7986
  enabled: !shutdown.requested,
7476
- lease,
7987
+ refresh: heartbeat.refresh,
7477
7988
  owner
7478
7989
  });
7479
7990
  let isFirstTask = true;
7480
7991
  while (true) {
7481
7992
  const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
7482
7993
  const task = await owner.nextTask(pollTimeoutMs);
7483
- if (!task) break;
7994
+ if (!task) {
7995
+ if (await keepOwnerForPendingControls(controls, shutdown)) continue;
7996
+ break;
7997
+ }
7484
7998
  isFirstTask = false;
7485
- const turnPromise = runPromptTurn(async (waitSignal) => {
7999
+ const turnPromise = runPromptTurn(async (waitSignal, ticket) => {
7486
8000
  try {
7487
8001
  await runQueuedTask(options.sessionId, task, {
7488
8002
  sharedClient,
@@ -7496,7 +8010,10 @@ async function runQueueOwnerRuntime(options, lease) {
7496
8010
  sessionOptions: options.sessionOptions,
7497
8011
  onClientAvailable: setActiveController,
7498
8012
  onClientClosed: clearActiveController,
8013
+ onClientCloseFailure: shutdown.request,
8014
+ onPromptFinalizing: () => controls.seal(ticket),
7499
8015
  onPromptActive: async () => {
8016
+ if (activeControls) controls.publish(ticket, activeControls);
7500
8017
  turnController.markPromptActive();
7501
8018
  await applyPendingCancel();
7502
8019
  },
@@ -7520,7 +8037,9 @@ async function runQueueOwnerRuntime(options, lease) {
7520
8037
  process.off("SIGTERM", onSignal);
7521
8038
  process.off("SIGHUP", onSignal);
7522
8039
  await shutdown.shutdown();
8040
+ await lease.updates;
7523
8041
  }
8042
+ heartbeat.throwIfFailed();
7524
8043
  }
7525
8044
  async function sendSession(options) {
7526
8045
  options.signal?.throwIfAborted();
@@ -7560,6 +8079,6 @@ async function sendSession(options) {
7560
8079
  }
7561
8080
  const queueOwnerRuntimeTestInternals = { queueOwnerExitIsFatal };
7562
8081
  //#endregion
7563
- export { normalizeQueueOwnerTtlMs as $, setDesiredModeId as A, recordClientOperation as B, withConnectedSession as C, applyInitialModelSelection as D, applyConfigOptionsToRecord as E, createInitialSessionRecord as F, AcpClient as G, recordSessionUpdate as H, reconcileAgentSessionId as I, RequestedModelUnsupportedError as J, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE as K, cloneSessionAcpxState as L, sessionOptionsFromRecord as M, applyConversation as N, applyModelSelection as O, applyLifecycleSnapshotToRecord as P, DEFAULT_QUEUE_OWNER_TTL_MS as Q, cloneSessionConversation as R, setSessionModel as S, applyConfigOptionSelection as T, trimConversationForRuntime as U, recordPromptSubmission as V, advertisedModelState as W, getAcpxVersion as X, isRequestedModelUnsupportedError as Y, permissionModeSatisfies as Z, cancelSessionPrompt as _, runQueuedTask as a, withInterrupt as at, setSessionConfigOption as b, LiveSessionCheckpoint as c, normalizeOutputError as ct, flushPerfMetricsCapture as d, listSessionEvents as et, installPerfMetricsCapture as f, listAgentSessions as g, ensureSession as h, runOnce as i, assertControlAuthority as it, persistSessionOptions as j, applyRequestedModelIfAdvertised as k, runPromptTurn as l, createSessionWithClient as m, runSessionQueueOwner as n, InterruptedError as nt, sendSessionDirect as o, withTimeout as ot, createSession as p, REQUESTED_MODEL_UNSUPPORTED_REASONS as q, sendSession as r, TimeoutError as rt, DISCARD_OUTPUT_FORMATTER as s, exitCodeForOutputErrorCode as st, queueOwnerRuntimeTestInternals as t, isLegacyZedCodexAcpInvocation as tt, buildQueueOwnerArgOverride as u, closeSession as v, connectAndLoadSession as w, setSessionMode as x, sessionControlTestInternals as y, createSessionConversation as z };
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 };
7564
8083
 
7565
- //# sourceMappingURL=queue-owner-runtime-Dgv3e2Tp.js.map
8084
+ //# sourceMappingURL=queue-owner-runtime-Cr0dWJuL.js.map