acpx 0.15.1 → 0.17.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 (47) hide show
  1. package/README.md +2 -0
  2. package/dist/agent-registry-DU6mhZBL.js +785 -0
  3. package/dist/agent-registry-DU6mhZBL.js.map +1 -0
  4. package/dist/agent-registry.d.ts +70 -0
  5. package/dist/agent-registry.d.ts.map +1 -0
  6. package/dist/agent-registry.js +2 -0
  7. package/dist/{cli-BHAT_D3V.js → cli-CiyfEC7L.js} +7 -14
  8. package/dist/cli-CiyfEC7L.js.map +1 -0
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +160 -349
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{client-Cvz6msGc.d.ts → client-C6NUFqr-.d.ts} +21 -13
  14. package/dist/client-C6NUFqr-.d.ts.map +1 -0
  15. package/dist/{flows-BOI1XkMp.js → flows-CTOdIk-m.js} +208 -279
  16. package/dist/flows-CTOdIk-m.js.map +1 -0
  17. package/dist/flows.d.ts +6 -17
  18. package/dist/flows.d.ts.map +1 -1
  19. package/dist/flows.js +1 -1
  20. package/dist/{flags-BHcZWTov.js → invocation-options-DbCcOAzM.js} +83 -58
  21. package/dist/invocation-options-DbCcOAzM.js.map +1 -0
  22. package/dist/ipc-D3akHMEq.js +3350 -0
  23. package/dist/ipc-D3akHMEq.js.map +1 -0
  24. package/dist/output-BPPjvJTI.js +980 -0
  25. package/dist/output-BPPjvJTI.js.map +1 -0
  26. package/dist/{live-checkpoint-sB7dFOYR.js → queue-owner-runtime--IbbndGz.js} +5359 -4574
  27. package/dist/queue-owner-runtime--IbbndGz.js.map +1 -0
  28. package/dist/runtime.d.ts +197 -27
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +864 -409
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/{session-options-DyIGRNfu.d.ts → session-options-Dp6mzNSy.d.ts} +8 -6
  33. package/dist/session-options-Dp6mzNSy.d.ts.map +1 -0
  34. package/dist/session-watch--UX5dh00.js +76 -0
  35. package/dist/session-watch--UX5dh00.js.map +1 -0
  36. package/dist/watch-BbBxO-kb.js +32 -0
  37. package/dist/watch-BbBxO-kb.js.map +1 -0
  38. package/package.json +27 -25
  39. package/skills/acpx/SKILL.md +24 -19
  40. package/dist/cli-BHAT_D3V.js.map +0 -1
  41. package/dist/client-Cvz6msGc.d.ts.map +0 -1
  42. package/dist/flags-BHcZWTov.js.map +0 -1
  43. package/dist/flows-BOI1XkMp.js.map +0 -1
  44. package/dist/live-checkpoint-sB7dFOYR.js.map +0 -1
  45. package/dist/output-8-D6Gd4Z.js +0 -4714
  46. package/dist/output-8-D6Gd4Z.js.map +0 -1
  47. package/dist/session-options-DyIGRNfu.d.ts.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,4 +1,8 @@
1
- import { A as RequestedModelUnsupportedError, At as DEFAULT_AGENT_NAME, Bt as extractAcpError, C as sessionOptionsFromRecord, D as AcpClient, E as reconcileAgentSessionId, Ft as resolveCanonicalAgentName, M as modelStateFromConfigOptions, Mt as normalizeAgentName, N as normalizeAgentCommandInput, Nt as resolveAgentArgv, O as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, Pt as resolveAgentCommand, Q as assertPersistedKeyPolicy, S as persistSessionOptions, T as applyLifecycleSnapshotToRecord, Tt as textPrompt, Vt as isAcpResourceNotFoundError, Z as createAtomicWriteTempPath, _ as recordPromptSubmission, a as applyConfigOptionSelection, b as advertisedModelState, c as applyRequestedModelIfAdvertised, ct as serializeSessionRecordForDisk, d as setDesiredModeId, dt as defaultSessionEventLog, f as syncAdvertisedModelState, g as recordClientOperation, h as createSessionConversation, i as connectAndLoadSession, j as isRequestedModelUnsupportedError, jt as listBuiltInAgents, k as REQUESTED_MODEL_UNSUPPORTED_REASONS, kt as withTimeout, l as currentModelIdFromSetModelResponse, m as cloneSessionConversation, n as runPromptTurn, o as applyConfigOptionsToRecord, p as cloneSessionAcpxState, r as withConnectedSession, s as applyModelSelection, st as parseSessionRecord, t as LiveSessionCheckpoint, u as setCurrentModelId, v as recordSessionUpdate, w as applyConversation, y as trimConversationForRuntime, zt as normalizeOutputError } from "./live-checkpoint-sB7dFOYR.js";
1
+ import { C as textPrompt, D as findSession, F as resolveSessionRecord, L as assertPersistedKeyPolicy, P as readSessionRecord, R as parseSessionRecord, gt as extractAcpError, mt as writePrivateJsonFile, t as tryCancelOnRunningOwner, vt as isAcpResourceNotFoundError, z as serializeSessionRecordForDisk } from "./ipc-D3akHMEq.js";
2
+ import { A as setDesiredModeId, B as recordClientOperation, C as withConnectedSession, D as applyInitialModelSelection, E as applyConfigOptionsToRecord, F as createInitialSessionRecord, G as AcpClient, H as recordSessionUpdate, I as reconcileAgentSessionId, J as RequestedModelUnsupportedError, K as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, L as cloneSessionAcpxState, M as sessionOptionsFromRecord, N as applyConversation, O as applyModelSelection, P as applyLifecycleSnapshotToRecord, R as cloneSessionConversation, T as applyConfigOptionSelection, U as trimConversationForRuntime, V as recordPromptSubmission, W as advertisedModelState, Y as isRequestedModelUnsupportedError, _ as cancelSessionPrompt, c as LiveSessionCheckpoint, ct as normalizeOutputError, h as ensureSession, it as assertControlAuthority, j as persistSessionOptions, k as applyRequestedModelIfAdvertised, l as runPromptTurn, ot as withTimeout, q as REQUESTED_MODEL_UNSUPPORTED_REASONS, r as sendSession, s as DISCARD_OUTPUT_FORMATTER, v as closeSession, w as connectAndLoadSession } from "./queue-owner-runtime--IbbndGz.js";
3
+ import { D as normalizeAgentCommandInput, a as createAgentRegistry, i as DEFAULT_AGENT_NAME } from "./agent-registry-DU6mhZBL.js";
4
+ import { t as watchSession } from "./watch-BbBxO-kb.js";
5
+ import { fileURLToPath } from "node:url";
2
6
  import path from "node:path";
3
7
  import fs from "node:fs/promises";
4
8
  import { randomUUID } from "node:crypto";
@@ -84,8 +88,7 @@ const STATUS_TEXT_RESOLVERS = {
84
88
  available_commands_update: availableCommandsStatusText,
85
89
  current_mode_update: currentModeStatusText,
86
90
  config_option_update: configOptionStatusText,
87
- session_info_update: sessionInfoStatusText,
88
- plan: planStatusText
91
+ session_info_update: sessionInfoStatusText
89
92
  };
90
93
  function availableCommandsStatusText(payload) {
91
94
  const commands = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
@@ -108,6 +111,51 @@ function planStatusText(payload) {
108
111
  const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
109
112
  return content ? `plan: ${content}` : null;
110
113
  }
114
+ function isPlanEntryStatus(value) {
115
+ return value === "pending" || value === "in_progress" || value === "completed";
116
+ }
117
+ function isPlanEntryPriority(value) {
118
+ return value === "high" || value === "medium" || value === "low";
119
+ }
120
+ function planUpdateEvent(payload) {
121
+ const raw = payload.entries;
122
+ const entries = normalizePlanEntries(raw);
123
+ if (Array.isArray(raw) && raw.length === 0) return {
124
+ type: "status",
125
+ text: "plan updated",
126
+ tag: "plan",
127
+ entries: []
128
+ };
129
+ const text = planStatusText(payload) ?? (entries.length > 0 ? `plan: ${entries[0]?.content}` : null);
130
+ if (!text) return null;
131
+ return {
132
+ type: "status",
133
+ text,
134
+ tag: "plan",
135
+ ...entries.length > 0 ? { entries } : {}
136
+ };
137
+ }
138
+ function normalizePlanEntries(value) {
139
+ if (!Array.isArray(value)) return [];
140
+ const entries = [];
141
+ for (const entry of value) {
142
+ const normalized = normalizePlanEntry(entry);
143
+ if (normalized) entries.push(normalized);
144
+ }
145
+ return entries;
146
+ }
147
+ function normalizePlanEntry(value) {
148
+ if (!isRecord(value)) return;
149
+ const content = asTrimmedString(value.content);
150
+ const status = asTrimmedString(value.status);
151
+ if (!content || !isPlanEntryStatus(status)) return;
152
+ const priority = asTrimmedString(value.priority);
153
+ return {
154
+ content,
155
+ status,
156
+ ...isPlanEntryPriority(priority) ? { priority } : {}
157
+ };
158
+ }
111
159
  /**
112
160
  * Documented allowlist for text_delta.meta origin fields.
113
161
  * Only these keys may be copied from ACP update `_meta`.
@@ -371,7 +419,7 @@ const PROMPT_EVENT_PARSERS = {
371
419
  current_mode_update: (payload) => statusUpdateEvent("current_mode_update", payload),
372
420
  config_option_update: (payload) => statusUpdateEvent("config_option_update", payload),
373
421
  session_info_update: (payload) => statusUpdateEvent("session_info_update", payload),
374
- plan: (payload) => statusUpdateEvent("plan", payload),
422
+ plan: planUpdateEvent,
375
423
  client_operation: clientOperationEvent,
376
424
  update: updateStatusEvent,
377
425
  done: () => null,
@@ -498,6 +546,88 @@ function updateStatusEvent(payload, tag) {
498
546
  } : null;
499
547
  }
500
548
  //#endregion
549
+ //#region src/runtime/public/probe.ts
550
+ function isPrimitiveDetail(value) {
551
+ return value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol";
552
+ }
553
+ function formatFunctionDetail(value) {
554
+ return value.name ? `[Function ${value.name}]` : "[Function]";
555
+ }
556
+ function serializeRuntimeDetail(value) {
557
+ const seen = /* @__PURE__ */ new WeakSet();
558
+ return JSON.stringify(value, (_key, nested) => {
559
+ if (nested instanceof Error) return nested.message || nested.name;
560
+ if (nested && typeof nested === "object") {
561
+ if (seen.has(nested)) return "[Circular]";
562
+ seen.add(nested);
563
+ }
564
+ return nested;
565
+ }) ?? "undefined";
566
+ }
567
+ function formatRuntimeDetail(value) {
568
+ if (value instanceof Error) return value.message || value.name;
569
+ if (typeof value === "string") return value;
570
+ if (isPrimitiveDetail(value)) return String(value);
571
+ if (typeof value === "function") return formatFunctionDetail(value);
572
+ try {
573
+ return serializeRuntimeDetail(value);
574
+ } catch {
575
+ return "unserializable object";
576
+ }
577
+ }
578
+ function normalizeRuntimeDetails(details) {
579
+ return details?.map((detail) => formatRuntimeDetail(detail));
580
+ }
581
+ async function probeRuntime(options, deps = {}) {
582
+ const agentName = options.probeAgent?.trim() || "codex";
583
+ const agentCommand = normalizeAgentCommandInput(options.agentRegistry.resolve(agentName));
584
+ const client = createProbeClient(options, agentName, agentCommand, deps);
585
+ try {
586
+ await client.start();
587
+ return {
588
+ ok: true,
589
+ message: "embedded ACP runtime ready",
590
+ details: [
591
+ `agent=${agentName}`,
592
+ `command=${agentCommand.agentCommand}`,
593
+ `cwd=${options.cwd}`,
594
+ ...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
595
+ ]
596
+ };
597
+ } catch (error) {
598
+ return {
599
+ ok: false,
600
+ message: "embedded ACP runtime probe failed",
601
+ details: [
602
+ `agent=${agentName}`,
603
+ `command=${agentCommand.agentCommand}`,
604
+ `cwd=${options.cwd}`,
605
+ formatRuntimeDetail(error)
606
+ ]
607
+ };
608
+ } finally {
609
+ await client.close().catch(() => {});
610
+ }
611
+ }
612
+ function createProbeClient(options, agentName, agentCommand, deps) {
613
+ const clientOptions = {
614
+ ...agentCommand,
615
+ cwd: options.cwd,
616
+ agentProcessEnv: options.agentProcessEnv,
617
+ mcpServers: typeof options.mcpServers === "function" ? [] : [...options.mcpServers ?? []],
618
+ permissionMode: options.permissionMode,
619
+ nonInteractivePermissions: options.nonInteractivePermissions,
620
+ permissionPolicy: options.permissionPolicy,
621
+ processLifecycle: options.processLifecycle,
622
+ processLaunchScope: {
623
+ kind: "runtime-probe",
624
+ agent: agentName
625
+ },
626
+ verbose: options.verbose
627
+ };
628
+ return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
629
+ }
630
+ //#endregion
501
631
  //#region src/runtime/engine/reuse-policy.ts
