acpx 0.15.0 → 0.16.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 (39) hide show
  1. package/dist/agent-registry-DU6mhZBL.js +785 -0
  2. package/dist/agent-registry-DU6mhZBL.js.map +1 -0
  3. package/dist/agent-registry.d.ts +70 -0
  4. package/dist/agent-registry.d.ts.map +1 -0
  5. package/dist/agent-registry.js +2 -0
  6. package/dist/{cli-Cen2Rb6S.js → cli-BkYIxj_N.js} +6 -14
  7. package/dist/cli-BkYIxj_N.js.map +1 -0
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +137 -333
  11. package/dist/cli.js.map +1 -1
  12. package/dist/{client-Cvz6msGc.d.ts → client-5levcBIs.d.ts} +11 -9
  13. package/dist/client-5levcBIs.d.ts.map +1 -0
  14. package/dist/{flows-D-F3Y2o9.js → flows-Bxs4udrs.js} +205 -277
  15. package/dist/flows-Bxs4udrs.js.map +1 -0
  16. package/dist/flows.d.ts +6 -17
  17. package/dist/flows.d.ts.map +1 -1
  18. package/dist/flows.js +1 -1
  19. package/dist/{flags-CRh8BJre.js → invocation-options-BY4lxkn3.js} +82 -58
  20. package/dist/invocation-options-BY4lxkn3.js.map +1 -0
  21. package/dist/{live-checkpoint-CYRs-D9t.js → live-checkpoint-BEfxBKCh.js} +1033 -1437
  22. package/dist/live-checkpoint-BEfxBKCh.js.map +1 -0
  23. package/dist/{output-CIVecnHU.js → output-uo5a27KB.js} +487 -490
  24. package/dist/output-uo5a27KB.js.map +1 -0
  25. package/dist/runtime.d.ts +105 -22
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +582 -366
  28. package/dist/runtime.js.map +1 -1
  29. package/dist/{session-options-DyIGRNfu.d.ts → session-options-t3d-F4dn.d.ts} +8 -6
  30. package/dist/session-options-t3d-F4dn.d.ts.map +1 -0
  31. package/package.json +27 -25
  32. package/skills/acpx/SKILL.md +19 -17
  33. package/dist/cli-Cen2Rb6S.js.map +0 -1
  34. package/dist/client-Cvz6msGc.d.ts.map +0 -1
  35. package/dist/flags-CRh8BJre.js.map +0 -1
  36. package/dist/flows-D-F3Y2o9.js.map +0 -1
  37. package/dist/live-checkpoint-CYRs-D9t.js.map +0 -1
  38. package/dist/output-CIVecnHU.js.map +0 -1
  39. package/dist/session-options-DyIGRNfu.d.ts.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,4 +1,5 @@
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-CYRs-D9t.js";
1
+ import { A as isRequestedModelUnsupportedError, C as recordSessionUpdate, D as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, Dt as withTimeout, E as AcpClient, L as textPrompt, Mt as extractAcpError, Nt as isAcpResourceNotFoundError, O as REQUESTED_MODEL_UNSUPPORTED_REASONS, S as recordPromptSubmission, T as advertisedModelState, Z as assertPersistedKeyPolicy, _ as reconcileAgentSessionId, a as applyConfigOptionSelection, c as applyModelSelection, f as persistSessionOptions, g as createInitialSessionRecord, gt as writePrivateJsonFile, h as applyLifecycleSnapshotToRecord, i as connectAndLoadSession, jt as normalizeOutputError, k as RequestedModelUnsupportedError, l as applyRequestedModelIfAdvertised, m as applyConversation, n as runPromptTurn, o as applyConfigOptionsToRecord, ot as parseSessionRecord, p as sessionOptionsFromRecord, r as withConnectedSession, s as applyInitialModelSelection, st as serializeSessionRecordForDisk, t as LiveSessionCheckpoint, u as setDesiredModeId, v as cloneSessionAcpxState, w as trimConversationForRuntime, x as recordClientOperation, y as cloneSessionConversation } from "./live-checkpoint-BEfxBKCh.js";
2
+ import { D as normalizeAgentCommandInput, a as createAgentRegistry, i as DEFAULT_AGENT_NAME } from "./agent-registry-DU6mhZBL.js";
2
3
  import path from "node:path";
3
4
  import fs from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
@@ -84,8 +85,7 @@ const STATUS_TEXT_RESOLVERS = {
84
85
  available_commands_update: availableCommandsStatusText,
85
86
  current_mode_update: currentModeStatusText,
86
87
  config_option_update: configOptionStatusText,
87
- session_info_update: sessionInfoStatusText,
88
- plan: planStatusText
88
+ session_info_update: sessionInfoStatusText
89
89
  };
