acpx 0.13.0 → 0.13.2

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.
package/dist/runtime.js CHANGED
@@ -1,7 +1,8 @@
1
- import { $ as createAtomicWriteTempPath, A as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, D as applyLifecycleSnapshotToRecord, Dt as textPrompt, E as applyConversation, F as modelStateFromConfigOptions, Ft as resolveAgentArgv, Ht as extractAcpError, I as normalizeAgentCommandInput, It as resolveAgentCommand, Lt as resolveCanonicalAgentName, M as RequestedModelUnsupportedError, Mt as DEFAULT_AGENT_NAME, Nt as listBuiltInAgents, O as reconcileAgentSessionId, P as isRequestedModelUnsupportedError, Pt as normalizeAgentName, S as advertisedModelState, T as sessionOptionsFromRecord, Ut as isAcpResourceNotFoundError, Vt as normalizeOutputError, _ as createSessionConversation, a as applyRequestedModelIfAdvertised, b as recordSessionUpdate, c as setCurrentModelId, d as setDesiredModelId, et as assertPersistedKeyPolicy, f as syncAdvertisedModelState, g as cloneSessionConversation, h as cloneSessionAcpxState, i as connectAndLoadSession, j as REQUESTED_MODEL_UNSUPPORTED_REASONS, jt as withTimeout, k as AcpClient, l as setDesiredConfigOption, lt as parseSessionRecord, m as applyConfigOptionsToState, n as runPromptTurn, o as currentModelIdFromSetModelResponse, p as applyConfigOptionsToRecord, pt as defaultSessionEventLog, r as withConnectedSession, s as clearDesiredConfigOption, t as LiveSessionCheckpoint, u as setDesiredModeId, ut as serializeSessionRecordForDisk, v as recordClientOperation, w as persistSessionOptions, x as trimConversationForRuntime, y as recordPromptSubmission } from "./live-checkpoint-CBecfnSH.js";
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-Gw2oGjhe.js";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
5
+ import { isDeepStrictEqual } from "node:util";
5
6
  //#region src/runtime/public/errors.ts