502
632
  function shouldReuseExistingRecord(record, params) {
503
633
  if (record.acpx?.reset_on_next_ensure === true) return false;
@@ -513,7 +643,100 @@ function sameArgv(left, right) {
513
643
  return leftValues.length === rightValues.length && leftValues.every((value, index) => value === rightValues[index]);
514
644
  }
515
645
  //#endregion
516
- //#region src/runtime/engine/manager.ts
646
+ //#region src/runtime/engine/status.ts
647
+ function runtimeStatusFromRecord(record) {
648
+ return {
649
+ ...record.lastRequestId ? { lastRequestId: record.lastRequestId } : {},
650
+ summary: statusSummary(record),
651
+ acpxRecordId: record.acpxRecordId,
652
+ backendSessionId: record.acpSessionId,
653
+ agentSessionId: record.agentSessionId,
654
+ ...buildModelsField(record),
655
+ ...buildUsageField(record),
656
+ ...buildAvailableCommandsField(record),
657
+ details: {
658
+ cwd: record.cwd,
659
+ lastUsedAt: record.lastUsedAt,
660
+ closed: record.closed === true,
661
+ ...record.acpx?.config_options !== void 0 ? { configOptions: structuredClone(record.acpx.config_options) } : {}
662
+ }
663
+ };
664
+ }
665
+ function statusSummary(record) {
666
+ return [
667
+ `session=${record.acpxRecordId}`,
668
+ `backendSessionId=${record.acpSessionId}`,
669
+ record.agentSessionId ? `agentSessionId=${record.agentSessionId}` : null,
670
+ record.pid != null ? `pid=${record.pid}` : null,
671
+ record.closed ? "closed" : "open"
672
+ ].filter(Boolean).join(" ");
673
+ }
674
+ function buildModelsField(record) {
675
+ const { available_models: available = [], current_model_id: currentModelId } = record.acpx ?? {};
676
+ if (available.length === 0 && currentModelId === void 0) return {};
677
+ return { models: {
678
+ ...currentModelId !== void 0 ? { currentModelId } : {},
679
+ availableModelIds: [...available],
680
+ ...buildModelNamesField(record.acpx)
681
+ } };
682
+ }
683
+ function buildModelNamesField(state) {
684
+ const models = advertisedModelState(state);
685
+ return models && (models.configId || state?.available_model_names !== void 0) ? { availableModels: models.availableModels } : {};
686
+ }
687
+ function tokenUsageToBreakdown(usage) {
688
+ if (!usage) return;
689
+ const breakdown = {};
690
+ assignUsageBreakdownField(breakdown, "inputTokens", usage.input_tokens);
691
+ assignUsageBreakdownField(breakdown, "outputTokens", usage.output_tokens);
692
+ assignUsageBreakdownField(breakdown, "cachedReadTokens", usage.cache_read_input_tokens);
693
+ assignUsageBreakdownField(breakdown, "cachedWriteTokens", usage.cache_creation_input_tokens);
694
+ assignUsageBreakdownField(breakdown, "thoughtTokens", usage.thought_tokens);
695
+ assignUsageBreakdownField(breakdown, "totalTokens", usage.total_tokens);
696
+ return Object.keys(breakdown).length > 0 ? breakdown : void 0;
697
+ }
698
+ function assignUsageBreakdownField(breakdown, key, value) {
699
+ if (value !== void 0) breakdown[key] = value;
700
+ }
701
+ function buildUsageField(record) {
702
+ const cumulative = tokenUsageToBreakdown(record.cumulative_token_usage);
703
+ const perRequestEntries = Object.entries(record.request_token_usage ?? {}).map(([id, value]) => [id, tokenUsageToBreakdown(value)]).filter((entry) => entry[1] !== void 0);
704
+ const perRequest = perRequestEntries.length > 0 ? Object.fromEntries(perRequestEntries) : void 0;
705
+ const cost = record.cumulative_cost;
706
+ const usage = {
707
+ ...cumulative ? { cumulative } : {},
708
+ ...cost ? { cost } : {},
709
+ ...perRequest ? { perRequest } : {}
710
+ };
711
+ return Object.keys(usage).length > 0 ? { usage } : {};
712
+ }
713
+ function buildAvailableCommandsField(record) {
714
+ const commands = record.acpx?.available_commands;
715
+ if (!commands || commands.length === 0) return {};
716
+ const availableCommands = commands.map((command) => runtimeAvailableCommand(command)).filter((command) => command !== void 0);
717
+ return availableCommands.length > 0 ? { availableCommands } : {};
718
+ }
719
+ function runtimeAvailableCommand(command) {
720
+ if (typeof command === "string") {
721
+ const name = command.trim();
722
+ return name ? { name } : void 0;
723
+ }
724
+ const record = commandRecord(command);
725
+ if (!record) return;
726
+ const name = asOptionalString(record.name);
727
+ if (!name) return;
728
+ const runtimeCommand = { name };
729
+ const description = asOptionalString(record.description);
730
+ if (description) runtimeCommand.description = description;
731
+ if (typeof record.has_input === "boolean") runtimeCommand.hasInput = record.has_input;
732
+ return runtimeCommand;
733
+ }
734
+ function commandRecord(value) {
735
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
736
+ return value;
737
+ }
738
+ //#endregion
739
+ //#region src/runtime/engine/turn.ts
517
740
  function createDeferred() {
518
741
  let resolve;
519
742
  let reject;
@@ -526,22 +749,6 @@ function createDeferred() {
526
749
  reject
527
750
  };
528
751
  }
529
- async function settleAttempt(run) {
530
- try {
531
- return {
532
- ok: true,
533
- value: await run()
534
- };
535
- } catch (error) {
536
- return {
537
- ok: false,
538
- error
539
- };
540
- }
541
- }
542
- function firstFailedAttempt(attempts) {
543
- return attempts.find((attempt) => !attempt.ok);
544
- }
545
752
  var AsyncEventQueue = class {
546
753
  items = [];
547
754
  waits = [];
@@ -578,17 +785,6 @@ var AsyncEventQueue = class {
578
785
  }
579
786
  }
580
787
  };
581
- function isoNow() {
582
- return (/* @__PURE__ */ new Date()).toISOString();
583
- }
584
- function isUnsupportedSessionCloseError(error) {
585
- const acp = extractAcpError(error);
586
- if (!acp) return false;
587
- if (acp.code === -32601 || acp.code === -32602) return true;
588
- if (acp.code !== -32603 || !acp.data || typeof acp.data !== "object") return false;
589
- const details = acp.data.details;
590
- return typeof details === "string" && details.toLowerCase().includes("invalid params");
591
- }
592
788
  function toPromptInput(text, attachments) {
593
789
  if (!attachments || attachments.length === 0) return text;
594
790
  const blocks = [];
@@ -617,34 +813,6 @@ function toPromptInput(text, attachments) {
617
813
  }
618
814
  return blocks.length > 0 ? blocks : textPrompt(text);
619
815
  }