90
90
  function availableCommandsStatusText(payload) {
91
91
  const commands = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
@@ -108,6 +108,51 @@ function planStatusText(payload) {
108
108
  const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
109
109
  return content ? `plan: ${content}` : null;
110
110
  }
111
+ function isPlanEntryStatus(value) {
112
+ return value === "pending" || value === "in_progress" || value === "completed";
113
+ }
114
+ function isPlanEntryPriority(value) {
115
+ return value === "high" || value === "medium" || value === "low";
116
+ }
117
+ function planUpdateEvent(payload) {
118
+ const raw = payload.entries;
119
+ const entries = normalizePlanEntries(raw);
120
+ if (Array.isArray(raw) && raw.length === 0) return {
121
+ type: "status",
122
+ text: "plan updated",
123
+ tag: "plan",
124
+ entries: []
125
+ };
126
+ const text = planStatusText(payload) ?? (entries.length > 0 ? `plan: ${entries[0]?.content}` : null);
127
+ if (!text) return null;
128
+ return {
129
+ type: "status",
130
+ text,
131
+ tag: "plan",
132
+ ...entries.length > 0 ? { entries } : {}
133
+ };
134
+ }
135
+ function normalizePlanEntries(value) {
136
+ if (!Array.isArray(value)) return [];
137
+ const entries = [];
138
+ for (const entry of value) {
139
+ const normalized = normalizePlanEntry(entry);
140
+ if (normalized) entries.push(normalized);
141
+ }
142
+ return entries;
143
+ }
144
+ function normalizePlanEntry(value) {
145
+ if (!isRecord(value)) return;
146
+ const content = asTrimmedString(value.content);
147
+ const status = asTrimmedString(value.status);
148
+ if (!content || !isPlanEntryStatus(status)) return;
149
+ const priority = asTrimmedString(value.priority);
150
+ return {
151
+ content,
152
+ status,
153
+ ...isPlanEntryPriority(priority) ? { priority } : {}
154
+ };
155
+ }
111
156
  /**
112
157
  * Documented allowlist for text_delta.meta origin fields.
113
158
  * Only these keys may be copied from ACP update `_meta`.
@@ -371,7 +416,7 @@ const PROMPT_EVENT_PARSERS = {
371
416
  current_mode_update: (payload) => statusUpdateEvent("current_mode_update", payload),
372
417
  config_option_update: (payload) => statusUpdateEvent("config_option_update", payload),
373
418
  session_info_update: (payload) => statusUpdateEvent("session_info_update", payload),
374
- plan: (payload) => statusUpdateEvent("plan", payload),
419
+ plan: planUpdateEvent,
375
420
  client_operation: clientOperationEvent,
376
421
  update: updateStatusEvent,
377
422
  done: () => null,
@@ -498,6 +543,88 @@ function updateStatusEvent(payload, tag) {
498
543
  } : null;
499
544
  }
500
545
  //#endregion
546
+ //#region src/runtime/public/probe.ts
547
+ function isPrimitiveDetail(value) {
548
+ return value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol";
549
+ }
550
+ function formatFunctionDetail(value) {
551
+ return value.name ? `[Function ${value.name}]` : "[Function]";
552
+ }
553
+ function serializeRuntimeDetail(value) {
554
+ const seen = /* @__PURE__ */ new WeakSet();
555
+ return JSON.stringify(value, (_key, nested) => {
556
+ if (nested instanceof Error) return nested.message || nested.name;
557
+ if (nested && typeof nested === "object") {
558
+ if (seen.has(nested)) return "[Circular]";
559
+ seen.add(nested);
560
+ }
561
+ return nested;
562
+ }) ?? "undefined";
563
+ }
564
+ function formatRuntimeDetail(value) {
565
+ if (value instanceof Error) return value.message || value.name;
566
+ if (typeof value === "string") return value;
567
+ if (isPrimitiveDetail(value)) return String(value);
568
+ if (typeof value === "function") return formatFunctionDetail(value);
569
+ try {
570
+ return serializeRuntimeDetail(value);
571
+ } catch {
572
+ return "unserializable object";
573
+ }
574
+ }
575
+ function normalizeRuntimeDetails(details) {
576
+ return details?.map((detail) => formatRuntimeDetail(detail));
577
+ }
578
+ async function probeRuntime(options, deps = {}) {
579
+ const agentName = options.probeAgent?.trim() || "codex";
580
+ const agentCommand = normalizeAgentCommandInput(options.agentRegistry.resolve(agentName));
581
+ const client = createProbeClient(options, agentName, agentCommand, deps);
582
+ try {
583
+ await client.start();
584
+ return {
585
+ ok: true,
586
+ message: "embedded ACP runtime ready",
587
+ details: [
588
+ `agent=${agentName}`,
589
+ `command=${agentCommand.agentCommand}`,
590
+ `cwd=${options.cwd}`,
591
+ ...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
592
+ ]
593
+ };
594
+ } catch (error) {
595
+ return {
596
+ ok: false,
597
+ message: "embedded ACP runtime probe failed",
598
+ details: [
599
+ `agent=${agentName}`,
600
+ `command=${agentCommand.agentCommand}`,
601
+ `cwd=${options.cwd}`,
602
+ formatRuntimeDetail(error)
603
+ ]
604
+ };
605
+ } finally {
606
+ await client.close().catch(() => {});
607
+ }
608
+ }
609
+ function createProbeClient(options, agentName, agentCommand, deps) {
610
+ const clientOptions = {
611
+ ...agentCommand,
612
+ cwd: options.cwd,
613
+ agentProcessEnv: options.agentProcessEnv,
614
+ mcpServers: typeof options.mcpServers === "function" ? [] : [...options.mcpServers ?? []],
615
+ permissionMode: options.permissionMode,
616
+ nonInteractivePermissions: options.nonInteractivePermissions,
617
+ permissionPolicy: options.permissionPolicy,
618
+ processLifecycle: options.processLifecycle,
619
+ processLaunchScope: {
620
+ kind: "runtime-probe",
621
+ agent: agentName
622
+ },
623
+ verbose: options.verbose
624
+ };
625
+ return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
626
+ }
627
+ //#endregion
501
628
  //#region src/runtime/engine/reuse-policy.ts