6
7
  var AcpRuntimeError = class extends Error {
7
8
  code;
@@ -107,7 +108,46 @@ function planStatusText(payload) {
107
108
  const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
108
109
  return content ? `plan: ${content}` : null;
109
110
  }
111
+ /**
112
+ * Documented allowlist for text_delta.meta origin fields.
113
+ * Only these keys may be copied from ACP update `_meta`.
114
+ * Unknown keys (including secret-like producer-controlled names) are dropped.
115
+ */
116
+ const ORIGIN_META_KEYS = [
117
+ "origin",
118
+ "kind",
119
+ "source"
120
+ ];
121
+ /**
122
+ * Preserve a fail-closed subset of ACP update origin fields for text_delta
123
+ * consumers. Empty or unusable values are omitted rather than inferred.
124
+ */
125
+ function extractTextDeltaOrigin(payload) {
126
+ const messageId = asOptionalString(payload.messageId);
127
+ const meta = sanitizeOriginMeta(payload._meta);
128
+ return {
129
+ ...messageId ? { messageId } : {},
130
+ ...meta ? { meta } : {}
131
+ };
132
+ }
133
+ /**
134
+ * Copy only allowlisted string origin keys from wire `_meta`.
135
+ * Nested objects, arrays, non-strings, and unknown keys are dropped.
136
+ */
137
+ function sanitizeOriginMeta(value) {
138
+ if (!isRecord(value)) return;
139
+ const out = {};
140
+ for (const key of ORIGIN_META_KEYS) {
141
+ const entry = value[key];
142
+ if (typeof entry !== "string") continue;
143
+ const trimmed = entry.trim();
144
+ if (!trimmed) continue;
145
+ out[key] = trimmed;
146
+ }
147
+ return Object.keys(out).length > 0 ? out : void 0;
148
+ }
110
149
  function resolveTextChunk(params) {
150
+ const origin = extractTextDeltaOrigin(params.payload);
111
151
  const contentRaw = params.payload.content;
112
152
  if (isRecord(contentRaw)) {
113
153
  const contentType = asTrimmedString(contentRaw.type);
@@ -117,7 +157,8 @@ function resolveTextChunk(params) {
117
157
  type: "text_delta",
118
158
  text,
119
159
  stream: params.stream,
120
- tag: params.tag
160
+ tag: params.tag,
161
+ ...origin
121
162
  };
122
163
  }
123
164
  const text = asString(params.payload.text);
@@ -126,7 +167,8 @@ function resolveTextChunk(params) {
126
167
  type: "text_delta",
127
168
  text,
128
169
  stream: params.stream,
129
- tag: params.tag
170
+ tag: params.tag,
171
+ ...origin
130
172
  };
131
173
  }
132
174
  function createTextDeltaEvent(params) {
@@ -179,7 +221,7 @@ function summarizeToolInput(rawInput) {
179
221
  }
180
222
  function truncateToolSummary(value) {
181
223
  if (value.length <= TOOL_OUTPUT_SUMMARY_MAX_CHARS) return value;
182
- return `${value.slice(0, TOOL_OUTPUT_SUMMARY_MAX_CHARS - 1)}…`;
224
+ return `${value.slice(0, 499)}…`;
183
225
  }
184
226
  function readToolContentText(value) {
185
227
  const record = isRecord(value) ? value : void 0;
@@ -461,9 +503,15 @@ function shouldReuseExistingRecord(record, params) {
461
503
  if (record.acpx?.reset_on_next_ensure === true) return false;
462
504
  if (path.resolve(record.cwd) !== path.resolve(params.cwd)) return false;
463
505
  if (record.agentCommand !== params.agentCommand) return false;
506
+ if (!sameArgv(record.agentArgv, params.agentArgv)) return false;
464
507
  if (params.resumeSessionId && record.acpSessionId !== params.resumeSessionId) return false;
465
508
  return true;
466
509
  }
510
+ function sameArgv(left, right) {
511
+ const leftValues = left ?? [];
512
+ const rightValues = right ?? [];
513
+ return leftValues.length === rightValues.length && leftValues.every((value, index) => value === rightValues[index]);
514
+ }
467
515
  //#endregion
468
516
  //#region src/runtime/engine/manager.ts
469
517
  function createDeferred() {
@@ -478,6 +526,22 @@ function createDeferred() {
478
526
  reject
479
527
  };
480
528
  }
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
+ }
481
545
  var AsyncEventQueue = class {
482
546
  items = [];
483
547
  waits = [];
@@ -685,30 +749,6 @@ function resolveSupportedConfigOptionId(record, configId) {
685
749
  const supportedText = supported.length > 0 ? supported.join(", ") : "none";
686
750
  throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
687
751
  }
688
- function applyConfigOptionResponseToTurn(turn, response) {
689
- if (!response?.configOptions) return;
690
- turn.acpxState = applyConfigOptionsToState(turn.acpxState, response.configOptions);
691
- }
692
- function applyDesiredConfigOptionToTurn(turn, configId, value) {
693
- const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
694
- if (configId === modelStateFromConfigOptions(nextState.config_options)?.configId) {
695
- nextState.session_options = {
696
- ...nextState.session_options,
697
- model: value
698
- };
699
- clearDesiredConfigOption(nextState, configId);
700
- } else if (configId === "mode") nextState.desired_mode_id = value;
701
- else nextState.desired_config_options = {
702
- ...nextState.desired_config_options,
703
- [configId]: value
704
- };
705
- turn.acpxState = nextState;
706
- }
707
- function applyDesiredConfigOptionToRecord(record, configId, value) {
708
- if (configId === modelStateFromConfigOptions(record.acpx?.config_options)?.configId) setDesiredModelId(record, value, configId);
709
- else if (configId === "mode") setDesiredModeId(record, value);
710
- else setDesiredConfigOption(record, configId, value);
711
- }
712
752
  async function createOrLoadRuntimeSession(client, resumeSessionId, cwd) {
713
753
  if (resumeSessionId) {
714
754
  if (client.supportsResumeSession()) {
@@ -738,7 +778,10 @@ var AcpRuntimeManager = class {
738
778
  options;
739
779
  deps;
740
780
  activeControllers = /* @__PURE__ */ new Map();
741
- pendingPersistentClients = /* @__PURE__ */ new Map();
781
+ retainedSessionOwners = /* @__PURE__ */ new Map();
782
+ pendingOneShotRecordIds = /* @__PURE__ */ new Map();
783
+ ensureSessionLocks = /* @__PURE__ */ new Map();
784
+ runtimeOperationLocks = /* @__PURE__ */ new Map();
742
785
  closingActiveRecords = /* @__PURE__ */ new Set();
743
786
  constructor(options, deps = {}) {
744
787
  this.options = options;
@@ -747,22 +790,116 @@ var AcpRuntimeManager = class {
747
790
  createClient(options) {
748
791
  return this.deps.clientFactory?.(options) ?? new AcpClient(options);
749
792
  }
750
- async readPendingPersistentClient(record, options) {
751
- const pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
752
- if (!pendingClient) return;
753
- if (!pendingClient.hasReusableSession(record.acpSessionId)) {
754
- this.pendingPersistentClients.delete(record.acpxRecordId);
755
- await pendingClient.close().catch(() => {});
793
+ createSessionOwner(input) {
794
+ const owner = {
795
+ ...input,
796
+ bufferSessionUpdates: false,
797
+ pendingSessionUpdates: []
798
+ };
799
+ input.client.setEventHandlers({
800
+ onSessionUpdate: (notification) => this.routeOwnedSessionUpdate(owner, notification),
801
+ onClientOperation: (operation) => this.routeOwnedClientOperation(owner, operation)
802
+ });
803
+ return owner;
804
+ }
805
+ routeOwnedSessionUpdate(owner, notification) {
806
+ const active = owner.activeTurn;
807
+ if (active) {
808
+ const { task, turn } = active;
809
+ if (turn.connected) {
810
+ turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
811
+ turn.liveCheckpoint.request();
812
+ } else turn.record.acpx = recordSessionUpdate(turn.conversation, turn.record.acpx, notification);
813
+ trimConversationForRuntime(turn.conversation);
814
+ this.emitRuntimeTurnEvent(task, {
815
+ jsonrpc: "2.0",
816
+ method: "session/update",
817
+ params: notification
818
+ });
819
+ return;
820
+ }
821
+ if (owner.bufferSessionUpdates) {
822
+ owner.pendingSessionUpdates.push(notification);
756
823
  return;
757
824
  }
758
- if (options.consume) this.pendingPersistentClients.delete(record.acpxRecordId);
759
- return pendingClient;
825
+ const projection = owner.projection;
826
+ if (!projection) {
827
+ owner.pendingSessionUpdates.push(notification);
828
+ return;
829
+ }
830
+ projection.record.acpx = recordSessionUpdate(projection.conversation, projection.record.acpx, notification);
831
+ trimConversationForRuntime(projection.conversation);
832
+ projection.checkpoint.request();
833
+ }
834
+ routeOwnedClientOperation(owner, operation) {
835
+ const active = owner.activeTurn;
836
+ if (!active) return;
837
+ const { task, turn } = active;
838
+ if (turn.connected) {
839
+ turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
840
+ turn.liveCheckpoint.request();
841
+ } else turn.record.acpx = recordClientOperation(turn.conversation, turn.record.acpx, operation);
842
+ trimConversationForRuntime(turn.conversation);
843
+ this.emitRuntimeTurnEvent(task, {
844
+ type: "client_operation",
845
+ ...operation
846
+ });
760
847
  }
761
- async closePendingPersistentClient(recordId) {
762
- const pendingClient = this.pendingPersistentClients.get(recordId);
763
- if (!pendingClient) return;
764
- this.pendingPersistentClients.delete(recordId);
765
- await pendingClient.close().catch(() => {});
848
+ attachIdleProjection(owner, record, conversation = cloneSessionConversation(record), acpxState = record.acpx) {
849
+ record.acpx = acpxState;
850
+ owner.projection = {
851
+ record,
852
+ conversation,
853
+ checkpoint: new LiveSessionCheckpoint({ save: async () => {
854
+ if (!owner.recordId) return;
855
+ record.lastUsedAt = isoNow();
856
+ applyConversation(record, conversation);
857
+ applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
858
+ await this.refreshClosedState(record);
859
+ await this.options.sessionStore.save(record);
860
+ } })
861
+ };
862
+ owner.activeTurn = void 0;
863
+ this.drainPendingSessionUpdates(owner);
864
+ }
865
+ drainPendingSessionUpdates(owner) {
866
+ for (const notification of owner.pendingSessionUpdates.splice(0)) this.routeOwnedSessionUpdate(owner, notification);
867
+ }
868
+ async flushSessionOwner(owner) {
869
+ await owner.client.waitForSessionUpdatesIdle?.().catch(() => {});
870
+ await owner.projection?.checkpoint.flush();
871
+ }
872
+ removeRetainedSessionOwner(owner) {
873
+ if (owner.recordId && this.retainedSessionOwners.get(owner.recordId) === owner) this.retainedSessionOwners.delete(owner.recordId);
874
+ if (owner.mode === "oneshot" && owner.recordId && this.pendingOneShotRecordIds.get(owner.sessionKey) === owner.recordId) this.pendingOneShotRecordIds.delete(owner.sessionKey);
875
+ }
876
+ async readRetainedSessionOwner(record, options) {
877
+ const owner = this.retainedSessionOwners.get(record.acpxRecordId);
878
+ if (!owner) return;
879
+ await this.flushSessionOwner(owner);
880
+ const projectedRecord = owner.projection?.record;
881
+ if (projectedRecord && projectedRecord !== record) Object.assign(record, structuredClone(projectedRecord));
882
+ if (!owner.client.hasReusableSession(record.acpSessionId)) {
883
+ this.removeRetainedSessionOwner(owner);
884
+ await this.stopSessionOwner(owner);
885
+ return;
886
+ }
887
+ if (options.consume) this.removeRetainedSessionOwner(owner);
888
+ return owner;
889
+ }
890
+ async closeRetainedSessionOwner(recordId) {
891
+ const owner = this.retainedSessionOwners.get(recordId);
892
+ if (!owner) return;
893
+ this.removeRetainedSessionOwner(owner);
894
+ await this.stopSessionOwner(owner);
895
+ }
896
+ async stopSessionOwner(owner) {
897
+ await this.flushSessionOwner(owner).catch(() => {});
898
+ await owner.client.close().catch(() => {});
899
+ await owner.projection?.checkpoint.flush().catch(() => {});
900
+ try {
901
+ owner.client.clearEventHandlers();
902
+ } catch {}
766
903
  }
767
904
  async refreshClosedState(record) {
768
905
  if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
@@ -775,32 +912,42 @@ var AcpRuntimeManager = class {
775
912
  };
776
913
  return true;
777
914
  }
778
- async retainPersistentClientAfterTurn(input) {
779
- const { record, client } = input;
780
- if (!!record.acpxRecordId.includes(":oneshot:") || record.closed || !client.hasReusableSession(record.acpSessionId)) return false;
781
- const previousClient = this.pendingPersistentClients.get(record.acpxRecordId);
782
- this.pendingPersistentClients.set(record.acpxRecordId, client);
783
- if (previousClient && previousClient !== client) await previousClient.close().catch(() => {});
915
+ async retainPersistentSessionOwnerAfterTurn(input) {
916
+ const { record, owner, conversation, acpxState } = input;
917
+ if (!this.canRetainPersistentSessionOwner(owner, record)) {
918
+ owner.activeTurn = void 0;
919
+ return false;
920
+ }
921
+ this.attachIdleProjection(owner, record, conversation, acpxState);
922
+ const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
923
+ this.retainedSessionOwners.set(record.acpxRecordId, owner);
924
+ if (previousOwner && previousOwner !== owner) {
925
+ this.removeRetainedSessionOwner(previousOwner);
926
+ await this.stopSessionOwner(previousOwner);
927
+ }
784
928
  return true;
785
929
  }
786
- async withRuntimeControlSession(record, sessionMode, run) {
787
- const pendingClient = await this.readPendingPersistentClient(record, { consume: false });
788
- if (pendingClient) {
789
- const value = await run({
790
- client: pendingClient,
791
- sessionId: record.acpSessionId,
792
- record
793
- });
794
- record.lastUsedAt = isoNow();
795
- record.closed = false;
796
- record.closedAt = void 0;
797
- record.protocolVersion = pendingClient.initializeResult?.protocolVersion;
798
- record.agentCapabilities = pendingClient.initializeResult?.agentCapabilities;
799
- applyLifecycleSnapshotToRecord(record, pendingClient.getAgentLifecycleSnapshot());
800
- return {
801
- value,
802
- record
803
- };
930
+ canRetainPersistentSessionOwner(owner, record) {
931
+ return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
932
+ }
933
+ async withRuntimeControlSession(record, sessionMode, run, replacingConfigOption) {
934
+ const owner = await this.readRetainedSessionOwner(record, { consume: false });
935
+ if (owner) {
936
+ const ownedRecord = owner.projection?.record ?? record;
937
+ try {
938
+ const value = await run({
939
+ client: owner.client,
940
+ sessionId: ownedRecord.acpSessionId,
941
+ record: ownedRecord
942
+ });
943
+ this.refreshOwnedRecordLifecycle(owner, ownedRecord);
944
+ return {
945
+ value,
946
+ record: ownedRecord
947
+ };
948
+ } finally {
949
+ await this.flushSessionOwner(owner);
950
+ }
804
951
  }
805
952
  const result = await withConnectedSession({
806
953
  sessionRecordId: record.acpxRecordId,
@@ -810,10 +957,13 @@ var AcpRuntimeManager = class {
810
957
  mcpServers: [...this.options.mcpServers ?? []],
811
958
  permissionMode: this.options.permissionMode,
812
959
  nonInteractivePermissions: this.options.nonInteractivePermissions,
960
+ permissionPolicy: this.options.permissionPolicy,
813
961
  onPermissionRequest: this.options.onPermissionRequest,
962
+ elicitationModes: this.options.elicitationModes,
814
963
  verbose: this.options.verbose,
815
964
  timeoutMs: this.options.timeoutMs,
816
965
  resumePolicy: resumePolicyForSessionMode(sessionMode),
966
+ replacingConfigOption,
817
967
  run
818
968
  });
819
969
  return {
@@ -821,21 +971,87 @@ var AcpRuntimeManager = class {
821
971
  record: result.record
822
972
  };
823
973
  }
974
+ refreshOwnedRecordLifecycle(owner, record) {
975
+ record.lastUsedAt = isoNow();
976
+ record.closed = false;
977
+ record.closedAt = void 0;
978
+ record.protocolVersion = owner.client.initializeResult?.protocolVersion;
979
+ record.agentCapabilities = owner.client.initializeResult?.agentCapabilities;
980
+ applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
981
+ }
824
982
  async ensureSession(input) {
983
+ return await this.withEnsureSessionLock(input, async () => this.ensureSessionWithOwnership(input));
984
+ }
985
+ async withEnsureSessionLock(input, run) {
986
+ const key = `${input.mode}\0${input.sessionKey}`;
987
+ return await this.withManagerLock(this.ensureSessionLocks, key, run);
988
+ }
989
+ async withManagerLock(locks, key, run) {
990
+ const previous = locks.get(key) ?? Promise.resolve();
991
+ let release;
992
+ const gate = new Promise((resolve) => {
993
+ release = resolve;
994
+ });
995
+ const tail = previous.then(() => gate);
996
+ locks.set(key, tail);
997
+ await previous;
998
+ try {
999
+ return await run();
1000
+ } finally {
1001
+ release();
1002
+ if (locks.get(key) === tail) locks.delete(key);
1003
+ }
1004
+ }
1005
+ async ensureSessionWithOwnership(input) {
825
1006
  const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
826
1007
  const { agentCommand, agentArgv } = normalizeAgentCommandInput(this.options.agentRegistry.resolve(input.agent));
827
- const existing = await this.options.sessionStore.load(input.sessionKey);
828
- if (input.mode === "persistent" && existing && shouldReuseExistingRecord(existing, {
1008
+ const agent = {
829
1009
  cwd,
830
1010
  agentCommand,
831
- resumeSessionId: input.resumeSessionId
832
- })) {
833
- existing.closed = false;
834
- existing.closedAt = void 0;
835
- this.closingActiveRecords.delete(existing.acpxRecordId);
836
- await this.options.sessionStore.save(existing);
837
- return existing;
1011
+ agentArgv
1012
+ };
1013
+ const existing = await this.loadExistingRuntimeSession(input);
1014
+ if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing.record);
1015
+ await this.closeConflictingPersistentSession(input, existing?.owner);
1016
+ return await this.createOwnedRuntimeSession(input, agent);
1017
+ }
1018
+ async loadExistingRuntimeSession(input) {
1019
+ const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
1020
+ if (!existingRecordId) return;
1021
+ let record = await this.options.sessionStore.load(existingRecordId);
1022
+ if (!record) return;
1023
+ const owner = this.retainedSessionOwners.get(record.acpxRecordId);
1024
+ if (owner) {
1025
+ await this.flushSessionOwner(owner);
1026
+ record = owner.projection?.record ?? record;
838
1027
  }
1028
+ return {
1029
+ record,
1030
+ owner
1031
+ };
1032
+ }
1033
+ canReuseRuntimeSession(input, agent, existing) {
1034
+ if (!shouldReuseExistingRecord(existing.record, {
1035
+ cwd: agent.cwd,
1036
+ agentCommand: agent.agentCommand,
1037
+ agentArgv: agent.agentArgv,
1038
+ resumeSessionId: input.resumeSessionId
1039
+ })) return false;
1040
+ if (input.mode === "persistent") return true;
1041
+ return Boolean(existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), input.sessionOptions));
1042
+ }
1043
+ async reuseRuntimeSession(record) {
1044
+ record.closed = false;
1045
+ record.closedAt = void 0;
1046
+ this.closingActiveRecords.delete(record.acpxRecordId);
1047
+ await this.options.sessionStore.save(record);
1048
+ return record;
1049
+ }
1050
+ async closeConflictingPersistentSession(input, owner) {
1051
+ if (input.mode === "persistent" && owner?.recordId) await this.closeRetainedSessionOwner(owner.recordId);
1052
+ }
1053
+ async createOwnedRuntimeSession(input, agent) {
1054
+ const { cwd, agentCommand, agentArgv } = agent;
839
1055
  const client = this.createClient({
840
1056
  agentCommand,
841
1057
  agentArgv,
@@ -843,30 +1059,43 @@ var AcpRuntimeManager = class {
843
1059
  mcpServers: [...this.options.mcpServers ?? []],
844
1060
  permissionMode: this.options.permissionMode,
845
1061
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1062
+ permissionPolicy: this.options.permissionPolicy,
846
1063
  onPermissionRequest: this.options.onPermissionRequest,
1064
+ elicitationModes: this.options.elicitationModes,
847
1065
  verbose: this.options.verbose,
848
1066
  sessionOptions: input.sessionOptions
849
1067
  });
850
- let keepClientOpen = false;
1068
+ const owner = this.createSessionOwner({
1069
+ client,
1070
+ sessionKey: input.sessionKey,
1071
+ mode: input.mode
1072
+ });
1073
+ let retained = false;
851
1074
  try {
852
1075
  await client.start();
853
1076
  const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
854
- const record = await this.createAndSaveRuntimeRecord({
1077
+ const record = await this.prepareInitialRuntimeRecord({
855
1078
  input,
856
1079
  client,
1080
+ owner,
857
1081
  agentCommand,
858
1082
  agentArgv,
859
1083
  cwd,
860
1084
  session
861
1085
  });
862
- keepClientOpen = await this.keepPersistentClient(input.mode, record.acpxRecordId, client);
1086
+ await this.retainInitializedSessionOwner(owner, record);
1087
+ retained = true;
863
1088
  return record;
864
1089
  } finally {
865
- if (!keepClientOpen) await client.close();
1090
+ if (!retained) {
1091
+ owner.recordId = void 0;
1092
+ client.clearEventHandlers();
1093
+ await client.close();
1094
+ }
866
1095
  }
867
1096
  }
868
- async createAndSaveRuntimeRecord(params) {
869
- const { input, client, agentCommand, agentArgv, cwd, session } = params;
1097
+ async prepareInitialRuntimeRecord(params) {
1098
+ const { input, client, owner, agentCommand, agentArgv, cwd, session } = params;
870
1099
  const record = createInitialRecord({
871
1100
  recordId: createRecordId(input.sessionKey, input.mode),
872
1101
  sessionName: input.sessionKey,
@@ -879,6 +1108,7 @@ var AcpRuntimeManager = class {
879
1108
  this.closingActiveRecords.delete(record.acpxRecordId);
880
1109
  record.protocolVersion = client.initializeResult?.protocolVersion;
881
1110
  record.agentCapabilities = client.initializeResult?.agentCapabilities;
1111
+ this.attachIdleProjection(owner, record);
882
1112
  applyConfigOptionsToRecord(record, session.sessionResult);
883
1113
  const modelApplication = await applyRequestedModelIfAdvertised({
884
1114
  client,
@@ -893,20 +1123,31 @@ var AcpRuntimeManager = class {
893
1123
  if (modelApplication.applied) setCurrentModelId(record, currentModelIdFromSetModelResponse(modelApplication.response, input.sessionOptions?.model));
894
1124
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
895
1125
  persistSessionOptions(record, input.sessionOptions);
896
- await this.options.sessionStore.save(record);
897
1126
  return record;
898
1127
  }
899
- async keepPersistentClient(mode, recordId, client) {
900
- if (mode !== "persistent") return false;
901
- const previousClient = this.pendingPersistentClients.get(recordId);
902
- this.pendingPersistentClients.set(recordId, client);
903
- await previousClient?.close().catch(() => {});
904
- return true;
1128
+ async retainInitializedSessionOwner(owner, record) {
1129
+ owner.recordId = record.acpxRecordId;
1130
+ await owner.projection?.checkpoint.checkpoint();
1131
+ const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
1132
+ this.retainedSessionOwners.set(record.acpxRecordId, owner);
1133
+ if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
1134
+ if (previousOwner && previousOwner !== owner) {
1135
+ this.removeRetainedSessionOwner(previousOwner);
1136
+ await this.stopSessionOwner(previousOwner);
1137
+ }
905
1138
  }
906
1139
  startTurn(input) {
907
- const promptInput = toPromptInput(input.text, input.attachments);
1140
+ let promptInput;
1141
+ try {
1142
+ promptInput = toPromptInput(input.text, input.attachments);
1143
+ } catch (error) {
1144
+ this.closeRetainedOneShotHandle(input.handle).catch(() => {});
1145
+ throw error;
1146
+ }
908
1147
  const queue = new AsyncEventQueue();
909
1148
  const result = createDeferred();
1149
+ const promptStarted = createDeferred();
1150
+ promptStarted.promise.catch(() => {});
910
1151
  const sessionReady = createDeferred();
911
1152
  sessionReady.promise.catch(() => {});
912
1153
  let resultSettled = false;
@@ -938,13 +1179,17 @@ var AcpRuntimeManager = class {
938
1179
  };
939
1180
  if (input.signal) {
940
1181
  if (input.signal.aborted) {
1182
+ promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
941
1183
  closeStream();
942
- settleResult({
943
- status: "cancelled",
944
- stopReason: "cancelled"
1184
+ this.closeRetainedOneShotHandle(input.handle).catch(() => {}).then(() => {
1185
+ settleResult({
1186
+ status: "cancelled",
1187
+ stopReason: "cancelled"
1188
+ });
945
1189
  });
946
1190
  return {
947
1191
  requestId: input.requestId,
1192
+ promptStarted: promptStarted.promise,
948
1193
  events: queue.iterate(),
949
1194
  result: result.promise,
950
1195
  cancel: async () => {},
@@ -957,6 +1202,7 @@ var AcpRuntimeManager = class {
957
1202
  input,
958
1203
  promptInput,
959
1204
  queue,
1205
+ promptStarted,
960
1206
  sessionReady,
961
1207
  state,
962
1208
  settleResult,
@@ -964,6 +1210,7 @@ var AcpRuntimeManager = class {
964
1210
  });
965
1211
  return {
966
1212
  requestId: input.requestId,
1213
+ promptStarted: promptStarted.promise,
967
1214
  events: queue.iterate(),
968
1215
  result: result.promise,
969
1216
  cancel: async () => {
@@ -974,61 +1221,159 @@ var AcpRuntimeManager = class {
974
1221
  }
975
1222
  };
976
1223
  }
1224
+ async closeRetainedOneShotHandle(handle) {
1225
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1226
+ if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
1227
+ }
977
1228
  async runRuntimeTurnTask(task) {
978
1229
  let turn;
1230
+ let terminalResult;
979
1231
  try {
980
1232
  turn = await this.prepareRuntimeTurn(task);
981
1233
  const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
982
1234
  await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
983
- if (this.cancelRuntimeTurnBeforePrompt(task)) return;
984
- await this.applyPendingRuntimeTurnCancel(task, turn);
985
- const response = await runPromptTurn({
1235
+ if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
1236
+ status: "cancelled",
1237
+ stopReason: "cancelled"
1238
+ };
1239
+ else {
1240
+ await this.applyPendingRuntimeTurnCancel(task, turn);
1241
+ const response = await this.runRuntimePrompt(task, turn, sessionId);
1242
+ await this.saveCompletedRuntimeTurn(turn, response.stopReason);
1243
+ terminalResult = {
1244
+ status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1245
+ ...response.stopReason ? { stopReason: response.stopReason } : {}
1246
+ };
1247
+ }
1248
+ } catch (error) {
1249
+ terminalResult = this.failRuntimeTurn(task, error);
1250
+ }
1251
+ try {
1252
+ await this.finalizeRuntimeTurn(task, turn);
1253
+ } catch (error) {
1254
+ terminalResult = this.failRuntimeTurn(task, error);
1255
+ }
1256
+ task.settleResult(terminalResult);
1257
+ }
1258
+ async runRuntimePrompt(task, turn, sessionId) {
1259
+ try {
1260
+ return await runPromptTurn({
986
1261
  client: turn.client,
987
1262
  sessionId,
988
1263
  prompt: task.promptInput,
989
1264
  timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
990
1265
  conversation: turn.conversation,
991
- promptMessageId: turn.promptMessageId
992
- });
993
- await this.saveCompletedRuntimeTurn(turn, response.stopReason);
994
- task.settleResult({
995
- status: response.stopReason === "cancelled" ? "cancelled" : "completed",
996
- ...response.stopReason ? { stopReason: response.stopReason } : {}
1266
+ promptMessageId: turn.promptMessageId,
1267
+ onPromptRequestStarted: () => task.promptStarted.resolve(),
1268
+ onElicitation: task.input.onElicitation
997
1269
  });
998
- } catch (error) {
999
- this.failRuntimeTurn(task, error);
1000
1270
  } finally {
1001
- await this.finalizeRuntimeTurn(task, turn);
1271
+ turn.client.endPromptElicitation?.(sessionId);
1002
1272
  }
1003
1273
  }
1004
1274
  async prepareRuntimeTurn(task) {
1005
- const record = await this.requireRecord(task.input.handle.acpxRecordId ?? task.input.handle.sessionKey);
1275
+ const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
1276
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.prepareRuntimeTurnWithOwnership(task));
1277
+ }
1278
+ async prepareRuntimeTurnWithOwnership(task) {
1279
+ const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
1280
+ try {
1281
+ const client = retainedOwner?.client ?? this.createTurnClient(record);
1282
+ const owner = this.resolveRuntimeTurnOwner(task, record, client, retainedOwner);
1283
+ const turn = this.createRunningRuntimeTurn({
1284
+ record,
1285
+ conversation,
1286
+ acpxState,
1287
+ client,
1288
+ owner,
1289
+ connected: retainedOwner !== void 0,
1290
+ promptMessageId
1291
+ });
1292
+ this.activateRuntimeTurn(task, turn);
1293
+ return turn;
1294
+ } catch (error) {
1295
+ this.restoreBufferedSessionOwner(retainedOwner);
1296
+ throw error;
1297
+ }
1298
+ }
1299
+ async prepareRuntimeTurnState(task) {
1300
+ const { record, retainedOwner } = await this.acquireRuntimeTurnState(task);
1006
1301
  const conversation = cloneSessionConversation(record);
1007
- let acpxState = cloneSessionAcpxState(record.acpx);
1008
- const promptStartedAt = isoNow();
1009
- const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
1010
- trimConversationForRuntime(conversation);
1011
- record.lastPromptAt = promptStartedAt;
1012
- record.lastUsedAt = promptStartedAt;
1013
- record.acpx = acpxState;
1014
- applyConversation(record, conversation);
1015
- await this.options.sessionStore.save(record);
1016
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
1017
- const client = pendingClient ?? this.createTurnClient(record);
1302
+ const acpxState = cloneSessionAcpxState(record.acpx);
1303
+ try {
1304
+ const promptStartedAt = isoNow();
1305
+ const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
1306
+ trimConversationForRuntime(conversation);
1307
+ record.lastPromptAt = promptStartedAt;
1308
+ record.lastUsedAt = promptStartedAt;
1309
+ record.acpx = acpxState;
1310
+ applyConversation(record, conversation);
1311
+ await this.options.sessionStore.save(record);
1312
+ return {
1313
+ record,
1314
+ retainedOwner,
1315
+ conversation,
1316
+ acpxState,
1317
+ promptMessageId
1318
+ };
1319
+ } catch (error) {
1320
+ this.restoreBufferedSessionOwner(retainedOwner);
1321
+ throw error;
1322
+ }
1323
+ }
1324
+ async acquireRuntimeTurnState(task) {
1325
+ const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
1326
+ let record = await this.requireRecord(recordId);
1327
+ const retainedOwner = await this.readRetainedSessionOwner(record, { consume: false });
1328
+ if (!retainedOwner) return { record };
1329
+ const projection = retainedOwner.projection;
1330
+ if (projection) record = structuredClone(projection.record);
1331
+ retainedOwner.bufferSessionUpdates = true;
1332
+ return {
1333
+ record,
1334
+ retainedOwner
1335
+ };
1336
+ }
1337
+ restoreBufferedSessionOwner(owner) {
1338
+ if (!owner) return;
1339
+ owner.bufferSessionUpdates = false;
1340
+ this.drainPendingSessionUpdates(owner);
1341
+ }
1342
+ resolveRuntimeTurnOwner(task, record, client, retainedOwner) {
1343
+ return retainedOwner ?? this.createSessionOwner({
1344
+ client,
1345
+ sessionKey: record.name ?? task.input.handle.sessionKey,
1346
+ mode: task.input.sessionMode
1347
+ });
1348
+ }
1349
+ createRunningRuntimeTurn(input) {
1350
+ const { record, conversation, acpxState, client, owner, connected, promptMessageId } = input;
1018
1351
  const turn = {
1019
1352
  record,
1020
1353
  conversation,
1021
1354
  acpxState,
1022
1355
  liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
1023
1356
  client,
1024
- pendingClient,
1357
+ owner,
1358
+ connected,
1025
1359
  promptMessageId,
1026
1360
  activeSessionId: record.acpSessionId
1027
1361
  };
1362
+ return turn;
1363
+ }
1364
+ activateRuntimeTurn(task, turn) {
1365
+ const { owner, record } = turn;
1366
+ this.removeRetainedSessionOwner(owner);
1367
+ owner.recordId = record.acpxRecordId;
1028
1368
  task.state.activeController = this.buildRuntimeTurnController(task, turn);
1029
1369
  this.activeControllers.set(record.acpxRecordId, task.state.activeController);
1030
- this.installRuntimeTurnEventHandlers(task, turn);
1031
- return turn;
1370
+ owner.projection = void 0;
1371
+ owner.activeTurn = {
1372
+ task,
1373
+ turn
1374
+ };
1375
+ owner.bufferSessionUpdates = false;
1376
+ this.drainPendingSessionUpdates(owner);
1032
1377
  }
1033
1378
  createTurnClient(record) {
1034
1379
  return this.createClient({
@@ -1038,7 +1383,9 @@ var AcpRuntimeManager = class {
1038
1383
  mcpServers: [...this.options.mcpServers ?? []],
1039
1384
  permissionMode: this.options.permissionMode,
1040
1385
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1386
+ permissionPolicy: this.options.permissionPolicy,
1041
1387
  onPermissionRequest: this.options.onPermissionRequest,
1388
+ elicitationModes: this.options.elicitationModes,
1042
1389
  verbose: this.options.verbose,
1043
1390
  sessionOptions: sessionOptionsFromRecord(record)
1044
1391
  });
@@ -1067,15 +1414,7 @@ var AcpRuntimeManager = class {
1067
1414
  await this.waitForRuntimeControlSession(task, turn);
1068
1415
  const models = advertisedModelState(turn.acpxState);
1069
1416
  const response = await turn.client.setSessionModel(turn.activeSessionId, modelId, models);
1070
- applyConfigOptionResponseToTurn(turn, response);
1071
- const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
1072
- nextState.session_options = {
1073
- ...nextState.session_options,
1074
- model: modelId
1075
- };
1076
- nextState.current_model_id = currentModelIdFromSetModelResponse(response, modelId);
1077
- clearDesiredConfigOption(nextState, models?.configId);
1078
- turn.acpxState = nextState;
1417
+ turn.acpxState = applyModelSelection(turn.acpxState, modelId, response);
1079
1418
  return response;
1080
1419
  },
1081
1420
  setSessionConfigOption: async (configId, value) => {
@@ -1100,52 +1439,28 @@ var AcpRuntimeManager = class {
1100
1439
  ...turn.record,
1101
1440
  acpx: turn.acpxState ?? void 0
1102
1441
  }, configId);
1442
+ const modelConfigId = advertisedModelState(turn.acpxState)?.configId;
1103
1443
  const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value);
1104
- this.applyRuntimeConfigOptionState(turn, resolvedConfigId, value, response);
1444
+ turn.acpxState = applyConfigOptionSelection(turn.acpxState, resolvedConfigId, value, response, modelConfigId);
1105
1445
  return {
1106
1446
  configId: resolvedConfigId,
1107
1447
  response
1108
1448
  };
1109
1449
  }
1110
- applyRuntimeConfigOptionState(turn, configId, value, response) {
1111
- applyConfigOptionResponseToTurn(turn, response);
1112
- applyDesiredConfigOptionToTurn(turn, configId, value);
1113
- }
1114
- installRuntimeTurnEventHandlers(task, turn) {
1115
- turn.client.setEventHandlers({
1116
- onSessionUpdate: (notification) => {
1117
- turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
1118
- trimConversationForRuntime(turn.conversation);
1119
- turn.liveCheckpoint.request();
1120
- this.emitRuntimeTurnEvent(task, {
1121
- jsonrpc: "2.0",
1122
- method: "session/update",
1123
- params: notification
1124
- });
1125
- },
1126
- onClientOperation: (operation) => {
1127
- turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
1128
- trimConversationForRuntime(turn.conversation);
1129
- turn.liveCheckpoint.request();
1130
- this.emitRuntimeTurnEvent(task, {
1131
- type: "client_operation",
1132
- ...operation
1133
- });
1134
- }
1135
- });
1136
- }
1137
1450
  emitRuntimeTurnEvent(task, payload) {
1138
1451
  const parsed = parsePromptEventLine(JSON.stringify(payload));
1139
1452
  if (!parsed) return;
1140
1453
  task.queue.push(parsed);
1141
1454
  }
1142
1455
  async connectRuntimeTurn(task, turn) {
1143
- const loaded = turn.pendingClient ? {
1456
+ if (turn.connected) return {
1144
1457
  sessionId: turn.record.acpSessionId,
1145
1458
  resumed: false,
1146
1459
  loadError: void 0
1147
- } : await this.connectRuntimeTurnClient(task, turn);
1460
+ };
1461
+ const loaded = await this.connectRuntimeTurnClient(task, turn);
1148
1462
  turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
1463
+ turn.connected = true;
1149
1464
  return loaded;
1150
1465
  }
1151
1466
  async connectRuntimeTurnClient(task, turn) {
@@ -1188,10 +1503,7 @@ var AcpRuntimeManager = class {
1188
1503
  cancelRuntimeTurnBeforePrompt(task) {
1189
1504
  if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
1190
1505
  task.state.pendingCancel = false;
1191
- task.settleResult({
1192
- status: "cancelled",
1193
- stopReason: "cancelled"
1194
- });
1506
+ task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
1195
1507
  return true;
1196
1508
  }
1197
1509
  async applyPendingRuntimeTurnCancel(task, turn) {
@@ -1211,9 +1523,10 @@ var AcpRuntimeManager = class {
1211
1523
  await this.options.sessionStore.save(turn.record);
1212
1524
  }
1213
1525
  failRuntimeTurn(task, error) {
1526
+ task.promptStarted.reject(error);
1214
1527
  task.sessionReady.reject(error);
1215
1528
  const normalized = normalizeOutputError(error, { origin: "runtime" });
1216
- task.settleResult({
1529
+ return {
1217
1530
  status: "failed",
1218
1531
  error: {
1219
1532
  message: normalized.message,
@@ -1221,13 +1534,36 @@ var AcpRuntimeManager = class {
1221
1534
  ...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
1222
1535
  ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
1223
1536
  }
1224
- });
1537
+ };
1225
1538
  }
1226
1539
  async finalizeRuntimeTurn(task, turn) {
1227
1540
  task.state.turnActive = false;
1228
- task.input.signal?.removeEventListener("abort", task.abortHandler);
1229
- turn?.client.clearEventHandlers();
1230
- if (!(turn ? await this.finalizeRuntimeTurnRecord(turn) : false)) await turn?.client.close().catch(() => {});
1541
+ const abortHandlerAttempt = await settleAttempt(() => task.input.signal?.removeEventListener("abort", task.abortHandler));
1542
+ const recordAttempt = await settleAttempt(async () => turn ? await this.finalizeRuntimeTurnRecord(turn) : false);
1543
+ let failure = firstFailedAttempt([abortHandlerAttempt, recordAttempt]);
1544
+ let pooled = recordAttempt.ok ? recordAttempt.value : false;
1545
+ if (failure) {
1546
+ this.discardRetainedRuntimeTurnOwner(turn);
1547
+ pooled = false;
1548
+ }
1549
+ const closeAttempt = await settleAttempt(async () => this.closeRuntimeTurnClient(turn, pooled));
1550
+ this.cleanupRuntimeTurn(task, turn);
1551
+ failure ??= firstFailedAttempt([closeAttempt]);
1552
+ if (failure) throw failure.error;
1553
+ }
1554
+ discardRetainedRuntimeTurnOwner(turn) {
1555
+ if (turn) {
1556
+ this.removeRetainedSessionOwner(turn.owner);
1557
+ turn.owner.activeTurn = void 0;
1558
+ }
1559
+ }
1560
+ async closeRuntimeTurnClient(turn, pooled) {
1561
+ if (!turn || pooled) return;
1562
+ turn.owner.activeTurn = void 0;
1563
+ const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => turn.client.close())]);
1564
+ if (failure) throw failure.error;
1565
+ }
1566
+ cleanupRuntimeTurn(task, turn) {
1231
1567
  if (turn) {
1232
1568
  this.activeControllers.delete(turn.record.acpxRecordId);
1233
1569
  this.closingActiveRecords.delete(turn.record.acpxRecordId);
@@ -1235,17 +1571,20 @@ var AcpRuntimeManager = class {
1235
1571
  task.queue.close();
1236
1572
  }
1237
1573
  async finalizeRuntimeTurnRecord(turn) {
1574
+ if (!turn.connected) turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
1238
1575
  applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1239
1576
  turn.record.acpx = turn.acpxState;
1240
1577
  applyConversation(turn.record, turn.conversation);
1241
1578
  turn.record.lastUsedAt = isoNow();
1242
- await turn.liveCheckpoint.flush().catch(() => {});
1579
+ await turn.liveCheckpoint.flush();
1243
1580
  const closed = await this.refreshClosedState(turn.record);
1244
- await this.options.sessionStore.save(turn.record).catch(() => {});
1245
- if (closed) return false;
1246
- return await this.retainPersistentClientAfterTurn({
1581
+ await this.options.sessionStore.save(turn.record);
1582
+ if (closed || !turn.connected) return false;
1583
+ return await this.retainPersistentSessionOwnerAfterTurn({
1247
1584
  record: turn.record,
1248
- client: turn.client
1585
+ owner: turn.owner,
1586
+ conversation: turn.conversation,
1587
+ acpxState: turn.acpxState
1249
1588
  });
1250
1589
  }
1251
1590
  async *runTurn(input) {
@@ -1254,7 +1593,10 @@ var AcpRuntimeManager = class {
1254
1593
  yield legacyTerminalEventFromTurnResult(await turn.result);
1255
1594
  }
1256
1595
  async getStatus(handle) {
1257
- const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1596
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1597
+ const owner = this.retainedSessionOwners.get(recordId);
1598
+ if (owner) await this.flushSessionOwner(owner);
1599
+ const record = await this.requireRecord(recordId);
1258
1600
  return {
1259
1601
  summary: statusSummary(record),
1260
1602
  acpxRecordId: record.acpxRecordId,
@@ -1272,6 +1614,10 @@ var AcpRuntimeManager = class {
1272
1614
  };
1273
1615
  }
1274
1616
  async setMode(handle, mode, sessionMode = "persistent") {
1617
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1618
+ await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode));
1619
+ }
1620
+ async setModeWithOwnership(handle, mode, sessionMode) {
1275
1621
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1276
1622
  const controller = this.activeControllers.get(record.acpxRecordId);
1277
1623
  let targetRecord = record;
@@ -1283,63 +1629,109 @@ var AcpRuntimeManager = class {
1283
1629
  await this.options.sessionStore.save(targetRecord);
1284
1630
  }
1285
1631
  async setConfigOption(handle, key, value, sessionMode = "persistent") {
1632
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1633
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode));
1634
+ }
1635
+ async setConfigOptionWithOwnership(handle, key, value, sessionMode) {
1286
1636
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1287
1637
  const controller = this.activeControllers.get(record.acpxRecordId);
1288
1638
  if (controller) {
1289
1639
  const { configId, response } = await controller.setResolvedSessionConfigOption(key, value);
1290
- applyConfigOptionsToRecord(record, response);
1291
- applyDesiredConfigOptionToRecord(record, configId, value);
1640
+ record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response);
1292
1641
  await this.options.sessionStore.save(record);
1293
- return;
1642
+ return response;
1294
1643
  }
1295
1644
  const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1296
1645
  const configId = resolveSupportedConfigOptionId(connectedRecord, key);
1297
- applyConfigOptionsToRecord(connectedRecord, await client.setSessionConfigOption(sessionId, configId, value));
1298
- applyDesiredConfigOptionToRecord(connectedRecord, configId, value);
1646
+ const modelConfigId = advertisedModelState(connectedRecord.acpx)?.configId;
1647
+ const response = await client.setSessionConfigOption(sessionId, configId, value);
1648
+ connectedRecord.acpx = applyConfigOptionSelection(connectedRecord.acpx, configId, value, response, modelConfigId);
1649
+ return response;
1650
+ }, {
1651
+ key,
1652
+ resolve: (connectedRecord) => resolveSupportedConfigOptionId(connectedRecord, key)
1299
1653
  });
1300
1654
  await this.options.sessionStore.save(result.record);
1655
+ return result.value;
1301
1656
  }
1302
1657
  async cancel(handle) {
1303
1658
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
1304
1659
  }
1305
1660
  async close(handle, options = {}) {
1306
- const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1307
- if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
1661
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1662
+ const record = await this.resolveRuntimeRecordForClose(recordId);
1663
+ this.markActiveRuntimeRecordClosing(record);
1308
1664
  await this.cancel(handle);
1309
- if (options.discardPersistentState) {
1665
+ await this.closeRuntimeRecordOwnership(record, options.discardPersistentState === true);
1666
+ record.closed = true;
1667
+ record.closedAt = isoNow();
1668
+ await this.options.sessionStore.save(record);
1669
+ }
1670
+ async resolveRuntimeRecordForClose(recordId) {
1671
+ const retainedOwner = this.retainedSessionOwners.get(recordId);
1672
+ if (retainedOwner) await this.flushSessionOwner(retainedOwner);
1673
+ return retainedOwner?.projection?.record ?? await this.requireRecord(recordId);
1674
+ }
1675
+ markActiveRuntimeRecordClosing(record) {
1676
+ if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
1677
+ }
1678
+ async closeRuntimeRecordOwnership(record, discardPersistentState) {
1679
+ if (discardPersistentState) {
1310
1680
  await this.closeBackendSession(record);
1311
1681
  record.acpx = {
1312
1682
  ...record.acpx,
1313
1683
  reset_on_next_ensure: true
1314
1684
  };
1315
- } else await this.closePendingPersistentClient(record.acpxRecordId);
1316
- record.closed = true;
1317
- record.closedAt = isoNow();
1318
- await this.options.sessionStore.save(record);
1685
+ } else await this.closeRetainedSessionOwner(record.acpxRecordId);
1319
1686
  }
1320
1687
  async closeBackendSession(record) {
1321
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
1322
- const client = pendingClient ?? this.createClient({
1688
+ const connection = await this.acquireBackendCloseConnection(record);
1689
+ try {
1690
+ await this.requestBackendSessionClose(record, connection);
1691
+ } catch (error) {
1692
+ this.handleBackendSessionCloseError(record, error);
1693
+ } finally {
1694
+ await this.finalizeBackendCloseConnection(connection);
1695
+ }
1696
+ }
1697
+ async finalizeBackendCloseConnection(connection) {
1698
+ const failure = firstFailedAttempt([
1699
+ await settleAttempt(async () => {
1700
+ if (connection.owner) await this.flushSessionOwner(connection.owner);
1701
+ }),
1702
+ await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
1703
+ await settleAttempt(async () => connection.client.close())
1704
+ ]);
1705
+ if (failure) throw failure.error;
1706
+ }
1707
+ async acquireBackendCloseConnection(record) {
1708
+ const owner = await this.readRetainedSessionOwner(record, { consume: true });
1709
+ if (owner) return {
1710
+ client: owner.client,
1711
+ owner
1712
+ };
1713
+ return { client: this.createClient({
1323
1714
  agentCommand: record.agentCommand,
1324
1715
  agentArgv: record.agentArgv,
1325
1716
  cwd: record.cwd,
1326
1717
  mcpServers: [...this.options.mcpServers ?? []],
1327
1718
  permissionMode: this.options.permissionMode,
1328
1719
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1720
+ permissionPolicy: this.options.permissionPolicy,
1329
1721
  onPermissionRequest: this.options.onPermissionRequest,
1722
+ elicitationModes: this.options.elicitationModes,
1330
1723
  verbose: this.options.verbose
1331
- });
1332
- try {
1333
- if (!pendingClient) await withTimeout(client.start(), this.options.timeoutMs);
1334
- if (!client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
1335
- await withTimeout(client.closeSession(record.acpSessionId), this.options.timeoutMs);
1336
- } catch (error) {
1337
- if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
1338
- if (isAcpResourceNotFoundError(error)) return;
1339
- throw error;
1340
- } finally {
1341
- await client.close().catch(() => {});
1342
- }
1724
+ }) };
1725
+ }
1726
+ async requestBackendSessionClose(record, connection) {
1727
+ if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
1728
+ if (!connection.client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
1729
+ await withTimeout(connection.client.closeSession(record.acpSessionId), this.options.timeoutMs);
1730
+ }
1731
+ handleBackendSessionCloseError(record, error) {
1732
+ if (isAcpResourceNotFoundError(error)) return;
1733
+ if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
1734
+ throw error;
1343
1735
  }
1344
1736
  async requireRecord(sessionId) {
1345
1737
  const record = await this.options.sessionStore.load(sessionId);
@@ -1506,6 +1898,7 @@ function createProbeClient(options, agentCommand, deps) {
1506
1898
  mcpServers: [...options.mcpServers ?? []],
1507
1899
  permissionMode: options.permissionMode,
1508
1900
  nonInteractivePermissions: options.nonInteractivePermissions,
1901
+ permissionPolicy: options.permissionPolicy,
1509
1902
  verbose: options.verbose
1510
1903
  };
1511
1904
  return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
@@ -1614,10 +2007,14 @@ var AcpxRuntime = class {
1614
2007
  sessionMode: state.mode,
1615
2008
  requestId: input.requestId,
1616
2009
  timeoutMs: input.timeoutMs,
1617
- signal: input.signal
2010
+ signal: input.signal,
2011
+ onElicitation: input.onElicitation
1618
2012
  }));
1619
2013
  return {
1620
2014
  requestId: input.requestId,
2015
+ get promptStarted() {
2016
+ return turnPromise.then((turn) => turn.promptStarted);
2017
+ },
1621
2018
  events: { async *[Symbol.asyncIterator]() {
1622
2019
  yield* (await turnPromise).events;
1623
2020
  } },
@@ -1642,7 +2039,8 @@ var AcpxRuntime = class {
1642
2039
  sessionMode: state.mode,
1643
2040
  requestId: input.requestId,
1644
2041
  timeoutMs: input.timeoutMs,
1645
- signal: input.signal
2042
+ signal: input.signal,
2043
+ onElicitation: input.onElicitation
1646
2044
  });
1647
2045
  }
1648
2046
  async getCapabilities(input) {
@@ -1666,7 +2064,7 @@ var AcpxRuntime = class {
1666
2064
  }
1667
2065
  async setConfigOption(input) {
1668
2066
  const { handle, state } = this.resolveManagerHandle(input.handle);
1669
- await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
2067
+ return await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
1670
2068
  }
1671
2069
  async cancel(input) {
1672
2070
  const { handle } = this.resolveManagerHandle(input.handle);