620
- function createInitialRecord(params) {
621
- const now = isoNow();
622
- return {
623
- schema: "acpx.session.v1",
624
- acpxRecordId: params.recordId,
625
- acpSessionId: params.sessionId,
626
- agentSessionId: params.agentSessionId,
627
- agentCommand: params.agentCommand,
628
- agentArgv: params.agentArgv,
629
- cwd: params.cwd,
630
- name: params.sessionName,
631
- createdAt: now,
632
- lastUsedAt: now,
633
- lastSeq: 0,
634
- eventLog: defaultSessionEventLog(params.recordId),
635
- closed: false,
636
- closedAt: void 0,
637
- ...createSessionConversation(now),
638
- acpx: {}
639
- };
640
- }
641
- function createRecordId(sessionKey, mode) {
642
- if (mode === "persistent") return sessionKey;
643
- return `${sessionKey}:oneshot:${randomUUID()}`;
644
- }
645
- function resumePolicyForSessionMode(mode) {
646
- return mode === "persistent" ? "same-session-only" : "allow-new";
647
- }
648
816
  function legacyTerminalEventFromTurnResult(result) {
649
817
  if (result.status === "failed") return {
650
818
  type: "error",
@@ -659,82 +827,41 @@ function legacyTerminalEventFromTurnResult(result) {
659
827
  ...result._meta === void 0 ? {} : { _meta: result._meta }
660
828
  };
661
829
  }
662
- function statusSummary(record) {
663
- return [
664
- `session=${record.acpxRecordId}`,
665
- `backendSessionId=${record.acpSessionId}`,
666
- record.agentSessionId ? `agentSessionId=${record.agentSessionId}` : null,
667
- record.pid != null ? `pid=${record.pid}` : null,
668
- record.closed ? "closed" : "open"
669
- ].filter(Boolean).join(" ");
670
- }
671
- function buildModelsField(record) {
672
- const available = record.acpx?.available_models;
673
- const currentModelId = record.acpx?.current_model_id;
674
- if (!available || available.length === 0) return currentModelId === void 0 ? {} : { models: {
675
- currentModelId,
676
- availableModelIds: []
677
- } };
678
- return { models: {
679
- ...currentModelId !== void 0 ? { currentModelId } : {},
680
- availableModelIds: [...available]
681
- } };
682
- }
683
- function tokenUsageToBreakdown(usage) {
684
- if (!usage) return;
685
- const breakdown = {};
686
- assignUsageBreakdownField(breakdown, "inputTokens", usage.input_tokens);
687
- assignUsageBreakdownField(breakdown, "outputTokens", usage.output_tokens);
688
- assignUsageBreakdownField(breakdown, "cachedReadTokens", usage.cache_read_input_tokens);
689
- assignUsageBreakdownField(breakdown, "cachedWriteTokens", usage.cache_creation_input_tokens);
690
- assignUsageBreakdownField(breakdown, "thoughtTokens", usage.thought_tokens);
691
- assignUsageBreakdownField(breakdown, "totalTokens", usage.total_tokens);
692
- return Object.keys(breakdown).length > 0 ? breakdown : void 0;
693
- }
694
- function assignUsageBreakdownField(breakdown, key, value) {
695
- if (value !== void 0) breakdown[key] = value;
830
+ //#endregion
831
+ //#region src/runtime/engine/manager.ts
832
+ async function settleAttempt(run) {
833
+ try {
834
+ return {
835
+ ok: true,
836
+ value: await run()
837
+ };
838
+ } catch (error) {
839
+ return {
840
+ ok: false,
841
+ error
842
+ };
843
+ }
696
844
  }
697
- function buildUsageField(record) {
698
- const cumulative = tokenUsageToBreakdown(record.cumulative_token_usage);
699
- const perRequestEntries = Object.entries(record.request_token_usage ?? {}).map(([id, value]) => [id, tokenUsageToBreakdown(value)]).filter((entry) => entry[1] !== void 0);
700
- const perRequest = perRequestEntries.length > 0 ? Object.fromEntries(perRequestEntries) : void 0;
701
- const cost = record.cumulative_cost;
702
- const usage = {
703
- ...cumulative ? { cumulative } : {},
704
- ...cost ? { cost } : {},
705
- ...perRequest ? { perRequest } : {}
706
- };
707
- return Object.keys(usage).length > 0 ? { usage } : {};
845
+ function firstFailedAttempt(attempts) {
846
+ return attempts.find((attempt) => !attempt.ok);
708
847
  }
709
- function buildAvailableCommandsField(record) {
710
- const commands = record.acpx?.available_commands;
711
- if (!commands || commands.length === 0) return {};
712
- const availableCommands = commands.map((command) => runtimeAvailableCommand(command)).filter((command) => command !== void 0);
713
- return availableCommands.length > 0 ? { availableCommands } : {};
848
+ function isoNow() {
849
+ return (/* @__PURE__ */ new Date()).toISOString();
714
850
  }
715
- function runtimeAvailableCommand(command) {
716
- if (typeof command === "string") {
717
- const name = command.trim();
718
- return name ? { name } : void 0;
719
- }
720
- const record = commandRecord(command);
721
- if (!record) return;
722
- const name = trimmedField(record.name);
723
- if (!name) return;
724
- const runtimeCommand = { name };
725
- const description = trimmedField(record.description);
726
- if (description) runtimeCommand.description = description;
727
- if (typeof record.has_input === "boolean") runtimeCommand.hasInput = record.has_input;
728
- return runtimeCommand;
851
+ function isUnsupportedSessionCloseError(error) {
852
+ const acp = extractAcpError(error);
853
+ if (!acp) return false;
854
+ if (acp.code === -32601 || acp.code === -32602) return true;
855
+ if (acp.code !== -32603 || !acp.data || typeof acp.data !== "object") return false;
856
+ const details = acp.data.details;
857
+ return typeof details === "string" && details.toLowerCase().includes("invalid params");
729
858
  }
730
- function commandRecord(value) {
731
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
732
- return value;
859
+ function createRecordId(sessionKey, mode) {
860
+ if (mode === "persistent") return sessionKey;
861
+ return `${sessionKey}:oneshot:${randomUUID()}`;
733
862
  }
734
- function trimmedField(value) {
735
- if (typeof value !== "string") return;
736
- const trimmed = value.trim();
737
- return trimmed ? trimmed : void 0;
863
+ function resumePolicyForSessionMode(mode) {
864
+ return mode === "persistent" ? "same-session-only" : "allow-new";
738
865
  }
739
866
  function advertisedConfigOptionIds(record) {
740
867
  const configOptions = record.acpx?.config_options;
@@ -780,20 +907,117 @@ var AcpRuntimeManager = class {
780
907
  deps;
781
908
  activeControllers = /* @__PURE__ */ new Map();
782
909
  retainedSessionOwners = /* @__PURE__ */ new Map();
910
+ retiringSessionOwners = /* @__PURE__ */ new Set();
783
911
  pendingOneShotRecordIds = /* @__PURE__ */ new Map();
784
912
  ensureSessionLocks = /* @__PURE__ */ new Map();
785
913
  runtimeOperationLocks = /* @__PURE__ */ new Map();
914
+ sessionTasks = /* @__PURE__ */ new Map();
786
915
  closingActiveRecords = /* @__PURE__ */ new Set();
916
+ liveClients = /* @__PURE__ */ new Set();
917
+ pendingTasks = /* @__PURE__ */ new Set();
918
+ shuttingDown = false;
919
+ shutdownTask;
787
920
  constructor(options, deps = {}) {
788
921
  this.options = options;
789
922
  this.deps = deps;
790
923
  }
791
924
  createClient(options) {
925
+ this.assertOpen();
792
926
  const clientOptions = {
793
927
  ...options,
794
- agentProcessEnv: this.options.agentProcessEnv
928
+ agentProcessEnv: this.options.agentProcessEnv,
929
+ processLifecycle: {
930
+ ...options.processLifecycle,
931
+ onBeforeSpawn: async (launch) => {
932
+ this.assertOpen();
933
+ await options.processLifecycle?.onBeforeSpawn?.(launch);
934
+ this.assertOpen();
935
+ },
936
+ onSpawned: async (process) => {
937
+ this.assertOpen();
938
+ await options.processLifecycle?.onSpawned?.(process);
939
+ this.assertOpen();
940
+ }
941
+ }
942
+ };
943
+ const client = this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
944
+ this.liveClients.add(client);
945
+ return client;
946
+ }
947
+ async probe() {
948
+ let client;
949
+ try {
950
+ return await probeRuntime(this.options, { clientFactory: (options) => client = this.createClient(options) });
951
+ } finally {
952
+ if (client) this.liveClients.delete(client);
953
+ }
954
+ }
955
+ assertOpen() {
956
+ if (this.shuttingDown) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
957
+ }
958
+ shutdown() {
959
+ if (!this.shutdownTask) {
960
+ this.shuttingDown = true;
961
+ this.shutdownTask = this.shutdownOwnedClients();
962
+ }
963
+ return this.shutdownTask;
964
+ }
965
+ async shutdownOwnedClients() {
966
+ const closed = await Promise.allSettled([...this.liveClients].map(async (client) => {
967
+ client.requestCancelActivePrompt().catch(() => {});
968
+ await this.closeClient(client);
969
+ }));
970
+ await Promise.allSettled([
971
+ ...this.ensureSessionLocks.values(),
972
+ ...this.runtimeOperationLocks.values(),
973
+ ...this.pendingTasks
974
+ ]);
975
+ await Promise.all([...this.retainedSessionOwners.values()].map(async (owner) => {
976
+ this.removeRetainedSessionOwner(owner);
977
+ await this.stopSessionOwner(owner);
978
+ }));
979
+ const errors = [];
980
+ for (const result of closed) if (result.status === "rejected") errors.push(result.reason);
981
+ if (errors.length) throw new AggregateError(errors, "ACP runtime shutdown failed.");
982
+ this.retiringSessionOwners.clear();
983
+ }
984
+ trackTask(task) {
985
+ this.pendingTasks.add(task);
986
+ const remove = () => {
987
+ this.pendingTasks.delete(task);
795
988
  };
796
- return this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
989
+ task.then(remove, remove);
990
+ return task;
991
+ }
992
+ queueSessionTask(recordId, run, cancel) {
993
+ const tasks = this.sessionTasks.get(recordId) ?? /* @__PURE__ */ new Set();
994
+ this.sessionTasks.set(recordId, tasks);
995
+ const previous = [...tasks].at(-1)?.completion;
996
+ const result = (async () => {
997
+ await previous?.catch(() => {});
998
+ return await run();
999
+ })();
1000
+ const task = {
1001
+ completion: result.then(() => {}),
1002
+ cancel
1003
+ };
1004
+ task.completion.catch(() => {});
1005
+ tasks.add(task);
1006
+ const remove = () => {
1007
+ tasks.delete(task);
1008
+ if (tasks.size === 0) this.sessionTasks.delete(recordId);
1009
+ };
1010
+ result.then(remove, remove);
1011
+ return this.trackTask(result);
1012
+ }
1013
+ async cancelSessionTasks(recordId) {
1014
+ const tasks = [...this.sessionTasks.get(recordId) ?? []];
1015
+ await Promise.all(tasks.flatMap((task) => task.cancel ? [task.cancel()] : []));
1016
+ return tasks;
1017
+ }
1018
+ async closeClient(client) {
1019
+ await client.close();
1020
+ this.liveClients.delete(client);
797
1021
  }
798
1022
  createSessionOwner(input) {
799
1023
  const owner = {
@@ -900,12 +1124,27 @@ var AcpRuntimeManager = class {
900
1124
  }
901
1125
  async stopSessionOwner(owner) {
902
1126
  await this.flushSessionOwner(owner).catch(() => {});
903
- await owner.client.close().catch(() => {});
1127
+ await this.closeClient(owner.client).catch(() => {
1128
+ this.retiringSessionOwners.add(owner);
1129
+ });
904
1130
  await owner.projection?.checkpoint.flush().catch(() => {});
905
1131
  try {
906
1132
  owner.client.clearEventHandlers();
907
1133
  } catch {}
908
1134
  }
1135
+ async retrySessionCleanup(recordId) {
1136
+ for (const owner of this.retiringSessionOwners) if (owner.recordId === recordId) {
1137
+ owner.retirement ??= this.finalizeSessionConnection({
1138
+ client: owner.client,
1139
+ owner
1140
+ }).then(() => {
1141
+ this.retiringSessionOwners.delete(owner);
1142
+ }).finally(() => {
1143
+ owner.retirement = void 0;
1144
+ });
1145
+ await owner.retirement;
1146
+ }
1147
+ }
909
1148
  async refreshClosedState(record) {
910
1149
  if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
911
1150
  const latest = await this.options.sessionStore.load(record.acpxRecordId).catch(() => void 0);
@@ -933,10 +1172,23 @@ var AcpRuntimeManager = class {
933
1172
  return true;
934
1173
  }
935
1174
  canRetainPersistentSessionOwner(owner, record) {
936
- return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
1175
+ return !this.shuttingDown && owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
1176
+ }
1177
+ resolveMcpServers(session) {
1178
+ const servers = this.options.mcpServers;
1179
+ const { sessionKey, cwd, agentCommand, agentArgv } = session;
1180
+ return [...typeof servers === "function" ? servers({
1181
+ sessionKey,
1182
+ cwd,
1183
+ agentCommand,
1184
+ agentArgv: agentArgv ? [...agentArgv] : void 0
1185
+ }) : servers ?? []];
937
1186
  }
938
- async withRuntimeControlSession(record, sessionMode, run, replacingConfigOption) {
1187
+ async withRuntimeControlSession(record, sessionMode, run, replacingConfigOption, authority) {
1188
+ await this.retrySessionCleanup(record.acpxRecordId);
1189
+ assertControlAuthority(authority);
939
1190
  const owner = await this.readRetainedSessionOwner(record, { consume: false });
1191
+ assertControlAuthority(authority);
940
1192
  if (owner) {
941
1193
  const ownedRecord = owner.projection?.record ?? record;
942
1194
  try {
@@ -954,34 +1206,43 @@ var AcpRuntimeManager = class {
954
1206
  await this.flushSessionOwner(owner);
955
1207
  }
956
1208
  }
957
- const result = await withConnectedSession({
958
- sessionRecordId: record.acpxRecordId,
959
- loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
960
- saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
961
- createClient: (options) => this.createClient({
962
- ...options,
963
- processLifecycle: this.options.processLifecycle,
964
- processLaunchScope: {
965
- kind: "runtime-session",
1209
+ let controlClient;
1210
+ try {
1211
+ const result = await withConnectedSession({
1212
+ sessionRecordId: record.acpxRecordId,
1213
+ loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
1214
+ saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
1215
+ createClient: (options) => controlClient = this.createClient({
1216
+ ...options,
1217
+ processLifecycle: this.options.processLifecycle,
1218
+ processLaunchScope: {
1219
+ kind: "runtime-session",
1220
+ sessionKey: record.name ?? record.acpxRecordId
1221
+ }
1222
+ }),
1223
+ mcpServers: this.resolveMcpServers({
1224
+ ...record,
966
1225
  sessionKey: record.name ?? record.acpxRecordId
967
- }
968
- }),
969
- mcpServers: [...this.options.mcpServers ?? []],
970
- permissionMode: this.options.permissionMode,
971
- nonInteractivePermissions: this.options.nonInteractivePermissions,
972
- permissionPolicy: this.options.permissionPolicy,
973
- onPermissionRequest: this.options.onPermissionRequest,
974
- elicitationModes: this.options.elicitationModes,
975
- verbose: this.options.verbose,
976
- timeoutMs: this.options.timeoutMs,
977
- resumePolicy: resumePolicyForSessionMode(sessionMode),
978
- replacingConfigOption,
979
- run
980
- });
981
- return {
982
- value: result.value,
983
- record: result.record
984
- };
1226
+ }),
1227
+ permissionMode: this.options.permissionMode,
1228
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
1229
+ permissionPolicy: this.options.permissionPolicy,
1230
+ onPermissionRequest: this.options.onPermissionRequest,
1231
+ elicitationModes: this.options.elicitationModes,
1232
+ verbose: this.options.verbose,
1233
+ timeoutMs: this.options.timeoutMs,
1234
+ resumePolicy: resumePolicyForSessionMode(sessionMode),
1235
+ replacingConfigOption,
1236
+ authority,
1237
+ run
1238
+ });
1239
+ return {
1240
+ value: result.value,
1241
+ record: result.record
1242
+ };
1243
+ } finally {
1244
+ if (controlClient) this.liveClients.delete(controlClient);
1245
+ }
985
1246
  }
986
1247
  refreshOwnedRecordLifecycle(owner, record) {
987
1248
  record.lastUsedAt = isoNow();
@@ -998,7 +1259,9 @@ var AcpRuntimeManager = class {
998
1259
  const key = `${input.mode}\0${input.sessionKey}`;
999
1260
  return await this.withManagerLock(this.ensureSessionLocks, key, run);
1000
1261
  }
1001
- async withManagerLock(locks, key, run) {
1262
+ async withManagerLock(locks, key, run, authority) {
1263
+ this.assertOpen();
1264
+ assertControlAuthority(authority);
1002
1265
  const previous = locks.get(key) ?? Promise.resolve();
1003
1266
  let release;
1004
1267
  const gate = new Promise((resolve) => {
@@ -1008,6 +1271,8 @@ var AcpRuntimeManager = class {
1008
1271
  locks.set(key, tail);
1009
1272
  await previous;
1010
1273
  try {
1274
+ this.assertOpen();
1275
+ assertControlAuthority(authority);
1011
1276
  return await run();
1012
1277
  } finally {
1013
1278
  release();
@@ -1030,7 +1295,8 @@ var AcpRuntimeManager = class {
1030
1295
  async loadExistingRuntimeSession(input) {
1031
1296
  const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
1032
1297
  if (!existingRecordId) return;
1033
- let record = await this.options.sessionStore.load(existingRecordId);
1298
+ await this.retrySessionCleanup(existingRecordId);
1299
+ let record = await this.findSession(existingRecordId);
1034
1300
  if (!record) return;
1035
1301
  const owner = this.retainedSessionOwners.get(record.acpxRecordId);
1036
1302
  if (owner) {
@@ -1042,6 +1308,9 @@ var AcpRuntimeManager = class {
1042
1308
  owner
1043
1309
  };
1044
1310
  }
1311
+ async findSession(sessionKey) {
1312
+ return await this.options.sessionStore.load(sessionKey);
1313
+ }
1045
1314
  canReuseRuntimeSession(input, agent, existing) {
1046
1315
  if (!shouldReuseExistingRecord(existing.record, {
1047
1316
  cwd: agent.cwd,
@@ -1068,7 +1337,10 @@ var AcpRuntimeManager = class {
1068
1337
  agentCommand,
1069
1338
  agentArgv,
1070
1339
  cwd,
1071
- mcpServers: [...this.options.mcpServers ?? []],
1340
+ mcpServers: this.resolveMcpServers({
1341
+ ...agent,
1342
+ sessionKey: input.sessionKey
1343
+ }),
1072
1344
  permissionMode: this.options.permissionMode,
1073
1345
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1074
1346
  permissionPolicy: this.options.permissionPolicy,
@@ -1090,6 +1362,7 @@ var AcpRuntimeManager = class {
1090
1362
  let retained = false;
1091
1363
  try {
1092
1364
  await client.start();
1365
+ this.assertOpen();
1093
1366
  const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
1094
1367
  const record = await this.prepareInitialRuntimeRecord({
1095
1368
  input,
@@ -1107,15 +1380,15 @@ var AcpRuntimeManager = class {
1107
1380
  if (!retained) {
1108
1381
  owner.recordId = void 0;
1109
1382
  client.clearEventHandlers();
1110
- await client.close();
1383
+ await this.closeClient(client);
1111
1384
  }
1112
1385
  }
1113
1386
  }
1114
1387
  async prepareInitialRuntimeRecord(params) {
1115
1388
  const { input, client, owner, agentCommand, agentArgv, cwd, session } = params;
1116
- const record = createInitialRecord({
1389
+ const record = createInitialSessionRecord({
1117
1390
  recordId: createRecordId(input.sessionKey, input.mode),
1118
- sessionName: input.sessionKey,
1391
+ name: input.sessionKey,
1119
1392
  sessionId: session.sessionId,
1120
1393
  agentCommand,
1121
1394
  agentArgv,
@@ -1135,9 +1408,7 @@ var AcpRuntimeManager = class {
1135
1408
  agentCommand,
1136
1409
  timeoutMs: this.options.timeoutMs
1137
1410
  });
1138
- applyConfigOptionsToRecord(record, modelApplication.response);
1139
- syncAdvertisedModelState(record, modelApplication.response ? modelStateFromConfigOptions(modelApplication.response.configOptions) : session.sessionResult.models);
1140
- if (modelApplication.applied) setCurrentModelId(record, currentModelIdFromSetModelResponse(modelApplication.response, input.sessionOptions?.model));
1411
+ applyInitialModelSelection(record, session.sessionResult.models, input.sessionOptions?.model, modelApplication);
1141
1412
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
1142
1413
  persistSessionOptions(record, input.sessionOptions);
1143
1414
  return record;
@@ -1145,6 +1416,7 @@ var AcpRuntimeManager = class {
1145
1416
  async retainInitializedSessionOwner(owner, record) {
1146
1417
  owner.recordId = record.acpxRecordId;
1147
1418
  await owner.projection?.checkpoint.checkpoint();
1419
+ this.assertOpen();
1148
1420
  const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
1149
1421
  this.retainedSessionOwners.set(record.acpxRecordId, owner);
1150
1422
  if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
@@ -1154,11 +1426,12 @@ var AcpRuntimeManager = class {
1154
1426
  }
1155
1427
  }
1156
1428
  startTurn(input) {
1429
+ this.assertOpen();
1157
1430
  let promptInput;
1158
1431
  try {
1159
1432
  promptInput = toPromptInput(input.text, input.attachments);
1160
1433
  } catch (error) {
1161
- this.closeRetainedOneShotHandle(input.handle).catch(() => {});
1434
+ this.trackTask(this.closeRetainedOneShotHandle(input.handle).catch(() => {}));
1162
1435
  throw error;
1163
1436
  }
1164
1437
  const queue = new AsyncEventQueue();
@@ -1194,28 +1467,8 @@ var AcpRuntimeManager = class {
1194
1467
  const abortHandler = () => {
1195
1468
  requestCancel();
1196
1469
  };
1197
- if (input.signal) {
1198
- if (input.signal.aborted) {
1199
- promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
1200
- closeStream();
1201
- this.closeRetainedOneShotHandle(input.handle).catch(() => {}).then(() => {
1202
- settleResult({
1203
- status: "cancelled",
1204
- stopReason: "cancelled"
1205
- });
1206
- });
1207
- return {
1208
- requestId: input.requestId,
1209
- promptStarted: promptStarted.promise,
1210
- events: queue.iterate(),
1211
- result: result.promise,
1212
- cancel: async () => {},
1213
- closeStream: async () => {}
1214
- };
1215
- }
1216
- input.signal.addEventListener("abort", abortHandler, { once: true });
1217
- }
1218
- this.runRuntimeTurnTask({
1470
+ if (input.signal && !input.signal.aborted) input.signal.addEventListener("abort", abortHandler, { once: true });
1471
+ this.queueSessionTask(input.handle.acpxRecordId ?? input.handle.sessionKey, async () => this.runRuntimeTurnTask({
1219
1472
  input,
1220
1473
  promptInput,
1221
1474
  queue,
@@ -1224,7 +1477,7 @@ var AcpRuntimeManager = class {
1224
1477
  state,
1225
1478
  settleResult,
1226
1479
  abortHandler
1227
- });
1480
+ }), requestCancel);
1228
1481
  return {
1229
1482
  requestId: input.requestId,
1230
1483
  promptStarted: promptStarted.promise,
@@ -1246,30 +1499,39 @@ var AcpRuntimeManager = class {
1246
1499
  let turn;
1247
1500
  let terminalResult;
1248
1501
  try {
1249
- turn = await this.prepareRuntimeTurn(task);
1250
- const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
1251
- await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
1252
- if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
1253
- status: "cancelled",
1254
- stopReason: "cancelled"
1255
- };
1256
- else {
1257
- await this.applyPendingRuntimeTurnCancel(task, turn);
1258
- const response = await this.runRuntimePrompt(task, turn, sessionId);
1259
- await this.saveCompletedRuntimeTurn(turn, response.stopReason);
1502
+ if (this.cancelRuntimeTurnBeforePrompt(task)) {
1503
+ await this.closeRetainedOneShotHandle(task.input.handle);
1260
1504
  terminalResult = {
1261
- status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1262
- ...response.stopReason ? { stopReason: response.stopReason } : {},
1263
- ...response._meta === void 0 ? {} : { _meta: response._meta }
1505
+ status: "cancelled",
1506
+ stopReason: "cancelled"
1507
+ };
1508
+ } else {
1509
+ turn = await this.prepareRuntimeTurn(task);
1510
+ const { sessionId } = await this.connectRuntimeTurn(task, turn);
1511
+ this.assertOpen();
1512
+ await this.resolveRuntimeTurnReady(task, turn);
1513
+ if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
1514
+ status: "cancelled",
1515
+ stopReason: "cancelled"
1264
1516
  };
1517
+ else {
1518
+ await this.applyPendingRuntimeTurnCancel(task, turn);
1519
+ const response = await this.runRuntimePrompt(task, turn, sessionId);
1520
+ await this.saveCompletedRuntimeTurn(turn);
1521
+ terminalResult = {
1522
+ status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1523
+ ...response.stopReason ? { stopReason: response.stopReason } : {},
1524
+ ...response._meta === void 0 ? {} : { _meta: response._meta }
1525
+ };
1526
+ }
1265
1527
  }
1266
1528
  } catch (error) {
1267
1529
  terminalResult = this.failRuntimeTurn(task, error);
1268
1530
  }
1269
- try {
1270
- await this.finalizeRuntimeTurn(task, turn);
1271
- } catch (error) {
1272
- terminalResult = this.failRuntimeTurn(task, error);
1531
+ const finalization = await settleAttempt(async () => this.finalizeRuntimeTurn(task, turn));
1532
+ if (!finalization.ok) {
1533
+ task.settleResult(this.failRuntimeTurn(task, finalization.error));
1534
+ throw finalization.error;
1273
1535
  }
1274
1536
  task.settleResult(terminalResult);
1275
1537
  }
@@ -1283,7 +1545,8 @@ var AcpRuntimeManager = class {
1283
1545
  conversation: turn.conversation,
1284
1546
  promptMessageId: turn.promptMessageId,
1285
1547
  onPromptRequestWritten: () => task.promptStarted.resolve(),
1286
- onElicitation: task.input.onElicitation
1548
+ onElicitation: task.input.onElicitation,
1549
+ onPermissionRequest: task.input.onPermissionRequest
1287
1550
  });
1288
1551
  } finally {
1289
1552
  turn.client.endPromptElicitation?.(sessionId);
@@ -1291,7 +1554,10 @@ var AcpRuntimeManager = class {
1291
1554
  }
1292
1555
  async prepareRuntimeTurn(task) {
1293
1556
  const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
1294
- return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.prepareRuntimeTurnWithOwnership(task));
1557
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => {
1558
+ await this.retrySessionCleanup(recordId);
1559
+ return await this.prepareRuntimeTurnWithOwnership(task);
1560
+ });
1295
1561
  }
1296
1562
  async prepareRuntimeTurnWithOwnership(task) {
1297
1563
  const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
@@ -1398,7 +1664,10 @@ var AcpRuntimeManager = class {
1398
1664
  agentCommand: record.agentCommand,
1399
1665
  agentArgv: record.agentArgv,
1400
1666
  cwd: record.cwd,
1401
- mcpServers: [...this.options.mcpServers ?? []],
1667
+ mcpServers: this.resolveMcpServers({
1668
+ ...record,
1669
+ sessionKey: record.name ?? record.acpxRecordId
1670
+ }),
1402
1671
  permissionMode: this.options.permissionMode,
1403
1672
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1404
1673
  permissionPolicy: this.options.permissionPolicy,
@@ -1426,24 +1695,24 @@ var AcpRuntimeManager = class {
1426
1695
  return {
1427
1696
  hasActivePrompt: () => turn.client.hasActivePrompt(),
1428
1697
  requestCancelActivePrompt: async () => await this.requestRuntimeTurnCancel(task, turn),
1429
- setSessionMode: async (modeId) => {
1698
+ setSessionMode: async (modeId, authority) => {
1430
1699
  await this.waitForRuntimeControlSession(task, turn);
1431
- await turn.client.setSessionMode(turn.activeSessionId, modeId);
1700
+ await turn.client.setSessionMode(turn.activeSessionId, modeId, authority);
1432
1701
  const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
1433
1702
  nextState.desired_mode_id = modeId;
1434
1703
  turn.acpxState = nextState;
1435
1704
  },
1436
- setSessionModel: async (modelId) => {
1705
+ setSessionModel: async (modelId, authority) => {
1437
1706
  await this.waitForRuntimeControlSession(task, turn);
1438
1707
  const models = advertisedModelState(turn.acpxState);
1439
- const response = await turn.client.setSessionModel(turn.activeSessionId, modelId, models);
1708
+ const response = await turn.client.setSessionModel(turn.activeSessionId, modelId, models, authority);
1440
1709
  turn.acpxState = applyModelSelection(turn.acpxState, modelId, response);
1441
1710
  return response;
1442
1711
  },
1443
- setSessionConfigOption: async (configId, value) => {
1444
- return (await task.state.activeController.setResolvedSessionConfigOption(configId, value)).response;
1712
+ setSessionConfigOption: async (configId, value, authority) => {
1713
+ return (await task.state.activeController.setResolvedSessionConfigOption(configId, value, authority)).response;
1445
1714
  },
1446
- setResolvedSessionConfigOption: async (configId, value) => await this.setRuntimeResolvedSessionConfigOption(task, turn, configId, value)
1715
+ setResolvedSessionConfigOption: async (configId, value, authority) => await this.setRuntimeResolvedSessionConfigOption(task, turn, configId, value, authority)
1447
1716
  };
1448
1717
  }
1449
1718
  async waitForRuntimeControlSession(task, turn) {
@@ -1456,14 +1725,14 @@ var AcpRuntimeManager = class {
1456
1725
  task.state.pendingCancel = true;
1457
1726
  return true;
1458
1727
  }
1459
- async setRuntimeResolvedSessionConfigOption(task, turn, configId, value) {
1728
+ async setRuntimeResolvedSessionConfigOption(task, turn, configId, value, authority) {
1460
1729
  await this.waitForRuntimeControlSession(task, turn);
1461
1730
  const resolvedConfigId = resolveSupportedConfigOptionId({
1462
1731
  ...turn.record,
1463
1732
  acpx: turn.acpxState ?? void 0
1464
1733
  }, configId);
1465
1734
  const modelConfigId = advertisedModelState(turn.acpxState)?.configId;
1466
- const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value);
1735
+ const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value, authority);
1467
1736
  turn.acpxState = applyConfigOptionSelection(turn.acpxState, resolvedConfigId, value, response, modelConfigId);
1468
1737
  return {
1469
1738
  configId: resolvedConfigId,
@@ -1506,7 +1775,7 @@ var AcpRuntimeManager = class {
1506
1775
  const controller = task.state.activeController;
1507
1776
  if (controller) this.activeControllers.set(turn.record.acpxRecordId, controller);
1508
1777
  }
1509
- async resolveRuntimeTurnReady(task, turn, resumed, loadError) {
1778
+ async resolveRuntimeTurnReady(task, turn) {
1510
1779
  task.sessionReady.resolve();
1511
1780
  turn.record.lastRequestId = task.input.requestId;
1512
1781
  turn.record.lastPromptAt = isoNow();
@@ -1514,14 +1783,6 @@ var AcpRuntimeManager = class {
1514
1783
  turn.record.closedAt = void 0;
1515
1784
  turn.record.lastUsedAt = isoNow();
1516
1785
  await turn.liveCheckpoint.checkpoint();
1517
- this.emitRuntimeTurnLoadStatus(task, resumed, loadError);
1518
- }
1519
- emitRuntimeTurnLoadStatus(task, resumed, loadError) {
1520
- if (!resumed && !loadError) return;
1521
- this.emitRuntimeTurnEvent(task, {
1522
- type: "status",
1523
- text: loadError ? `session reconnect fallback: ${loadError}` : "session resumed"
1524
- });
1525
1786
  }
1526
1787
  cancelRuntimeTurnBeforePrompt(task) {
1527
1788
  if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
@@ -1535,7 +1796,7 @@ var AcpRuntimeManager = class {
1535
1796
  if (cancelled) task.state.pendingCancel = false;
1536
1797
  return cancelled;
1537
1798
  }
1538
- async saveCompletedRuntimeTurn(turn, _stopReason) {
1799
+ async saveCompletedRuntimeTurn(turn) {
1539
1800
  turn.record.acpSessionId = turn.activeSessionId;
1540
1801
  reconcileAgentSessionId(turn.record, turn.record.agentSessionId);
1541
1802
  turn.record.protocolVersion = turn.client.initializeResult?.protocolVersion;
@@ -1583,8 +1844,11 @@ var AcpRuntimeManager = class {
1583
1844
  async closeRuntimeTurnClient(turn, pooled) {
1584
1845
  if (!turn || pooled) return;
1585
1846
  turn.owner.activeTurn = void 0;
1586
- const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => turn.client.close())]);
1587
- if (failure) throw failure.error;
1847
+ const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => this.closeClient(turn.client))]);
1848
+ if (failure) {
1849
+ this.retiringSessionOwners.add(turn.owner);
1850
+ throw failure.error;
1851
+ }
1588
1852
  }
1589
1853
  cleanupRuntimeTurn(task, turn) {
1590
1854
  if (turn) {
@@ -1615,51 +1879,62 @@ var AcpRuntimeManager = class {
1615
1879
  yield* turn.events;
1616
1880
  yield legacyTerminalEventFromTurnResult(await turn.result);
1617
1881
  }
1618
- async getStatus(handle) {
1882
+ getStatus(handle) {
1883
+ this.assertOpen();
1884
+ return this.trackTask(this.readStatus(handle));
1885
+ }
1886
+ async readStatus(handle) {
1619
1887
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1620
1888
  const owner = this.retainedSessionOwners.get(recordId);
1621
1889
  if (owner) await this.flushSessionOwner(owner);
1622
- const record = await this.requireRecord(recordId);
1623
- return {
1624
- summary: statusSummary(record),
1625
- acpxRecordId: record.acpxRecordId,
1626
- backendSessionId: record.acpSessionId,
1627
- agentSessionId: record.agentSessionId,
1628
- ...buildModelsField(record),
1629
- ...buildUsageField(record),
1630
- ...buildAvailableCommandsField(record),
1631
- details: {
1632
- cwd: record.cwd,
1633
- lastUsedAt: record.lastUsedAt,
1634
- closed: record.closed === true,
1635
- ...record.acpx?.config_options !== void 0 ? { configOptions: structuredClone(record.acpx.config_options) } : {}
1636
- }
1637
- };
1890
+ return runtimeStatusFromRecord(await this.requireRecord(recordId));
1638
1891
  }
1639
- async setMode(handle, mode, sessionMode = "persistent") {
1892
+ async setMode(handle, mode, sessionMode = "persistent", authority) {
1640
1893
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1641
- await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode));
1894
+ await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode, authority), authority);
1642
1895
  }
1643
- async setModeWithOwnership(handle, mode, sessionMode) {
1896
+ async setModeWithOwnership(handle, mode, sessionMode, authority) {
1644
1897
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1898
+ assertControlAuthority(authority);
1645
1899
  const controller = this.activeControllers.get(record.acpxRecordId);
1646
1900
  let targetRecord = record;
1647
- if (controller) await controller.setSessionMode(mode);
1901
+ if (controller) await controller.setSessionMode(mode, authority);
1648
1902
  else targetRecord = (await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId }) => {
1649
- await client.setSessionMode(sessionId, mode);
1650
- })).record;
1903
+ await client.setSessionMode(sessionId, mode, authority);
1904
+ }, void 0, authority)).record;
1651
1905
  setDesiredModeId(targetRecord, mode);
1652
1906
  await this.options.sessionStore.save(targetRecord);
1653
1907
  }
1654
- async setConfigOption(handle, key, value, sessionMode = "persistent") {
1908
+ async setModel(handle, model, sessionMode = "persistent", authority) {
1909
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1910
+ await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModelWithOwnership(handle, model, sessionMode, authority), authority);
1911
+ }
1912
+ async setModelWithOwnership(handle, model, sessionMode, authority) {
1913
+ const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1914
+ assertControlAuthority(authority);
1915
+ const controller = this.activeControllers.get(record.acpxRecordId);
1916
+ if (controller) {
1917
+ const response = await controller.setSessionModel(model, authority);
1918
+ record.acpx = applyModelSelection(record.acpx, model, response);
1919
+ await this.options.sessionStore.save(record);
1920
+ return;
1921
+ }
1922
+ const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1923
+ const response = await client.setSessionModel(sessionId, model, advertisedModelState(connectedRecord.acpx), authority);
1924
+ connectedRecord.acpx = applyModelSelection(connectedRecord.acpx, model, response);
1925
+ }, { key: "model" }, authority);
1926
+ await this.options.sessionStore.save(result.record);
1927
+ }
1928
+ async setConfigOption(handle, key, value, sessionMode = "persistent", authority) {
1655
1929
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1656
- return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode));
1930
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode, authority), authority);
1657
1931
  }
1658
- async setConfigOptionWithOwnership(handle, key, value, sessionMode) {
1932
+ async setConfigOptionWithOwnership(handle, key, value, sessionMode, authority) {
1659
1933
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1934
+ assertControlAuthority(authority);
1660
1935
  const controller = this.activeControllers.get(record.acpxRecordId);
1661
1936
  if (controller) {
1662
- const { configId, response } = await controller.setResolvedSessionConfigOption(key, value);
1937
+ const { configId, response } = await controller.setResolvedSessionConfigOption(key, value, authority);
1663
1938
  record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response);
1664
1939
  await this.options.sessionStore.save(record);
1665
1940
  return response;
@@ -1667,25 +1942,44 @@ var AcpRuntimeManager = class {
1667
1942
  const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1668
1943
  const configId = resolveSupportedConfigOptionId(connectedRecord, key);
1669
1944
  const modelConfigId = advertisedModelState(connectedRecord.acpx)?.configId;
1670
- const response = await client.setSessionConfigOption(sessionId, configId, value);
1945
+ const response = await client.setSessionConfigOption(sessionId, configId, value, authority);
1671
1946
  connectedRecord.acpx = applyConfigOptionSelection(connectedRecord.acpx, configId, value, response, modelConfigId);
1672
1947
  return response;
1673
1948
  }, {
1674
1949
  key,
1675
1950
  resolve: (connectedRecord) => resolveSupportedConfigOptionId(connectedRecord, key)
1676
- });
1951
+ }, authority);
1677
1952
  await this.options.sessionStore.save(result.record);
1678
1953
  return result.value;
1679
1954
  }
1680
1955
  async cancel(handle) {
1681
1956
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
1682
1957
  }
1683
- async close(handle, options = {}) {
1958
+ prepareFreshSession(handle) {
1959
+ this.assertOpen();
1960
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1961
+ const cancelled = this.cancelSessionTasks(recordId);
1962
+ cancelled.catch(() => {});
1963
+ return this.queueSessionTask(recordId, async () => {
1964
+ const tasks = await cancelled;
1965
+ await Promise.all(tasks.map((task) => task.completion));
1966
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.closeRuntimeSession(handle, "prepare-fresh"));
1967
+ });
1968
+ }
1969
+ close(handle, options = {}) {
1970
+ this.assertOpen();
1971
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1972
+ return this.trackTask(this.withManagerLock(this.runtimeOperationLocks, recordId, async () => {
1973
+ return await this.closeRuntimeSession(handle, options.discardPersistentState === true ? "discard" : "release");
1974
+ }));
1975
+ }
1976
+ async closeRuntimeSession(handle, intent) {
1684
1977
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1978
+ await this.retrySessionCleanup(recordId);
1685
1979
  const record = await this.resolveRuntimeRecordForClose(recordId);
1686
1980
  this.markActiveRuntimeRecordClosing(record);
1687
- await this.cancel(handle);
1688
- await this.closeRuntimeRecordOwnership(record, options.discardPersistentState === true);
1981
+ if (intent !== "prepare-fresh") await this.cancelSessionTasks(recordId);
1982
+ await this.closeRuntimeRecordOwnership(record, intent);
1689
1983
  record.closed = true;
1690
1984
  record.closedAt = isoNow();
1691
1985
  await this.options.sessionStore.save(record);
@@ -1698,14 +1992,20 @@ var AcpRuntimeManager = class {
1698
1992
  markActiveRuntimeRecordClosing(record) {
1699
1993
  if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
1700
1994
  }
1701
- async closeRuntimeRecordOwnership(record, discardPersistentState) {
1702
- if (discardPersistentState) {
1703
- await this.closeBackendSession(record);
1704
- record.acpx = {
1705
- ...record.acpx,
1706
- reset_on_next_ensure: true
1707
- };
1995
+ async closeRuntimeRecordOwnership(record, intent) {
1996
+ if (intent === "discard") await this.closeBackendSession(record);
1997
+ else if (intent === "prepare-fresh") {
1998
+ const owner = this.retainedSessionOwners.get(record.acpxRecordId);
1999
+ if (owner) {
2000
+ this.removeRetainedSessionOwner(owner);
2001
+ this.retiringSessionOwners.add(owner);
2002
+ }
2003
+ await this.retrySessionCleanup(record.acpxRecordId);
1708
2004
  } else await this.closeRetainedSessionOwner(record.acpxRecordId);
2005
+ if (intent !== "release") record.acpx = {
2006
+ ...record.acpx,
2007
+ reset_on_next_ensure: true
2008
+ };
1709
2009
  }
1710
2010
  async closeBackendSession(record) {
1711
2011
  const connection = await this.acquireBackendCloseConnection(record);
@@ -1714,16 +2014,16 @@ var AcpRuntimeManager = class {
1714
2014
  } catch (error) {
1715
2015
  this.handleBackendSessionCloseError(record, error);
1716
2016
  } finally {
1717
- await this.finalizeBackendCloseConnection(connection);
2017
+ await this.finalizeSessionConnection(connection);
1718
2018
  }
1719
2019
  }
1720
- async finalizeBackendCloseConnection(connection) {
2020
+ async finalizeSessionConnection(connection) {
1721
2021
  const failure = firstFailedAttempt([
1722
2022
  await settleAttempt(async () => {
1723
2023
  if (connection.owner) await this.flushSessionOwner(connection.owner);
1724
2024
  }),
1725
2025
  await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
1726
- await settleAttempt(async () => connection.client.close())
2026
+ await settleAttempt(async () => this.closeClient(connection.client))
1727
2027
  ]);
1728
2028
  if (failure) throw failure.error;
1729
2029
  }
@@ -1733,23 +2033,7 @@ var AcpRuntimeManager = class {
1733
2033
  client: owner.client,
1734
2034
  owner
1735
2035
  };
1736
- return { client: this.createClient({
1737
- agentCommand: record.agentCommand,
1738
- agentArgv: record.agentArgv,
1739
- cwd: record.cwd,
1740
- mcpServers: [...this.options.mcpServers ?? []],
1741
- permissionMode: this.options.permissionMode,
1742
- nonInteractivePermissions: this.options.nonInteractivePermissions,
1743
- permissionPolicy: this.options.permissionPolicy,
1744
- onPermissionRequest: this.options.onPermissionRequest,
1745
- elicitationModes: this.options.elicitationModes,
1746
- processLifecycle: this.options.processLifecycle,
1747
- processLaunchScope: {
1748
- kind: "runtime-session",
1749
- sessionKey: record.name ?? record.acpxRecordId
1750
- },
1751
- verbose: this.options.verbose
1752
- }) };
2036
+ return { client: this.createTurnClient(record) };
1753
2037
  }
1754
2038
  async requestBackendSessionClose(record, connection) {
1755
2039
  if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
@@ -1784,7 +2068,10 @@ var FileSessionStore = class {
1784
2068
  return path.join(this.sessionDir, `${safeSessionId(sessionId)}.json`);
1785
2069
  }
1786
2070
  async ensureDir() {
1787
- await fs.mkdir(this.sessionDir, { recursive: true });
2071
+ await fs.mkdir(this.sessionDir, {
2072
+ recursive: true,
2073
+ mode: 448
2074
+ });
1788
2075
  }
1789
2076
  async load(sessionId) {
1790
2077
  await this.ensureDir();
@@ -1804,14 +2091,9 @@ var FileSessionStore = class {
1804
2091
  return parseSessionRecord(parsed) ?? void 0;
1805
2092
  }
1806
2093
  async save(record) {
1807
- await this.ensureDir();
1808
2094
  const persisted = serializeSessionRecordForDisk(record);
1809
2095
  assertPersistedKeyPolicy(persisted);
1810
- const file = this.filePath(record.acpxRecordId);
1811
- const tempFile = createAtomicWriteTempPath(file);
1812
- const payload = JSON.stringify(persisted, null, 2);
1813
- await fs.writeFile(tempFile, `${payload}\n`, "utf8");
1814
- await fs.rename(tempFile, file);
2096
+ await writePrivateJsonFile(this.filePath(record.acpxRecordId), persisted);
1815
2097
  }
1816
2098
  };
1817
2099
  function createFileSessionStore(options) {
@@ -1856,125 +2138,246 @@ function writeHandleState(handle, state) {
1856
2138
  handle.agentSessionId = state.agentSessionId;
1857
2139
  }
1858
2140
  //#endregion
1859
- //#region src/runtime/public/probe.ts
1860
- function isPrimitiveDetail(value) {
1861
- return value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol";
2141
+ //#region src/runtime/shared.ts
2142
+ const SHARED_BACKEND = "acpx-shared";
2143
+ function invalidOption(message) {
2144
+ throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", message);
1862
2145
  }
1863
- function formatFunctionDetail(value) {
1864
- return value.name ? `[Function ${value.name}]` : "[Function]";
2146
+ function sharedHandle(record) {
2147
+ return {
2148
+ backend: SHARED_BACKEND,
2149
+ sessionKey: record.name ?? "",
2150
+ runtimeSessionName: record.acpxRecordId,
2151
+ acpxRecordId: record.acpxRecordId,
2152
+ backendSessionId: record.acpSessionId,
2153
+ agentSessionId: record.agentSessionId,
2154
+ cwd: record.cwd
2155
+ };
1865
2156
  }
1866
- function serializeRuntimeDetail(value) {
1867
- const seen = /* @__PURE__ */ new WeakSet();
1868
- return JSON.stringify(value, (_key, nested) => {
1869
- if (nested instanceof Error) return nested.message || nested.name;
1870
- if (nested && typeof nested === "object") {
1871
- if (seen.has(nested)) return "[Circular]";
1872
- seen.add(nested);
2157
+ function sharedRecordId(handle) {
2158
+ if (handle.backend !== SHARED_BACKEND || !handle.acpxRecordId) return invalidOption("A shared runtime requires a handle from a shared session.");
2159
+ return handle.acpxRecordId;
2160
+ }
2161
+ function turnFailure(error) {
2162
+ const normalized = normalizeOutputError(error, {
2163
+ defaultCode: "RUNTIME",
2164
+ origin: "queue"
2165
+ });
2166
+ if (normalized.detailCode === "QUEUE_REQUEST_CANCELLED") return {
2167
+ status: "cancelled",
2168
+ stopReason: "cancelled"
2169
+ };
2170
+ return {
2171
+ status: "failed",
2172
+ error: {
2173
+ message: normalized.message,
2174
+ code: normalized.code,
2175
+ detailCode: normalized.detailCode,
2176
+ retryable: normalized.retryable
1873
2177
  }
1874
- return nested;
1875
- }) ?? "undefined";
2178
+ };
1876
2179
  }
1877
- function formatRuntimeDetail(value) {
1878
- if (value instanceof Error) return value.message || value.name;
1879
- if (typeof value === "string") return value;
1880
- if (isPrimitiveDetail(value)) return String(value);
1881
- if (typeof value === "function") return formatFunctionDetail(value);
1882
- try {
1883
- return serializeRuntimeDetail(value);
1884
- } catch {
1885
- return "unserializable object";
2180
+ /** A client of the CLI's local session owner. Shutdown detaches this client. */
2181
+ var SharedAcpRuntime = class {
2182
+ registry;
2183
+ options;
2184
+ disconnect = new AbortController();
2185
+ pending = /* @__PURE__ */ new Set();
2186
+ constructor(options) {
2187
+ for (const name of [
2188
+ "sessionStore",
2189
+ "agentProcessEnv",
2190
+ "mcpServers",
2191
+ "processLifecycle",
2192
+ "onPermissionRequest",
2193
+ "elicitationModes"
2194
+ ]) if (Object.hasOwn(options, name)) invalidOption(`${name} belongs to the in-process runtime and is not supported by shared sessions.`);
2195
+ this.options = {
2196
+ ...options,
2197
+ cwd: path.resolve(options.cwd),
2198
+ authCredentials: options.authCredentials ? { ...options.authCredentials } : void 0
2199
+ };
2200
+ this.registry = options.agentRegistry ?? createAgentRegistry();
1886
2201
  }
1887
- }
1888
- function normalizeRuntimeDetails(details) {
1889
- return details?.map((detail) => formatRuntimeDetail(detail));
1890
- }
1891
- async function probeRuntime(options, deps = {}) {
1892
- const agentName = options.probeAgent?.trim() || "codex";
1893
- const agentCommand = normalizeAgentCommandInput(options.agentRegistry.resolve(agentName));
1894
- const client = createProbeClient(options, agentName, agentCommand, deps);
1895
- try {
1896
- await client.start();
2202
+ assertOpen() {
2203
+ if (this.disconnect.signal.aborted) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "Shared ACP runtime is shut down.");
2204
+ }
2205
+ track(task) {
2206
+ this.pending.add(task);
2207
+ task.then(() => this.pending.delete(task), () => this.pending.delete(task));
2208
+ return task;
2209
+ }
2210
+ sessionOptions(input) {
2211
+ if (!input.agent.trim()) invalidOption("ACP agent id is required.");
1897
2212
  return {
1898
- ok: true,
1899
- message: "embedded ACP runtime ready",
1900
- details: [
1901
- `agent=${agentName}`,
1902
- `command=${agentCommand.agentCommand}`,
1903
- `cwd=${options.cwd}`,
1904
- ...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
1905
- ]
2213
+ ...normalizeAgentCommandInput(this.registry.resolve(input.agent)),
2214
+ cwd: path.resolve(input.cwd ?? this.options.cwd),
2215
+ name: input.sessionKey.trim() || void 0
1906
2216
  };
1907
- } catch (error) {
2217
+ }
2218
+ async findSession(input) {
2219
+ this.assertOpen();
2220
+ const record = await findSession(this.sessionOptions(input));
2221
+ return record ? sharedHandle(record) : void 0;
2222
+ }
2223
+ ensureSession(input) {
2224
+ this.assertOpen();
2225
+ if (input.mode !== "persistent") invalidOption("Shared sessions require persistent mode; use the in-process runtime for oneshot sessions.");
2226
+ const session = this.sessionOptions(input);
2227
+ return this.track(ensureSession({
2228
+ ...this.options,
2229
+ ...session,
2230
+ walkBoundary: session.cwd,
2231
+ sessionOptions: input.sessionOptions,
2232
+ resumeSessionId: input.resumeSessionId,
2233
+ handleProcessInterrupts: false,
2234
+ signal: this.disconnect.signal
2235
+ }).then(({ record }) => {
2236
+ this.assertOpen();
2237
+ return sharedHandle(record);
2238
+ }));
2239
+ }
2240
+ startTurn(input) {
2241
+ this.assertOpen();
2242
+ if (input.mode !== "prompt" || input.onPermissionRequest || input.onElicitation) invalidOption("Shared turns support prompt mode and the client's static permission policy; in-process callbacks and steering are unsupported.");
2243
+ if (!input.requestId.trim()) invalidOption("A shared turn requires a requestId.");
2244
+ const sessionId = sharedRecordId(input.handle);
2245
+ const prompt = toPromptInput(input.text, input.attachments);
2246
+ const events = new AsyncEventQueue();
2247
+ const promptStarted = createDeferred();
2248
+ promptStarted.promise.catch(() => {});
2249
+ let accepted = false;
2250
+ let settled = false;
2251
+ let cancelRequested = input.signal?.aborted === true;
2252
+ let cancellation;
2253
+ const cancel = async () => {
2254
+ cancelRequested = true;
2255
+ if (!accepted || settled) return;
2256
+ cancellation ??= tryCancelOnRunningOwner({
2257
+ sessionId,
2258
+ targetRequestId: input.requestId
2259
+ }).then(() => {});
2260
+ await cancellation;
2261
+ };
2262
+ const onAbort = () => {
2263
+ cancel().catch(() => {});
2264
+ };
2265
+ input.signal?.addEventListener("abort", onAbort, { once: true });
2266
+ const run = async () => {
2267
+ if (cancelRequested) return {
2268
+ status: "cancelled",
2269
+ stopReason: "cancelled"
2270
+ };
2271
+ if ((await resolveSessionRecord(sessionId)).closed) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Shared session is closed; ensure it again before submitting a turn.");
2272
+ if (cancelRequested) return {
2273
+ status: "cancelled",
2274
+ stopReason: "cancelled"
2275
+ };
2276
+ const result = await sendSession({
2277
+ ...this.options,
2278
+ sessionId,
2279
+ requestId: input.requestId,
2280
+ prompt: typeof prompt === "string" ? textPrompt(prompt) : prompt,
2281
+ resumePolicy: "same-session-only",
2282
+ requireSharedRuntime: true,
2283
+ signal: this.disconnect.signal,
2284
+ timeoutMs: input.timeoutMs ?? this.options.timeoutMs,
2285
+ queueOwnerArgs: [fileURLToPath(import.meta.resolve("acpx/dist/cli.js")), "__queue-owner"],
2286
+ onQueueAccepted: () => {
2287
+ accepted = true;
2288
+ if (cancelRequested) cancel().catch(() => {});
2289
+ },
2290
+ onPromptStarted: () => promptStarted.resolve(),
2291
+ outputFormatter: {
2292
+ ...DISCARD_OUTPUT_FORMATTER,
2293
+ onAcpMessage: (message) => {
2294
+ const event = parsePromptEventLine(JSON.stringify(message));
2295
+ if (event) events.push(event);
2296
+ }
2297
+ },
2298
+ errorEmissionPolicy: { queueErrorAlreadyEmitted: false },
2299
+ waitForCompletion: true
2300
+ });
2301
+ if ("queued" in result) throw new AcpRuntimeError("ACP_TURN_FAILED", "Shared turn ended without a completion result.");
2302
+ return {
2303
+ status: result.stopReason === "cancelled" ? "cancelled" : "completed",
2304
+ stopReason: result.stopReason,
2305
+ _meta: result._meta
2306
+ };
2307
+ };
2308
+ const result = this.track(run().catch(turnFailure).finally(() => {
2309
+ settled = true;
2310
+ promptStarted.reject(new AcpRuntimeError("ACP_TURN_FAILED", "Turn settled before its prompt was written."));
2311
+ input.signal?.removeEventListener("abort", onAbort);
2312
+ events.close();
2313
+ }));
1908
2314
  return {
1909
- ok: false,
1910
- message: "embedded ACP runtime probe failed",
1911
- details: [
1912
- `agent=${agentName}`,
1913
- `command=${agentCommand.agentCommand}`,
1914
- `cwd=${options.cwd}`,
1915
- formatRuntimeDetail(error)
1916
- ]
2315
+ requestId: input.requestId,
2316
+ promptStarted: promptStarted.promise,
2317
+ events: events.iterate(),
2318
+ result,
2319
+ cancel,
2320
+ closeStream: async () => {
2321
+ events.clear();
2322
+ events.close();
2323
+ }
1917
2324
  };
1918
- } finally {
1919
- await client.close().catch(() => {});
1920
2325
  }
1921
- }
1922
- function createProbeClient(options, agentName, agentCommand, deps) {
1923
- const clientOptions = {
1924
- ...agentCommand,
1925
- cwd: options.cwd,
1926
- agentProcessEnv: options.agentProcessEnv,
1927
- mcpServers: [...options.mcpServers ?? []],
1928
- permissionMode: options.permissionMode,
1929
- nonInteractivePermissions: options.nonInteractivePermissions,
1930
- permissionPolicy: options.permissionPolicy,
1931
- processLifecycle: options.processLifecycle,
1932
- processLaunchScope: {
1933
- kind: "runtime-probe",
1934
- agent: agentName
1935
- },
1936
- verbose: options.verbose
1937
- };
1938
- return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
2326
+ async *runTurn(input) {
2327
+ const turn = this.startTurn(input);
2328
+ yield* turn.events;
2329
+ yield legacyTerminalEventFromTurnResult(await turn.result);
2330
+ }
2331
+ async getStatus(input) {
2332
+ this.assertOpen();
2333
+ return runtimeStatusFromRecord(await resolveSessionRecord(sharedRecordId(input.handle)));
2334
+ }
2335
+ watchSession(input) {
2336
+ this.assertOpen();
2337
+ const signal = input.signal ? AbortSignal.any([input.signal, this.disconnect.signal]) : this.disconnect.signal;
2338
+ const recordId = sharedRecordId(input.handle);
2339
+ const record = readSessionRecord(recordId).then((value) => {
2340
+ if (!value || value.acpxRecordId !== recordId) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Shared session record is unavailable.");
2341
+ return value;
2342
+ });
2343
+ record.catch(() => {});
2344
+ return watchSession({
2345
+ record,
2346
+ cursor: input.cursor,
2347
+ signal
2348
+ });
2349
+ }
2350
+ async cancel(input) {
2351
+ this.assertOpen();
2352
+ await this.track(cancelSessionPrompt({ sessionId: sharedRecordId(input.handle) }));
2353
+ }
2354
+ async close(input) {
2355
+ this.assertOpen();
2356
+ await this.track(closeSession(sharedRecordId(input.handle)));
2357
+ }
2358
+ async shutdown() {
2359
+ this.disconnect.abort(new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "Shared client detached; submitted work may still be running."));
2360
+ await Promise.allSettled(this.pending);
2361
+ }
2362
+ };
2363
+ function createSharedAcpRuntime(options) {
2364
+ return new SharedAcpRuntime(options);
1939
2365
  }
1940
2366
  //#endregion
1941
2367
  //#region src/runtime.ts
1942
2368
  const ACPX_BACKEND_ID = "acpx";
1943
2369
  const ACPX_CAPABILITIES = { controls: [
1944
2370
  "session/set_mode",
2371
+ "session/set_model",
1945
2372
  "session/set_config_option",
1946
2373
  "session/status"
1947
2374
  ] };
1948
- function createAgentRegistry(params) {
1949
- const overrides = normalizeRegistryOverrides(params?.overrides);
1950
- return {
1951
- resolve(agentName) {
1952
- const normalizedAgentName = normalizeAgentName(agentName);
1953
- return overrides[normalizedAgentName] ?? overrides[resolveCanonicalAgentName(agentName)] ?? resolveAgentArgv(agentName) ?? resolveAgentCommand(agentName);
1954
- },
1955
- list() {
1956
- return listBuiltInAgents(overrides);
1957
- }
1958
- };
1959
- }
1960
- function normalizeRegistryOverrides(values) {
1961
- const normalized = {};
1962
- for (const [name, value] of Object.entries(values ?? {})) {
1963
- const normalizedName = normalizeAgentName(name);
1964
- if (!normalizedName) continue;
1965
- const normalizedValue = normalizeRegistryOverride(value);
1966
- if (normalizedValue) normalized[normalizedName] = normalizedValue;
1967
- }
1968
- return normalized;
1969
- }
1970
- function normalizeRegistryOverride(value) {
1971
- if (typeof value === "string") return value.trim() || void 0;
1972
- return value.length > 0 && value[0]?.length ? [...value] : void 0;
1973
- }
1974
2375
  var AcpxRuntime = class {
1975
2376
  options;
1976
2377
  testOptions;
1977
2378
  healthy = false;
2379
+ shutdownTask;
2380
+ probeTasks = /* @__PURE__ */ new Set();
1978
2381
  manager = null;
1979
2382
  managerPromise = null;
1980
2383
  constructor(options, testOptions) {
@@ -2002,11 +2405,27 @@ var AcpxRuntime = class {
2002
2405
  details: normalizeRuntimeDetails(report.details)
2003
2406
  };
2004
2407
  }
2005
- async ensureSession(input) {
2408
+ resolveSessionIdentity(input) {
2006
2409
  const sessionName = input.sessionKey.trim();
2007
2410
  if (!sessionName) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
2008
2411
  const agent = input.agent.trim();
2009
2412
  if (!agent) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP agent id is required.");
2413
+ return {
2414
+ sessionName,
2415
+ agent
2416
+ };
2417
+ }
2418
+ async findSession(input) {
2419
+ const { sessionName, agent } = this.resolveSessionIdentity(input);
2420
+ const record = await (await this.getManager()).findSession(sessionName);
2421
+ return record ? this.createSessionHandle({
2422
+ sessionKey: input.sessionKey,
2423
+ agent,
2424
+ mode: "persistent"
2425
+ }, record) : void 0;
2426
+ }
2427
+ async ensureSession(input) {
2428
+ const { sessionName, agent } = this.resolveSessionIdentity(input);
2010
2429
  const record = await (await this.getManager()).ensureSession({
2011
2430
  sessionKey: sessionName,
2012
2431
  agent,
@@ -2015,6 +2434,12 @@ var AcpxRuntime = class {
2015
2434
  resumeSessionId: input.resumeSessionId,
2016
2435
  sessionOptions: input.sessionOptions
2017
2436
  });
2437
+ return this.createSessionHandle({
2438
+ ...input,
2439
+ agent
2440
+ }, record);
2441
+ }
2442
+ createSessionHandle(input, record) {
2018
2443
  const handle = {
2019
2444
  sessionKey: input.sessionKey,
2020
2445
  backend: ACPX_BACKEND_ID,
@@ -2025,8 +2450,8 @@ var AcpxRuntime = class {
2025
2450
  agentSessionId: record.agentSessionId
2026
2451
  };
2027
2452
  writeHandleState(handle, {
2028
- name: sessionName,
2029
- agent,
2453
+ name: input.sessionKey.trim(),
2454
+ agent: input.agent,
2030
2455
  cwd: record.cwd,
2031
2456
  mode: input.mode,
2032
2457
  acpxRecordId: record.acpxRecordId,
@@ -2046,7 +2471,8 @@ var AcpxRuntime = class {
2046
2471
  requestId: input.requestId,
2047
2472
  timeoutMs: input.timeoutMs,
2048
2473
  signal: input.signal,
2049
- onElicitation: input.onElicitation
2474
+ onElicitation: input.onElicitation,
2475
+ onPermissionRequest: input.onPermissionRequest
2050
2476
  }));
2051
2477
  return {
2052
2478
  requestId: input.requestId,
@@ -2078,7 +2504,8 @@ var AcpxRuntime = class {
2078
2504
  requestId: input.requestId,
2079
2505
  timeoutMs: input.timeoutMs,
2080
2506
  signal: input.signal,
2081
- onElicitation: input.onElicitation
2507
+ onElicitation: input.onElicitation,
2508
+ onPermissionRequest: input.onPermissionRequest
2082
2509
  });
2083
2510
  }
2084
2511
  async getCapabilities(input) {
@@ -2098,21 +2525,40 @@ var AcpxRuntime = class {
2098
2525
  }
2099
2526
  async setMode(input) {
2100
2527
  const { handle, state } = this.resolveManagerHandle(input.handle);
2101
- await (await this.getManager()).setMode(handle, input.mode, state.mode);
2528
+ await (await this.getManager()).setMode(handle, input.mode, state.mode, input);
2529
+ }
2530
+ async setModel(input) {
2531
+ const { handle, state } = this.resolveManagerHandle(input.handle);
2532
+ await (await this.getManager()).setModel(handle, input.model, state.mode, input);
2102
2533
  }
2103
2534
  async setConfigOption(input) {
2104
2535
  const { handle, state } = this.resolveManagerHandle(input.handle);
2105
- return await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
2536
+ return await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode, input);
2106
2537
  }
2107
2538
  async cancel(input) {
2108
2539
  const { handle } = this.resolveManagerHandle(input.handle);
2109
2540
  await (await this.getManager()).cancel(handle);
2110
2541
  }
2542
+ async prepareFreshSession(input) {
2543
+ const { handle } = this.resolveManagerHandle(input.handle);
2544
+ await (await this.getManager()).prepareFreshSession(handle);
2545
+ }
2111
2546
  async close(input) {
2112
2547
  const { handle } = this.resolveManagerHandle(input.handle);
2113
2548
  await (await this.getManager()).close(handle, { discardPersistentState: input.discardPersistentState });
2114
2549
  }
2550
+ shutdown() {
2551
+ if (!this.shutdownTask) {
2552
+ this.healthy = false;
2553
+ const managerShutdown = this.managerPromise?.then((manager) => manager.shutdown());
2554
+ this.shutdownTask = Promise.allSettled([managerShutdown, ...this.probeTasks]).then(([manager]) => {
2555
+ if (manager.status === "rejected") throw manager.reason;
2556
+ });
2557
+ }
2558
+ return this.shutdownTask;
2559
+ }
2115
2560
  async getManager() {
2561
+ if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
2116
2562
  if (this.manager) return this.manager;
2117
2563
  if (!this.managerPromise) this.managerPromise = Promise.resolve(this.testOptions?.managerFactory?.(this.options) ?? new AcpRuntimeManager(this.options)).then((manager) => {
2118
2564
  this.manager = manager;
@@ -2121,7 +2567,16 @@ var AcpxRuntime = class {
2121
2567
  return await this.managerPromise;
2122
2568
  }
2123
2569
  async runProbe() {
2124
- return await (this.testOptions?.probeRunner?.(this.options) ?? probeRuntime(this.options));
2570
+ if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
2571
+ const probe = this.testOptions?.probeRunner?.(this.options) ?? this.getManager().then((manager) => manager.probe());
2572
+ this.probeTasks.add(probe);
2573
+ try {
2574
+ const report = await probe;
2575
+ if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
2576
+ return report;
2577
+ } finally {
2578
+ this.probeTasks.delete(probe);
2579
+ }
2125
2580
  }
2126
2581
  resolveManagerHandle(handle) {
2127
2582
  const state = this.resolveHandleState(handle);
@@ -2161,6 +2616,6 @@ function createRuntimeStore(options) {
2161
2616
  return createFileSessionStore(options);
2162
2617
  }
2163
2618
  //#endregion
2164
- export { ACPX_BACKEND_ID, AcpRuntimeError, AcpxRuntime, DEFAULT_AGENT_NAME, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, REQUESTED_MODEL_UNSUPPORTED_REASONS, RequestedModelUnsupportedError, createAcpRuntime, createAgentRegistry, createFileSessionStore, createRuntimeStore, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, isAcpRuntimeError, isRequestedModelUnsupportedError };
2619
+ export { ACPX_BACKEND_ID, AcpRuntimeError, AcpxRuntime, DEFAULT_AGENT_NAME, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, REQUESTED_MODEL_UNSUPPORTED_REASONS, RequestedModelUnsupportedError, SharedAcpRuntime, createAcpRuntime, createAgentRegistry, createFileSessionStore, createRuntimeStore, createSharedAcpRuntime, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, isAcpRuntimeError, isRequestedModelUnsupportedError };
2165
2620
 
2166
2621
  //# sourceMappingURL=runtime.js.map