502
629
  function shouldReuseExistingRecord(record, params) {
503
630
  if (record.acpx?.reset_on_next_ensure === true) return false;
@@ -513,6 +640,99 @@ function sameArgv(left, right) {
513
640
  return leftValues.length === rightValues.length && leftValues.every((value, index) => value === rightValues[index]);
514
641
  }
515
642
  //#endregion
643
+ //#region src/runtime/engine/status.ts
644
+ function runtimeStatusFromRecord(record) {
645
+ return {
646
+ ...record.lastRequestId ? { lastRequestId: record.lastRequestId } : {},
647
+ summary: statusSummary(record),
648
+ acpxRecordId: record.acpxRecordId,
649
+ backendSessionId: record.acpSessionId,
650
+ agentSessionId: record.agentSessionId,
651
+ ...buildModelsField(record),
652
+ ...buildUsageField(record),
653
+ ...buildAvailableCommandsField(record),
654
+ details: {
655
+ cwd: record.cwd,
656
+ lastUsedAt: record.lastUsedAt,
657
+ closed: record.closed === true,
658
+ ...record.acpx?.config_options !== void 0 ? { configOptions: structuredClone(record.acpx.config_options) } : {}
659
+ }
660
+ };
661
+ }
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_models: available = [], current_model_id: currentModelId } = record.acpx ?? {};
673
+ if (available.length === 0 && currentModelId === void 0) return {};
674
+ return { models: {
675
+ ...currentModelId !== void 0 ? { currentModelId } : {},
676
+ availableModelIds: [...available],
677
+ ...buildModelNamesField(record.acpx)
678
+ } };
679
+ }
680
+ function buildModelNamesField(state) {
681
+ const models = advertisedModelState(state);
682
+ return models && (models.configId || state?.available_model_names !== void 0) ? { availableModels: models.availableModels } : {};
683
+ }
684
+ function tokenUsageToBreakdown(usage) {
685
+ if (!usage) return;
686
+ const breakdown = {};
687
+ assignUsageBreakdownField(breakdown, "inputTokens", usage.input_tokens);
688
+ assignUsageBreakdownField(breakdown, "outputTokens", usage.output_tokens);
689
+ assignUsageBreakdownField(breakdown, "cachedReadTokens", usage.cache_read_input_tokens);
690
+ assignUsageBreakdownField(breakdown, "cachedWriteTokens", usage.cache_creation_input_tokens);
691
+ assignUsageBreakdownField(breakdown, "thoughtTokens", usage.thought_tokens);
692
+ assignUsageBreakdownField(breakdown, "totalTokens", usage.total_tokens);
693
+ return Object.keys(breakdown).length > 0 ? breakdown : void 0;
694
+ }
695
+ function assignUsageBreakdownField(breakdown, key, value) {
696
+ if (value !== void 0) breakdown[key] = value;
697
+ }
698
+ function buildUsageField(record) {
699
+ const cumulative = tokenUsageToBreakdown(record.cumulative_token_usage);
700
+ const perRequestEntries = Object.entries(record.request_token_usage ?? {}).map(([id, value]) => [id, tokenUsageToBreakdown(value)]).filter((entry) => entry[1] !== void 0);
701
+ const perRequest = perRequestEntries.length > 0 ? Object.fromEntries(perRequestEntries) : void 0;
702
+ const cost = record.cumulative_cost;
703
+ const usage = {
704
+ ...cumulative ? { cumulative } : {},
705
+ ...cost ? { cost } : {},
706
+ ...perRequest ? { perRequest } : {}
707
+ };
708
+ return Object.keys(usage).length > 0 ? { usage } : {};
709
+ }
710
+ function buildAvailableCommandsField(record) {
711
+ const commands = record.acpx?.available_commands;
712
+ if (!commands || commands.length === 0) return {};
713
+ const availableCommands = commands.map((command) => runtimeAvailableCommand(command)).filter((command) => command !== void 0);
714
+ return availableCommands.length > 0 ? { availableCommands } : {};
715
+ }
716
+ function runtimeAvailableCommand(command) {
717
+ if (typeof command === "string") {
718
+ const name = command.trim();
719
+ return name ? { name } : void 0;
720
+ }
721
+ const record = commandRecord(command);
722
+ if (!record) return;
723
+ const name = asOptionalString(record.name);
724
+ if (!name) return;
725
+ const runtimeCommand = { name };
726
+ const description = asOptionalString(record.description);
727
+ if (description) runtimeCommand.description = description;
728
+ if (typeof record.has_input === "boolean") runtimeCommand.hasInput = record.has_input;
729
+ return runtimeCommand;
730
+ }
731
+ function commandRecord(value) {
732
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
733
+ return value;
734
+ }
735
+ //#endregion
516
736
  //#region src/runtime/engine/manager.ts
517
737
  function createDeferred() {
518
738
  let resolve;
@@ -617,27 +837,6 @@ function toPromptInput(text, attachments) {
617
837
  }
618
838
  return blocks.length > 0 ? blocks : textPrompt(text);
619
839
  }
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
840
  function createRecordId(sessionKey, mode) {
642
841
  if (mode === "persistent") return sessionKey;
643
842
  return `${sessionKey}:oneshot:${randomUUID()}`;
@@ -659,83 +858,6 @@ function legacyTerminalEventFromTurnResult(result) {
659
858
  ...result._meta === void 0 ? {} : { _meta: result._meta }
660
859
  };
661
860
  }
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;
696
- }
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 } : {};
708
- }
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 } : {};
714
- }
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;
729
- }
730
- function commandRecord(value) {
731
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
732
- return value;
733
- }
734
- function trimmedField(value) {
735
- if (typeof value !== "string") return;
736
- const trimmed = value.trim();
737
- return trimmed ? trimmed : void 0;
738
- }
739
861
  function advertisedConfigOptionIds(record) {
740
862
  const configOptions = record.acpx?.config_options;
741
863
  if (!configOptions) return;
@@ -780,20 +902,117 @@ var AcpRuntimeManager = class {
780
902
  deps;
781
903
  activeControllers = /* @__PURE__ */ new Map();
782
904
  retainedSessionOwners = /* @__PURE__ */ new Map();
905
+ retiringSessionOwners = /* @__PURE__ */ new Set();
783
906
  pendingOneShotRecordIds = /* @__PURE__ */ new Map();
784
907
  ensureSessionLocks = /* @__PURE__ */ new Map();
785
908
  runtimeOperationLocks = /* @__PURE__ */ new Map();
909
+ sessionTasks = /* @__PURE__ */ new Map();
786
910
  closingActiveRecords = /* @__PURE__ */ new Set();
911
+ liveClients = /* @__PURE__ */ new Set();
912
+ pendingTasks = /* @__PURE__ */ new Set();
913
+ shuttingDown = false;
914
+ shutdownTask;
787
915
  constructor(options, deps = {}) {
788
916
  this.options = options;
789
917
  this.deps = deps;
790
918
  }
791
919
  createClient(options) {
920
+ this.assertOpen();
792
921
  const clientOptions = {
793
922
  ...options,
794
- agentProcessEnv: this.options.agentProcessEnv
923
+ agentProcessEnv: this.options.agentProcessEnv,
924
+ processLifecycle: {
925
+ ...options.processLifecycle,
926
+ onBeforeSpawn: async (launch) => {
927
+ this.assertOpen();
928
+ await options.processLifecycle?.onBeforeSpawn?.(launch);
929
+ this.assertOpen();
930
+ },
931
+ onSpawned: async (process) => {
932
+ this.assertOpen();
933
+ await options.processLifecycle?.onSpawned?.(process);
934
+ this.assertOpen();
935
+ }
936
+ }
795
937
  };
796
- return this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
938
+ const client = this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
939
+ this.liveClients.add(client);
940
+ return client;
941
+ }
942
+ async probe() {
943
+ let client;
944
+ try {
945
+ return await probeRuntime(this.options, { clientFactory: (options) => client = this.createClient(options) });
946
+ } finally {
947
+ if (client) this.liveClients.delete(client);
948
+ }
949
+ }
950
+ assertOpen() {
951
+ if (this.shuttingDown) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
952
+ }
953
+ shutdown() {
954
+ if (!this.shutdownTask) {
955
+ this.shuttingDown = true;
956
+ this.shutdownTask = this.shutdownOwnedClients();
957
+ }
958
+ return this.shutdownTask;
959
+ }
960
+ async shutdownOwnedClients() {
961
+ const closed = await Promise.allSettled([...this.liveClients].map(async (client) => {
962
+ client.requestCancelActivePrompt().catch(() => {});
963
+ await this.closeClient(client);
964
+ }));
965
+ await Promise.allSettled([
966
+ ...this.ensureSessionLocks.values(),
967
+ ...this.runtimeOperationLocks.values(),
968
+ ...this.pendingTasks
969
+ ]);
970
+ await Promise.all([...this.retainedSessionOwners.values()].map(async (owner) => {
971
+ this.removeRetainedSessionOwner(owner);
972
+ await this.stopSessionOwner(owner);
973
+ }));
974
+ const errors = [];
975
+ for (const result of closed) if (result.status === "rejected") errors.push(result.reason);
976
+ if (errors.length) throw new AggregateError(errors, "ACP runtime shutdown failed.");
977
+ this.retiringSessionOwners.clear();
978
+ }
979
+ trackTask(task) {
980
+ this.pendingTasks.add(task);
981
+ const remove = () => {
982
+ this.pendingTasks.delete(task);
983
+ };
984
+ task.then(remove, remove);
985
+ return task;
986
+ }
987
+ queueSessionTask(recordId, run, cancel) {
988
+ const tasks = this.sessionTasks.get(recordId) ?? /* @__PURE__ */ new Set();
989
+ this.sessionTasks.set(recordId, tasks);
990
+ const previous = [...tasks].at(-1)?.completion;
991
+ const result = (async () => {
992
+ await previous?.catch(() => {});
993
+ return await run();
994
+ })();
995
+ const task = {
996
+ completion: result.then(() => {}),
997
+ cancel
998
+ };
999
+ task.completion.catch(() => {});
1000
+ tasks.add(task);
1001
+ const remove = () => {
1002
+ tasks.delete(task);
1003
+ if (tasks.size === 0) this.sessionTasks.delete(recordId);
1004
+ };
1005
+ result.then(remove, remove);
1006
+ return this.trackTask(result);
1007
+ }
1008
+ async cancelSessionTasks(recordId) {
1009
+ const tasks = [...this.sessionTasks.get(recordId) ?? []];
1010
+ await Promise.all(tasks.flatMap((task) => task.cancel ? [task.cancel()] : []));
1011
+ return tasks;
1012
+ }
1013
+ async closeClient(client) {
1014
+ await client.close();
1015
+ this.liveClients.delete(client);
797
1016
  }
798
1017
  createSessionOwner(input) {
799
1018
  const owner = {
@@ -900,12 +1119,27 @@ var AcpRuntimeManager = class {
900
1119
  }
901
1120
  async stopSessionOwner(owner) {
902
1121
  await this.flushSessionOwner(owner).catch(() => {});
903
- await owner.client.close().catch(() => {});
1122
+ await this.closeClient(owner.client).catch(() => {
1123
+ this.retiringSessionOwners.add(owner);
1124
+ });
904
1125
  await owner.projection?.checkpoint.flush().catch(() => {});
905
1126
  try {
906
1127
  owner.client.clearEventHandlers();
907
1128
  } catch {}
908
1129
  }
1130
+ async retrySessionCleanup(recordId) {
1131
+ for (const owner of this.retiringSessionOwners) if (owner.recordId === recordId) {
1132
+ owner.retirement ??= this.finalizeSessionConnection({
1133
+ client: owner.client,
1134
+ owner
1135
+ }).then(() => {
1136
+ this.retiringSessionOwners.delete(owner);
1137
+ }).finally(() => {
1138
+ owner.retirement = void 0;
1139
+ });
1140
+ await owner.retirement;
1141
+ }
1142
+ }
909
1143
  async refreshClosedState(record) {
910
1144
  if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
911
1145
  const latest = await this.options.sessionStore.load(record.acpxRecordId).catch(() => void 0);
@@ -933,9 +1167,20 @@ var AcpRuntimeManager = class {
933
1167
  return true;
934
1168
  }
935
1169
  canRetainPersistentSessionOwner(owner, record) {
936
- return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
1170
+ return !this.shuttingDown && owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
1171
+ }
1172
+ resolveMcpServers(session) {
1173
+ const servers = this.options.mcpServers;
1174
+ const { sessionKey, cwd, agentCommand, agentArgv } = session;
1175
+ return [...typeof servers === "function" ? servers({
1176
+ sessionKey,
1177
+ cwd,
1178
+ agentCommand,
1179
+ agentArgv: agentArgv ? [...agentArgv] : void 0
1180
+ }) : servers ?? []];
937
1181
  }
938
1182
  async withRuntimeControlSession(record, sessionMode, run, replacingConfigOption) {
1183
+ await this.retrySessionCleanup(record.acpxRecordId);
939
1184
  const owner = await this.readRetainedSessionOwner(record, { consume: false });
940
1185
  if (owner) {
941
1186
  const ownedRecord = owner.projection?.record ?? record;
@@ -954,34 +1199,42 @@ var AcpRuntimeManager = class {
954
1199
  await this.flushSessionOwner(owner);
955
1200
  }
956
1201
  }
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",
1202
+ let controlClient;
1203
+ try {
1204
+ const result = await withConnectedSession({
1205
+ sessionRecordId: record.acpxRecordId,
1206
+ loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
1207
+ saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
1208
+ createClient: (options) => controlClient = this.createClient({
1209
+ ...options,
1210
+ processLifecycle: this.options.processLifecycle,
1211
+ processLaunchScope: {
1212
+ kind: "runtime-session",
1213
+ sessionKey: record.name ?? record.acpxRecordId
1214
+ }
1215
+ }),
1216
+ mcpServers: this.resolveMcpServers({
1217
+ ...record,
966
1218
  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
- };
1219
+ }),
1220
+ permissionMode: this.options.permissionMode,
1221
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
1222
+ permissionPolicy: this.options.permissionPolicy,
1223
+ onPermissionRequest: this.options.onPermissionRequest,
1224
+ elicitationModes: this.options.elicitationModes,
1225
+ verbose: this.options.verbose,
1226
+ timeoutMs: this.options.timeoutMs,
1227
+ resumePolicy: resumePolicyForSessionMode(sessionMode),
1228
+ replacingConfigOption,
1229
+ run
1230
+ });
1231
+ return {
1232
+ value: result.value,
1233
+ record: result.record
1234
+ };
1235
+ } finally {
1236
+ if (controlClient) this.liveClients.delete(controlClient);
1237
+ }
985
1238
  }
986
1239
  refreshOwnedRecordLifecycle(owner, record) {
987
1240
  record.lastUsedAt = isoNow();
@@ -999,6 +1252,7 @@ var AcpRuntimeManager = class {
999
1252
  return await this.withManagerLock(this.ensureSessionLocks, key, run);
1000
1253
  }
1001
1254
  async withManagerLock(locks, key, run) {
1255
+ this.assertOpen();
1002
1256
  const previous = locks.get(key) ?? Promise.resolve();
1003
1257
  let release;
1004
1258
  const gate = new Promise((resolve) => {
@@ -1008,6 +1262,7 @@ var AcpRuntimeManager = class {
1008
1262
  locks.set(key, tail);
1009
1263
  await previous;
1010
1264
  try {
1265
+ this.assertOpen();
1011
1266
  return await run();
1012
1267
  } finally {
1013
1268
  release();
@@ -1030,7 +1285,8 @@ var AcpRuntimeManager = class {
1030
1285
  async loadExistingRuntimeSession(input) {
1031
1286
  const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
1032
1287
  if (!existingRecordId) return;
1033
- let record = await this.options.sessionStore.load(existingRecordId);
1288
+ await this.retrySessionCleanup(existingRecordId);
1289
+ let record = await this.findSession(existingRecordId);
1034
1290
  if (!record) return;
1035
1291
  const owner = this.retainedSessionOwners.get(record.acpxRecordId);
1036
1292
  if (owner) {
@@ -1042,6 +1298,9 @@ var AcpRuntimeManager = class {
1042
1298
  owner
1043
1299
  };
1044
1300
  }
1301
+ async findSession(sessionKey) {
1302
+ return await this.options.sessionStore.load(sessionKey);
1303
+ }
1045
1304
  canReuseRuntimeSession(input, agent, existing) {
1046
1305
  if (!shouldReuseExistingRecord(existing.record, {
1047
1306
  cwd: agent.cwd,
@@ -1068,7 +1327,10 @@ var AcpRuntimeManager = class {
1068
1327
  agentCommand,
1069
1328
  agentArgv,
1070
1329
  cwd,
1071
- mcpServers: [...this.options.mcpServers ?? []],
1330
+ mcpServers: this.resolveMcpServers({
1331
+ ...agent,
1332
+ sessionKey: input.sessionKey
1333
+ }),
1072
1334
  permissionMode: this.options.permissionMode,
1073
1335
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1074
1336
  permissionPolicy: this.options.permissionPolicy,
@@ -1090,6 +1352,7 @@ var AcpRuntimeManager = class {
1090
1352
  let retained = false;
1091
1353
  try {
1092
1354
  await client.start();
1355
+ this.assertOpen();
1093
1356
  const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
1094
1357
  const record = await this.prepareInitialRuntimeRecord({
1095
1358
  input,
@@ -1107,15 +1370,15 @@ var AcpRuntimeManager = class {
1107
1370
  if (!retained) {
1108
1371
  owner.recordId = void 0;
1109
1372
  client.clearEventHandlers();
1110
- await client.close();
1373
+ await this.closeClient(client);
1111
1374
  }
1112
1375
  }
1113
1376
  }
1114
1377
  async prepareInitialRuntimeRecord(params) {
1115
1378
  const { input, client, owner, agentCommand, agentArgv, cwd, session } = params;
1116
- const record = createInitialRecord({
1379
+ const record = createInitialSessionRecord({
1117
1380
  recordId: createRecordId(input.sessionKey, input.mode),
1118
- sessionName: input.sessionKey,
1381
+ name: input.sessionKey,
1119
1382
  sessionId: session.sessionId,
1120
1383
  agentCommand,
1121
1384
  agentArgv,
@@ -1135,9 +1398,7 @@ var AcpRuntimeManager = class {
1135
1398
  agentCommand,
1136
1399
  timeoutMs: this.options.timeoutMs
1137
1400
  });
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));
1401
+ applyInitialModelSelection(record, session.sessionResult.models, input.sessionOptions?.model, modelApplication);
1141
1402
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
1142
1403
  persistSessionOptions(record, input.sessionOptions);
1143
1404
  return record;
@@ -1145,6 +1406,7 @@ var AcpRuntimeManager = class {
1145
1406
  async retainInitializedSessionOwner(owner, record) {
1146
1407
  owner.recordId = record.acpxRecordId;
1147
1408
  await owner.projection?.checkpoint.checkpoint();
1409
+ this.assertOpen();
1148
1410
  const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
1149
1411
  this.retainedSessionOwners.set(record.acpxRecordId, owner);
1150
1412
  if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
@@ -1154,11 +1416,12 @@ var AcpRuntimeManager = class {
1154
1416
  }
1155
1417
  }
1156
1418
  startTurn(input) {
1419
+ this.assertOpen();
1157
1420
  let promptInput;
1158
1421
  try {
1159
1422
  promptInput = toPromptInput(input.text, input.attachments);
1160
1423
  } catch (error) {
1161
- this.closeRetainedOneShotHandle(input.handle).catch(() => {});
1424
+ this.trackTask(this.closeRetainedOneShotHandle(input.handle).catch(() => {}));
1162
1425
  throw error;
1163
1426
  }
1164
1427
  const queue = new AsyncEventQueue();
@@ -1194,28 +1457,8 @@ var AcpRuntimeManager = class {
1194
1457
  const abortHandler = () => {
1195
1458
  requestCancel();
1196
1459
  };
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({
1460
+ if (input.signal && !input.signal.aborted) input.signal.addEventListener("abort", abortHandler, { once: true });
1461
+ this.queueSessionTask(input.handle.acpxRecordId ?? input.handle.sessionKey, async () => this.runRuntimeTurnTask({
1219
1462
  input,
1220
1463
  promptInput,
1221
1464
  queue,
@@ -1224,7 +1467,7 @@ var AcpRuntimeManager = class {
1224
1467
  state,
1225
1468
  settleResult,
1226
1469
  abortHandler
1227
- });
1470
+ }), requestCancel);
1228
1471
  return {
1229
1472
  requestId: input.requestId,
1230
1473
  promptStarted: promptStarted.promise,
@@ -1246,30 +1489,39 @@ var AcpRuntimeManager = class {
1246
1489
  let turn;
1247
1490
  let terminalResult;
1248
1491
  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);
1492
+ if (this.cancelRuntimeTurnBeforePrompt(task)) {
1493
+ await this.closeRetainedOneShotHandle(task.input.handle);
1260
1494
  terminalResult = {
1261
- status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1262
- ...response.stopReason ? { stopReason: response.stopReason } : {},
1263
- ...response._meta === void 0 ? {} : { _meta: response._meta }
1495
+ status: "cancelled",
1496
+ stopReason: "cancelled"
1497
+ };
1498
+ } else {
1499
+ turn = await this.prepareRuntimeTurn(task);
1500
+ const { sessionId } = await this.connectRuntimeTurn(task, turn);
1501
+ this.assertOpen();
1502
+ await this.resolveRuntimeTurnReady(task, turn);
1503
+ if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
1504
+ status: "cancelled",
1505
+ stopReason: "cancelled"
1264
1506
  };
1507
+ else {
1508
+ await this.applyPendingRuntimeTurnCancel(task, turn);
1509
+ const response = await this.runRuntimePrompt(task, turn, sessionId);
1510
+ await this.saveCompletedRuntimeTurn(turn);
1511
+ terminalResult = {
1512
+ status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1513
+ ...response.stopReason ? { stopReason: response.stopReason } : {},
1514
+ ...response._meta === void 0 ? {} : { _meta: response._meta }
1515
+ };
1516
+ }
1265
1517
  }
1266
1518
  } catch (error) {
1267
1519
  terminalResult = this.failRuntimeTurn(task, error);
1268
1520
  }
1269
- try {
1270
- await this.finalizeRuntimeTurn(task, turn);
1271
- } catch (error) {
1272
- terminalResult = this.failRuntimeTurn(task, error);
1521
+ const finalization = await settleAttempt(async () => this.finalizeRuntimeTurn(task, turn));
1522
+ if (!finalization.ok) {
1523
+ task.settleResult(this.failRuntimeTurn(task, finalization.error));
1524
+ throw finalization.error;
1273
1525
  }
1274
1526
  task.settleResult(terminalResult);
1275
1527
  }
@@ -1283,7 +1535,8 @@ var AcpRuntimeManager = class {
1283
1535
  conversation: turn.conversation,
1284
1536
  promptMessageId: turn.promptMessageId,
1285
1537
  onPromptRequestWritten: () => task.promptStarted.resolve(),
1286
- onElicitation: task.input.onElicitation
1538
+ onElicitation: task.input.onElicitation,
1539
+ onPermissionRequest: task.input.onPermissionRequest
1287
1540
  });
1288
1541
  } finally {
1289
1542
  turn.client.endPromptElicitation?.(sessionId);
@@ -1291,7 +1544,10 @@ var AcpRuntimeManager = class {
1291
1544
  }
1292
1545
  async prepareRuntimeTurn(task) {
1293
1546
  const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
1294
- return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.prepareRuntimeTurnWithOwnership(task));
1547
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => {
1548
+ await this.retrySessionCleanup(recordId);
1549
+ return await this.prepareRuntimeTurnWithOwnership(task);
1550
+ });
1295
1551
  }
1296
1552
  async prepareRuntimeTurnWithOwnership(task) {
1297
1553
  const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
@@ -1398,7 +1654,10 @@ var AcpRuntimeManager = class {
1398
1654
  agentCommand: record.agentCommand,
1399
1655
  agentArgv: record.agentArgv,
1400
1656
  cwd: record.cwd,
1401
- mcpServers: [...this.options.mcpServers ?? []],
1657
+ mcpServers: this.resolveMcpServers({
1658
+ ...record,
1659
+ sessionKey: record.name ?? record.acpxRecordId
1660
+ }),
1402
1661
  permissionMode: this.options.permissionMode,
1403
1662
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1404
1663
  permissionPolicy: this.options.permissionPolicy,
@@ -1506,7 +1765,7 @@ var AcpRuntimeManager = class {
1506
1765
  const controller = task.state.activeController;
1507
1766
  if (controller) this.activeControllers.set(turn.record.acpxRecordId, controller);
1508
1767
  }
1509
- async resolveRuntimeTurnReady(task, turn, resumed, loadError) {
1768
+ async resolveRuntimeTurnReady(task, turn) {
1510
1769
  task.sessionReady.resolve();
1511
1770
  turn.record.lastRequestId = task.input.requestId;
1512
1771
  turn.record.lastPromptAt = isoNow();
@@ -1514,14 +1773,6 @@ var AcpRuntimeManager = class {
1514
1773
  turn.record.closedAt = void 0;
1515
1774
  turn.record.lastUsedAt = isoNow();
1516
1775
  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
1776
  }
1526
1777
  cancelRuntimeTurnBeforePrompt(task) {
1527
1778
  if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
@@ -1535,7 +1786,7 @@ var AcpRuntimeManager = class {
1535
1786
  if (cancelled) task.state.pendingCancel = false;
1536
1787
  return cancelled;
1537
1788
  }
1538
- async saveCompletedRuntimeTurn(turn, _stopReason) {
1789
+ async saveCompletedRuntimeTurn(turn) {
1539
1790
  turn.record.acpSessionId = turn.activeSessionId;
1540
1791
  reconcileAgentSessionId(turn.record, turn.record.agentSessionId);
1541
1792
  turn.record.protocolVersion = turn.client.initializeResult?.protocolVersion;
@@ -1583,8 +1834,11 @@ var AcpRuntimeManager = class {
1583
1834
  async closeRuntimeTurnClient(turn, pooled) {
1584
1835
  if (!turn || pooled) return;
1585
1836
  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;
1837
+ const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => this.closeClient(turn.client))]);
1838
+ if (failure) {
1839
+ this.retiringSessionOwners.add(turn.owner);
1840
+ throw failure.error;
1841
+ }
1588
1842
  }
1589
1843
  cleanupRuntimeTurn(task, turn) {
1590
1844
  if (turn) {
@@ -1615,26 +1869,15 @@ var AcpRuntimeManager = class {
1615
1869
  yield* turn.events;
1616
1870
  yield legacyTerminalEventFromTurnResult(await turn.result);
1617
1871
  }
1618
- async getStatus(handle) {
1872
+ getStatus(handle) {
1873
+ this.assertOpen();
1874
+ return this.trackTask(this.readStatus(handle));
1875
+ }
1876
+ async readStatus(handle) {
1619
1877
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1620
1878
  const owner = this.retainedSessionOwners.get(recordId);
1621
1879
  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
- };
1880
+ return runtimeStatusFromRecord(await this.requireRecord(recordId));
1638
1881
  }
1639
1882
  async setMode(handle, mode, sessionMode = "persistent") {
1640
1883
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
@@ -1651,6 +1894,25 @@ var AcpRuntimeManager = class {
1651
1894
  setDesiredModeId(targetRecord, mode);
1652
1895
  await this.options.sessionStore.save(targetRecord);
1653
1896
  }
1897
+ async setModel(handle, model, sessionMode = "persistent") {
1898
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1899
+ await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModelWithOwnership(handle, model, sessionMode));
1900
+ }
1901
+ async setModelWithOwnership(handle, model, sessionMode) {
1902
+ const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1903
+ const controller = this.activeControllers.get(record.acpxRecordId);
1904
+ if (controller) {
1905
+ const response = await controller.setSessionModel(model);
1906
+ record.acpx = applyModelSelection(record.acpx, model, response);
1907
+ await this.options.sessionStore.save(record);
1908
+ return;
1909
+ }
1910
+ const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1911
+ const response = await client.setSessionModel(sessionId, model, advertisedModelState(connectedRecord.acpx));
1912
+ connectedRecord.acpx = applyModelSelection(connectedRecord.acpx, model, response);
1913
+ }, { key: "model" });
1914
+ await this.options.sessionStore.save(result.record);
1915
+ }
1654
1916
  async setConfigOption(handle, key, value, sessionMode = "persistent") {
1655
1917
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1656
1918
  return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode));
@@ -1680,12 +1942,31 @@ var AcpRuntimeManager = class {
1680
1942
  async cancel(handle) {
1681
1943
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
1682
1944
  }
1683
- async close(handle, options = {}) {
1945
+ prepareFreshSession(handle) {
1946
+ this.assertOpen();
1684
1947
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1948
+ const cancelled = this.cancelSessionTasks(recordId);
1949
+ cancelled.catch(() => {});
1950
+ return this.queueSessionTask(recordId, async () => {
1951
+ const tasks = await cancelled;
1952
+ await Promise.all(tasks.map((task) => task.completion));
1953
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.closeRuntimeSession(handle, "prepare-fresh"));
1954
+ });
1955
+ }
1956
+ close(handle, options = {}) {
1957
+ this.assertOpen();
1958
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1959
+ return this.trackTask(this.withManagerLock(this.runtimeOperationLocks, recordId, async () => {
1960
+ return await this.closeRuntimeSession(handle, options.discardPersistentState === true ? "discard" : "release");
1961
+ }));
1962
+ }
1963
+ async closeRuntimeSession(handle, intent) {
1964
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1965
+ await this.retrySessionCleanup(recordId);
1685
1966
  const record = await this.resolveRuntimeRecordForClose(recordId);
1686
1967
  this.markActiveRuntimeRecordClosing(record);
1687
- await this.cancel(handle);
1688
- await this.closeRuntimeRecordOwnership(record, options.discardPersistentState === true);
1968
+ if (intent !== "prepare-fresh") await this.cancelSessionTasks(recordId);
1969
+ await this.closeRuntimeRecordOwnership(record, intent);
1689
1970
  record.closed = true;
1690
1971
  record.closedAt = isoNow();
1691
1972
  await this.options.sessionStore.save(record);
@@ -1698,14 +1979,20 @@ var AcpRuntimeManager = class {
1698
1979
  markActiveRuntimeRecordClosing(record) {
1699
1980
  if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
1700
1981
  }
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
- };
1982
+ async closeRuntimeRecordOwnership(record, intent) {
1983
+ if (intent === "discard") await this.closeBackendSession(record);
1984
+ else if (intent === "prepare-fresh") {
1985
+ const owner = this.retainedSessionOwners.get(record.acpxRecordId);
1986
+ if (owner) {
1987
+ this.removeRetainedSessionOwner(owner);
1988
+ this.retiringSessionOwners.add(owner);
1989
+ }
1990
+ await this.retrySessionCleanup(record.acpxRecordId);
1708
1991
  } else await this.closeRetainedSessionOwner(record.acpxRecordId);
1992
+ if (intent !== "release") record.acpx = {
1993
+ ...record.acpx,
1994
+ reset_on_next_ensure: true
1995
+ };
1709
1996
  }
1710
1997
  async closeBackendSession(record) {
1711
1998
  const connection = await this.acquireBackendCloseConnection(record);
@@ -1714,16 +2001,16 @@ var AcpRuntimeManager = class {
1714
2001
  } catch (error) {
1715
2002
  this.handleBackendSessionCloseError(record, error);
1716
2003
  } finally {
1717
- await this.finalizeBackendCloseConnection(connection);
2004
+ await this.finalizeSessionConnection(connection);
1718
2005
  }
1719
2006
  }
1720
- async finalizeBackendCloseConnection(connection) {
2007
+ async finalizeSessionConnection(connection) {
1721
2008
  const failure = firstFailedAttempt([
1722
2009
  await settleAttempt(async () => {
1723
2010
  if (connection.owner) await this.flushSessionOwner(connection.owner);
1724
2011
  }),
1725
2012
  await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
1726
- await settleAttempt(async () => connection.client.close())
2013
+ await settleAttempt(async () => this.closeClient(connection.client))
1727
2014
  ]);
1728
2015
  if (failure) throw failure.error;
1729
2016
  }
@@ -1733,23 +2020,7 @@ var AcpRuntimeManager = class {
1733
2020
  client: owner.client,
1734
2021
  owner
1735
2022
  };
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
- }) };
2023
+ return { client: this.createTurnClient(record) };
1753
2024
  }
1754
2025
  async requestBackendSessionClose(record, connection) {
1755
2026
  if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
@@ -1784,7 +2055,10 @@ var FileSessionStore = class {
1784
2055
  return path.join(this.sessionDir, `${safeSessionId(sessionId)}.json`);
1785
2056
  }
1786
2057
  async ensureDir() {
1787
- await fs.mkdir(this.sessionDir, { recursive: true });
2058
+ await fs.mkdir(this.sessionDir, {
2059
+ recursive: true,
2060
+ mode: 448
2061
+ });
1788
2062
  }
1789
2063
  async load(sessionId) {
1790
2064
  await this.ensureDir();
@@ -1804,14 +2078,9 @@ var FileSessionStore = class {
1804
2078
  return parseSessionRecord(parsed) ?? void 0;
1805
2079
  }
1806
2080
  async save(record) {
1807
- await this.ensureDir();
1808
2081
  const persisted = serializeSessionRecordForDisk(record);
1809
2082
  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);
2083
+ await writePrivateJsonFile(this.filePath(record.acpxRecordId), persisted);
1815
2084
  }
1816
2085
  };
1817
2086
  function createFileSessionStore(options) {
@@ -1856,125 +2125,20 @@ function writeHandleState(handle, state) {
1856
2125
  handle.agentSessionId = state.agentSessionId;
1857
2126
  }
1858
2127
  //#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";
1862
- }
1863
- function formatFunctionDetail(value) {
1864
- return value.name ? `[Function ${value.name}]` : "[Function]";
1865
- }
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);
1873
- }
1874
- return nested;
1875
- }) ?? "undefined";
1876
- }
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";
1886
- }
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();
1897
- 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
- ]
1906
- };
1907
- } catch (error) {
1908
- 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
- ]
1917
- };
1918
- } finally {
1919
- await client.close().catch(() => {});
1920
- }
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);
1939
- }
1940
- //#endregion
1941
2128
  //#region src/runtime.ts
1942
2129
  const ACPX_BACKEND_ID = "acpx";
1943
2130
  const ACPX_CAPABILITIES = { controls: [
1944
2131
  "session/set_mode",
2132
+ "session/set_model",
1945
2133
  "session/set_config_option",
1946
2134
  "session/status"
1947
2135
  ] };
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
2136
  var AcpxRuntime = class {
1975
2137
  options;
1976
2138
  testOptions;
1977
2139
  healthy = false;
2140
+ shutdownTask;
2141
+ probeTasks = /* @__PURE__ */ new Set();
1978
2142
  manager = null;
1979
2143
  managerPromise = null;
1980
2144
  constructor(options, testOptions) {
@@ -2002,11 +2166,27 @@ var AcpxRuntime = class {
2002
2166
  details: normalizeRuntimeDetails(report.details)
2003
2167
  };
2004
2168
  }
2005
- async ensureSession(input) {
2169
+ resolveSessionIdentity(input) {
2006
2170
  const sessionName = input.sessionKey.trim();
2007
2171
  if (!sessionName) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
2008
2172
  const agent = input.agent.trim();
2009
2173
  if (!agent) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP agent id is required.");
2174
+ return {
2175
+ sessionName,
2176
+ agent
2177
+ };
2178
+ }
2179
+ async findSession(input) {
2180
+ const { sessionName, agent } = this.resolveSessionIdentity(input);
2181
+ const record = await (await this.getManager()).findSession(sessionName);
2182
+ return record ? this.createSessionHandle({
2183
+ sessionKey: input.sessionKey,
2184
+ agent,
2185
+ mode: "persistent"
2186
+ }, record) : void 0;
2187
+ }
2188
+ async ensureSession(input) {
2189
+ const { sessionName, agent } = this.resolveSessionIdentity(input);
2010
2190
  const record = await (await this.getManager()).ensureSession({
2011
2191
  sessionKey: sessionName,
2012
2192
  agent,
@@ -2015,6 +2195,12 @@ var AcpxRuntime = class {
2015
2195
  resumeSessionId: input.resumeSessionId,
2016
2196
  sessionOptions: input.sessionOptions
2017
2197
  });
2198
+ return this.createSessionHandle({
2199
+ ...input,
2200
+ agent
2201
+ }, record);
2202
+ }
2203
+ createSessionHandle(input, record) {
2018
2204
  const handle = {
2019
2205
  sessionKey: input.sessionKey,
2020
2206
  backend: ACPX_BACKEND_ID,
@@ -2025,8 +2211,8 @@ var AcpxRuntime = class {
2025
2211
  agentSessionId: record.agentSessionId
2026
2212
  };
2027
2213
  writeHandleState(handle, {
2028
- name: sessionName,
2029
- agent,
2214
+ name: input.sessionKey.trim(),
2215
+ agent: input.agent,
2030
2216
  cwd: record.cwd,
2031
2217
  mode: input.mode,
2032
2218
  acpxRecordId: record.acpxRecordId,
@@ -2046,7 +2232,8 @@ var AcpxRuntime = class {
2046
2232
  requestId: input.requestId,
2047
2233
  timeoutMs: input.timeoutMs,
2048
2234
  signal: input.signal,
2049
- onElicitation: input.onElicitation
2235
+ onElicitation: input.onElicitation,
2236
+ onPermissionRequest: input.onPermissionRequest
2050
2237
  }));
2051
2238
  return {
2052
2239
  requestId: input.requestId,
@@ -2078,7 +2265,8 @@ var AcpxRuntime = class {
2078
2265
  requestId: input.requestId,
2079
2266
  timeoutMs: input.timeoutMs,
2080
2267
  signal: input.signal,
2081
- onElicitation: input.onElicitation
2268
+ onElicitation: input.onElicitation,
2269
+ onPermissionRequest: input.onPermissionRequest
2082
2270
  });
2083
2271
  }
2084
2272
  async getCapabilities(input) {
@@ -2100,6 +2288,10 @@ var AcpxRuntime = class {
2100
2288
  const { handle, state } = this.resolveManagerHandle(input.handle);
2101
2289
  await (await this.getManager()).setMode(handle, input.mode, state.mode);
2102
2290
  }
2291
+ async setModel(input) {
2292
+ const { handle, state } = this.resolveManagerHandle(input.handle);
2293
+ await (await this.getManager()).setModel(handle, input.model, state.mode);
2294
+ }
2103
2295
  async setConfigOption(input) {
2104
2296
  const { handle, state } = this.resolveManagerHandle(input.handle);
2105
2297
  return await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
@@ -2108,11 +2300,26 @@ var AcpxRuntime = class {
2108
2300
  const { handle } = this.resolveManagerHandle(input.handle);
2109
2301
  await (await this.getManager()).cancel(handle);
2110
2302
  }
2303
+ async prepareFreshSession(input) {
2304
+ const { handle } = this.resolveManagerHandle(input.handle);
2305
+ await (await this.getManager()).prepareFreshSession(handle);
2306
+ }
2111
2307
  async close(input) {
2112
2308
  const { handle } = this.resolveManagerHandle(input.handle);
2113
2309
  await (await this.getManager()).close(handle, { discardPersistentState: input.discardPersistentState });
2114
2310
  }
2311
+ shutdown() {
2312
+ if (!this.shutdownTask) {
2313
+ this.healthy = false;
2314
+ const managerShutdown = this.managerPromise?.then((manager) => manager.shutdown());
2315
+ this.shutdownTask = Promise.allSettled([managerShutdown, ...this.probeTasks]).then(([manager]) => {
2316
+ if (manager.status === "rejected") throw manager.reason;
2317
+ });
2318
+ }
2319
+ return this.shutdownTask;
2320
+ }
2115
2321
  async getManager() {
2322
+ if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
2116
2323
  if (this.manager) return this.manager;
2117
2324
  if (!this.managerPromise) this.managerPromise = Promise.resolve(this.testOptions?.managerFactory?.(this.options) ?? new AcpRuntimeManager(this.options)).then((manager) => {
2118
2325
  this.manager = manager;
@@ -2121,7 +2328,16 @@ var AcpxRuntime = class {
2121
2328
  return await this.managerPromise;
2122
2329
  }
2123
2330
  async runProbe() {
2124
- return await (this.testOptions?.probeRunner?.(this.options) ?? probeRuntime(this.options));
2331
+ if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
2332
+ const probe = this.testOptions?.probeRunner?.(this.options) ?? this.getManager().then((manager) => manager.probe());
2333
+ this.probeTasks.add(probe);
2334
+ try {
2335
+ const report = await probe;
2336
+ if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
2337
+ return report;
2338
+ } finally {
2339
+ this.probeTasks.delete(probe);
2340
+ }
2125
2341
  }
2126
2342
  resolveManagerHandle(handle) {
2127
2343
  const state = this.resolveHandleState(handle);