acpx 0.13.0 → 0.13.1

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 REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, D as applyLifecycleSnapshotToRecord, E as applyConversation, F as modelStateFromConfigOptions, Ft as normalizeAgentName, Ht as normalizeOutputError, I as normalizeAgentCommandInput, It as resolveAgentArgv, Lt as resolveAgentCommand, M as RequestedModelUnsupportedError, Mt as withTimeout, Nt as DEFAULT_AGENT_NAME, O as reconcileAgentSessionId, Ot as textPrompt, P as isRequestedModelUnsupportedError, Pt as listBuiltInAgents, Rt as resolveCanonicalAgentName, S as advertisedModelState, T as sessionOptionsFromRecord, Ut as extractAcpError, Wt as isAcpResourceNotFoundError, _ as createSessionConversation, a as applyRequestedModelIfAdvertised, b as recordSessionUpdate, c as setCurrentModelId, d as setDesiredModelId, dt as serializeSessionRecordForDisk, et as createAtomicWriteTempPath, f as syncAdvertisedModelState, g as cloneSessionConversation, h as cloneSessionAcpxState, i as connectAndLoadSession, j as REQUESTED_MODEL_UNSUPPORTED_REASONS, k as AcpClient, l as setDesiredConfigOption, m as applyConfigOptionsToState, mt as defaultSessionEventLog, n as runPromptTurn, o as currentModelIdFromSetModelResponse, p as applyConfigOptionsToRecord, r as withConnectedSession, s as clearDesiredConfigOption, t as LiveSessionCheckpoint, tt as assertPersistedKeyPolicy, u as setDesiredModeId, ut as parseSessionRecord, v as recordClientOperation, w as persistSessionOptions, x as trimConversationForRuntime, y as recordPromptSubmission } from "./live-checkpoint-BSIrfgVo.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) {
@@ -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 = [];
@@ -738,7 +802,10 @@ var AcpRuntimeManager = class {
738
802
  options;
739
803
  deps;
740
804
  activeControllers = /* @__PURE__ */ new Map();
741
- pendingPersistentClients = /* @__PURE__ */ new Map();
805
+ retainedSessionOwners = /* @__PURE__ */ new Map();
806
+ pendingOneShotRecordIds = /* @__PURE__ */ new Map();
807
+ ensureSessionLocks = /* @__PURE__ */ new Map();
808
+ runtimeOperationLocks = /* @__PURE__ */ new Map();
742
809
  closingActiveRecords = /* @__PURE__ */ new Set();
743
810
  constructor(options, deps = {}) {
744
811
  this.options = options;
@@ -747,22 +814,123 @@ var AcpRuntimeManager = class {
747
814
  createClient(options) {
748
815
  return this.deps.clientFactory?.(options) ?? new AcpClient(options);
749
816
  }
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(() => {});
817
+ createSessionOwner(input) {
818
+ const owner = {
819
+ ...input,
820
+ bufferSessionUpdates: false,
821
+ pendingSessionUpdates: []
822
+ };
823
+ input.client.setEventHandlers({
824
+ onSessionUpdate: (notification) => this.routeOwnedSessionUpdate(owner, notification),
825
+ onClientOperation: (operation) => this.routeOwnedClientOperation(owner, operation)
826
+ });
827
+ return owner;
828
+ }
829
+ routeOwnedSessionUpdate(owner, notification) {
830
+ const active = owner.activeTurn;
831
+ if (active) {
832
+ const { task, turn } = active;
833
+ if (turn.connectionUpdates) {
834
+ turn.connectionUpdates.push(notification);
835
+ this.emitRuntimeTurnEvent(task, {
836
+ jsonrpc: "2.0",
837
+ method: "session/update",
838
+ params: notification
839
+ });
840
+ return;
841
+ }
842
+ turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
843
+ trimConversationForRuntime(turn.conversation);
844
+ turn.liveCheckpoint.request();
845
+ this.emitRuntimeTurnEvent(task, {
846
+ jsonrpc: "2.0",
847
+ method: "session/update",
848
+ params: notification
849
+ });
850
+ return;
851
+ }
852
+ if (owner.bufferSessionUpdates) {
853
+ owner.pendingSessionUpdates.push(notification);
854
+ return;
855
+ }
856
+ const projection = owner.projection;
857
+ if (!projection) {
858
+ owner.pendingSessionUpdates.push(notification);
756
859
  return;
757
860
  }
758
- if (options.consume) this.pendingPersistentClients.delete(record.acpxRecordId);
759
- return pendingClient;
861
+ projection.acpxState = recordSessionUpdate(projection.conversation, projection.acpxState, notification);
862
+ trimConversationForRuntime(projection.conversation);
863
+ projection.checkpoint.request();
864
+ }
865
+ routeOwnedClientOperation(owner, operation) {
866
+ const active = owner.activeTurn;
867
+ if (!active) return;
868
+ const { task, turn } = active;
869
+ turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
870
+ trimConversationForRuntime(turn.conversation);
871
+ turn.liveCheckpoint.request();
872
+ this.emitRuntimeTurnEvent(task, {
873
+ type: "client_operation",
874
+ ...operation
875
+ });
760
876
  }
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(() => {});
877
+ attachIdleProjection(owner, record, conversation = cloneSessionConversation(record), acpxState = cloneSessionAcpxState(record.acpx)) {
878
+ let projection;
879
+ projection = {
880
+ record,
881
+ conversation,
882
+ acpxState,
883
+ checkpoint: new LiveSessionCheckpoint({ save: async () => {
884
+ record.lastUsedAt = isoNow();
885
+ record.acpx = projection.acpxState;
886
+ applyConversation(record, projection.conversation);
887
+ applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
888
+ await this.refreshClosedState(record);
889
+ await this.options.sessionStore.save(record);
890
+ } })
891
+ };
892
+ owner.projection = projection;
893
+ owner.activeTurn = void 0;
894
+ this.drainPendingSessionUpdates(owner);
895
+ }
896
+ drainPendingSessionUpdates(owner) {
897
+ for (const notification of owner.pendingSessionUpdates.splice(0)) this.routeOwnedSessionUpdate(owner, notification);
898
+ }
899
+ async flushSessionOwner(owner) {
900
+ await owner.client.waitForSessionUpdatesIdle?.().catch(() => {});
901
+ await owner.projection?.checkpoint.flush();
902
+ }
903
+ removeRetainedSessionOwner(owner) {
904
+ if (owner.recordId && this.retainedSessionOwners.get(owner.recordId) === owner) this.retainedSessionOwners.delete(owner.recordId);
905
+ if (owner.mode === "oneshot" && owner.recordId && this.pendingOneShotRecordIds.get(owner.sessionKey) === owner.recordId) this.pendingOneShotRecordIds.delete(owner.sessionKey);
906
+ }
907
+ async readRetainedSessionOwner(record, options) {
908
+ const owner = this.retainedSessionOwners.get(record.acpxRecordId);
909
+ if (!owner) return;
910
+ await this.flushSessionOwner(owner);
911
+ const projectedRecord = owner.projection?.record;
912
+ if (projectedRecord && projectedRecord !== record) Object.assign(record, structuredClone(projectedRecord));
913
+ if (!owner.client.hasReusableSession(record.acpSessionId)) {
914
+ this.removeRetainedSessionOwner(owner);
915
+ await this.stopSessionOwner(owner);
916
+ return;
917
+ }
918
+ if (options.consume) this.removeRetainedSessionOwner(owner);
919
+ return owner;
920
+ }
921
+ async closeRetainedSessionOwner(recordId) {
922
+ const owner = this.retainedSessionOwners.get(recordId);
923
+ if (!owner) return;
924
+ this.removeRetainedSessionOwner(owner);
925
+ await this.stopSessionOwner(owner);
926
+ }
927
+ async stopSessionOwner(owner) {
928
+ await this.flushSessionOwner(owner).catch(() => {});
929
+ await owner.client.close().catch(() => {});
930
+ await owner.projection?.checkpoint.flush().catch(() => {});
931
+ try {
932
+ owner.client.clearEventHandlers();
933
+ } catch {}
766
934
  }
767
935
  async refreshClosedState(record) {
768
936
  if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
@@ -775,32 +943,43 @@ var AcpRuntimeManager = class {
775
943
  };
776
944
  return true;
777
945
  }
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(() => {});
946
+ async retainPersistentSessionOwnerAfterTurn(input) {
947
+ const { record, owner, conversation, acpxState } = input;
948
+ if (!this.canRetainPersistentSessionOwner(owner, record)) {
949
+ owner.activeTurn = void 0;
950
+ return false;
951
+ }
952
+ this.attachIdleProjection(owner, record, conversation, acpxState);
953
+ const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
954
+ this.retainedSessionOwners.set(record.acpxRecordId, owner);
955
+ if (previousOwner && previousOwner !== owner) {
956
+ this.removeRetainedSessionOwner(previousOwner);
957
+ await this.stopSessionOwner(previousOwner);
958
+ }
784
959
  return true;
785
960
  }
961
+ canRetainPersistentSessionOwner(owner, record) {
962
+ return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
963
+ }
786
964
  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
- };
965
+ const owner = await this.readRetainedSessionOwner(record, { consume: false });
966
+ if (owner) {
967
+ const ownedRecord = owner.projection?.record ?? record;
968
+ owner.bufferSessionUpdates = true;
969
+ try {
970
+ const value = await run({
971
+ client: owner.client,
972
+ sessionId: ownedRecord.acpSessionId,
973
+ record: ownedRecord
974
+ });
975
+ this.refreshOwnedRecordLifecycle(owner, ownedRecord);
976
+ return {
977
+ value,
978
+ record: ownedRecord
979
+ };
980
+ } finally {
981
+ await this.finishBufferedOwnerControl(owner, ownedRecord);
982
+ }
804
983
  }
805
984
  const result = await withConnectedSession({
806
985
  sessionRecordId: record.acpxRecordId,
@@ -810,7 +989,9 @@ var AcpRuntimeManager = class {
810
989
  mcpServers: [...this.options.mcpServers ?? []],
811
990
  permissionMode: this.options.permissionMode,
812
991
  nonInteractivePermissions: this.options.nonInteractivePermissions,
992
+ permissionPolicy: this.options.permissionPolicy,
813
993
  onPermissionRequest: this.options.onPermissionRequest,
994
+ elicitationModes: this.options.elicitationModes,
814
995
  verbose: this.options.verbose,
815
996
  timeoutMs: this.options.timeoutMs,
816
997
  resumePolicy: resumePolicyForSessionMode(sessionMode),
@@ -821,21 +1002,98 @@ var AcpRuntimeManager = class {
821
1002
  record: result.record
822
1003
  };
823
1004
  }
1005
+ refreshOwnedRecordLifecycle(owner, record) {
1006
+ record.lastUsedAt = isoNow();
1007
+ record.closed = false;
1008
+ record.closedAt = void 0;
1009
+ record.protocolVersion = owner.client.initializeResult?.protocolVersion;
1010
+ record.agentCapabilities = owner.client.initializeResult?.agentCapabilities;
1011
+ applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
1012
+ }
1013
+ async finishBufferedOwnerControl(owner, record) {
1014
+ const projection = owner.projection;
1015
+ if (projection) projection.acpxState = cloneSessionAcpxState(record.acpx);
1016
+ owner.bufferSessionUpdates = false;
1017
+ this.drainPendingSessionUpdates(owner);
1018
+ if (projection) {
1019
+ record.acpx = projection.acpxState;
1020
+ applyConversation(record, projection.conversation);
1021
+ await projection.checkpoint.flush();
1022
+ }
1023
+ }
824
1024
  async ensureSession(input) {
1025
+ return await this.withEnsureSessionLock(input, async () => this.ensureSessionWithOwnership(input));
1026
+ }
1027
+ async withEnsureSessionLock(input, run) {
1028
+ const key = `${input.mode}\0${input.sessionKey}`;
1029
+ return await this.withManagerLock(this.ensureSessionLocks, key, run);
1030
+ }
1031
+ async withManagerLock(locks, key, run) {
1032
+ const previous = locks.get(key) ?? Promise.resolve();
1033
+ let release;
1034
+ const gate = new Promise((resolve) => {
1035
+ release = resolve;
1036
+ });
1037
+ const tail = previous.then(() => gate);
1038
+ locks.set(key, tail);
1039
+ await previous;
1040
+ try {
1041
+ return await run();
1042
+ } finally {
1043
+ release();
1044
+ if (locks.get(key) === tail) locks.delete(key);
1045
+ }
1046
+ }
1047
+ async ensureSessionWithOwnership(input) {
825
1048
  const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
826
1049
  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, {
1050
+ const agent = {
829
1051
  cwd,
830
1052
  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;
1053
+ agentArgv
1054
+ };
1055
+ const existing = await this.loadExistingRuntimeSession(input);
1056
+ if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing.record);
1057
+ await this.closeConflictingPersistentSession(input, existing?.owner);
1058
+ return await this.createOwnedRuntimeSession(input, agent);
1059
+ }
1060
+ async loadExistingRuntimeSession(input) {
1061
+ const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
1062
+ if (!existingRecordId) return;
1063
+ let record = await this.options.sessionStore.load(existingRecordId);
1064
+ if (!record) return;
1065
+ const owner = this.retainedSessionOwners.get(record.acpxRecordId);
1066
+ if (owner) {
1067
+ await this.flushSessionOwner(owner);
1068
+ record = owner.projection?.record ?? record;
838
1069
  }
1070
+ return {
1071
+ record,
1072
+ owner
1073
+ };
1074
+ }
1075
+ canReuseRuntimeSession(input, agent, existing) {
1076
+ if (!shouldReuseExistingRecord(existing.record, {
1077
+ cwd: agent.cwd,
1078
+ agentCommand: agent.agentCommand,
1079
+ agentArgv: agent.agentArgv,
1080
+ resumeSessionId: input.resumeSessionId
1081
+ })) return false;
1082
+ if (input.mode === "persistent") return true;
1083
+ return Boolean(existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), input.sessionOptions));
1084
+ }
1085
+ async reuseRuntimeSession(record) {
1086
+ record.closed = false;
1087
+ record.closedAt = void 0;
1088
+ this.closingActiveRecords.delete(record.acpxRecordId);
1089
+ await this.options.sessionStore.save(record);
1090
+ return record;
1091
+ }
1092
+ async closeConflictingPersistentSession(input, owner) {
1093
+ if (input.mode === "persistent" && owner?.recordId) await this.closeRetainedSessionOwner(owner.recordId);
1094
+ }
1095
+ async createOwnedRuntimeSession(input, agent) {
1096
+ const { cwd, agentCommand, agentArgv } = agent;
839
1097
  const client = this.createClient({
840
1098
  agentCommand,
841
1099
  agentArgv,
@@ -843,11 +1101,18 @@ var AcpRuntimeManager = class {
843
1101
  mcpServers: [...this.options.mcpServers ?? []],
844
1102
  permissionMode: this.options.permissionMode,
845
1103
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1104
+ permissionPolicy: this.options.permissionPolicy,
846
1105
  onPermissionRequest: this.options.onPermissionRequest,
1106
+ elicitationModes: this.options.elicitationModes,
847
1107
  verbose: this.options.verbose,
848
1108
  sessionOptions: input.sessionOptions
849
1109
  });
850
- let keepClientOpen = false;
1110
+ const owner = this.createSessionOwner({
1111
+ client,
1112
+ sessionKey: input.sessionKey,
1113
+ mode: input.mode
1114
+ });
1115
+ let retained = false;
851
1116
  try {
852
1117
  await client.start();
853
1118
  const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
@@ -859,10 +1124,15 @@ var AcpRuntimeManager = class {
859
1124
  cwd,
860
1125
  session
861
1126
  });
862
- keepClientOpen = await this.keepPersistentClient(input.mode, record.acpxRecordId, client);
1127
+ this.attachIdleProjection(owner, record);
1128
+ await this.retainInitializedSessionOwner(owner, record);
1129
+ retained = true;
863
1130
  return record;
864
1131
  } finally {
865
- if (!keepClientOpen) await client.close();
1132
+ if (!retained) {
1133
+ client.clearEventHandlers();
1134
+ await client.close();
1135
+ }
866
1136
  }
867
1137
  }
868
1138
  async createAndSaveRuntimeRecord(params) {
@@ -896,17 +1166,28 @@ var AcpRuntimeManager = class {
896
1166
  await this.options.sessionStore.save(record);
897
1167
  return record;
898
1168
  }
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;
1169
+ async retainInitializedSessionOwner(owner, record) {
1170
+ owner.recordId = record.acpxRecordId;
1171
+ const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
1172
+ this.retainedSessionOwners.set(record.acpxRecordId, owner);
1173
+ if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
1174
+ if (previousOwner && previousOwner !== owner) {
1175
+ this.removeRetainedSessionOwner(previousOwner);
1176
+ await this.stopSessionOwner(previousOwner);
1177
+ }
905
1178
  }
906
1179
  startTurn(input) {
907
- const promptInput = toPromptInput(input.text, input.attachments);
1180
+ let promptInput;
1181
+ try {
1182
+ promptInput = toPromptInput(input.text, input.attachments);
1183
+ } catch (error) {
1184
+ this.closeRetainedOneShotHandle(input.handle).catch(() => {});
1185
+ throw error;
1186
+ }
908
1187
  const queue = new AsyncEventQueue();
909
1188
  const result = createDeferred();
1189
+ const promptStarted = createDeferred();
1190
+ promptStarted.promise.catch(() => {});
910
1191
  const sessionReady = createDeferred();
911
1192
  sessionReady.promise.catch(() => {});
912
1193
  let resultSettled = false;
@@ -938,13 +1219,17 @@ var AcpRuntimeManager = class {
938
1219
  };
939
1220
  if (input.signal) {
940
1221
  if (input.signal.aborted) {
1222
+ promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
941
1223
  closeStream();
942
- settleResult({
943
- status: "cancelled",
944
- stopReason: "cancelled"
1224
+ this.closeRetainedOneShotHandle(input.handle).catch(() => {}).then(() => {
1225
+ settleResult({
1226
+ status: "cancelled",
1227
+ stopReason: "cancelled"
1228
+ });
945
1229
  });
946
1230
  return {
947
1231
  requestId: input.requestId,
1232
+ promptStarted: promptStarted.promise,
948
1233
  events: queue.iterate(),
949
1234
  result: result.promise,
950
1235
  cancel: async () => {},
@@ -957,6 +1242,7 @@ var AcpRuntimeManager = class {
957
1242
  input,
958
1243
  promptInput,
959
1244
  queue,
1245
+ promptStarted,
960
1246
  sessionReady,
961
1247
  state,
962
1248
  settleResult,
@@ -964,6 +1250,7 @@ var AcpRuntimeManager = class {
964
1250
  });
965
1251
  return {
966
1252
  requestId: input.requestId,
1253
+ promptStarted: promptStarted.promise,
967
1254
  events: queue.iterate(),
968
1255
  result: result.promise,
969
1256
  cancel: async () => {
@@ -974,61 +1261,160 @@ var AcpRuntimeManager = class {
974
1261
  }
975
1262
  };
976
1263
  }
1264
+ async closeRetainedOneShotHandle(handle) {
1265
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1266
+ if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
1267
+ }
977
1268
  async runRuntimeTurnTask(task) {
978
1269
  let turn;
1270
+ let terminalResult;
979
1271
  try {
980
1272
  turn = await this.prepareRuntimeTurn(task);
981
1273
  const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
982
1274
  await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
983
- if (this.cancelRuntimeTurnBeforePrompt(task)) return;
984
- await this.applyPendingRuntimeTurnCancel(task, turn);
985
- const response = await runPromptTurn({
1275
+ if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
1276
+ status: "cancelled",
1277
+ stopReason: "cancelled"
1278
+ };
1279
+ else {
1280
+ await this.applyPendingRuntimeTurnCancel(task, turn);
1281
+ const response = await this.runRuntimePrompt(task, turn, sessionId);
1282
+ await this.saveCompletedRuntimeTurn(turn, response.stopReason);
1283
+ terminalResult = {
1284
+ status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1285
+ ...response.stopReason ? { stopReason: response.stopReason } : {}
1286
+ };
1287
+ }
1288
+ } catch (error) {
1289
+ terminalResult = this.failRuntimeTurn(task, error);
1290
+ }
1291
+ try {
1292
+ await this.finalizeRuntimeTurn(task, turn);
1293
+ } catch (error) {
1294
+ terminalResult = this.failRuntimeTurn(task, error);
1295
+ }
1296
+ task.settleResult(terminalResult);
1297
+ }
1298
+ async runRuntimePrompt(task, turn, sessionId) {
1299
+ try {
1300
+ return await runPromptTurn({
986
1301
  client: turn.client,
987
1302
  sessionId,
988
1303
  prompt: task.promptInput,
989
1304
  timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
990
1305
  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 } : {}
1306
+ promptMessageId: turn.promptMessageId,
1307
+ onPromptRequestStarted: () => task.promptStarted.resolve(),
1308
+ onElicitation: task.input.onElicitation
997
1309
  });
998
- } catch (error) {
999
- this.failRuntimeTurn(task, error);
1000
1310
  } finally {
1001
- await this.finalizeRuntimeTurn(task, turn);
1311
+ turn.client.endPromptElicitation?.(sessionId);
1002
1312
  }
1003
1313
  }
1004
1314
  async prepareRuntimeTurn(task) {
1005
- const record = await this.requireRecord(task.input.handle.acpxRecordId ?? task.input.handle.sessionKey);
1315
+ const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
1316
+ return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.prepareRuntimeTurnWithOwnership(task));
1317
+ }
1318
+ async prepareRuntimeTurnWithOwnership(task) {
1319
+ const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
1320
+ try {
1321
+ const client = retainedOwner?.client ?? this.createTurnClient(record);
1322
+ const owner = this.resolveRuntimeTurnOwner(task, record, client, retainedOwner);
1323
+ const turn = this.createRunningRuntimeTurn({
1324
+ record,
1325
+ conversation,
1326
+ acpxState,
1327
+ client,
1328
+ owner,
1329
+ pendingClient: retainedOwner?.client,
1330
+ promptMessageId
1331
+ });
1332
+ this.activateRuntimeTurn(task, turn);
1333
+ return turn;
1334
+ } catch (error) {
1335
+ this.restoreBufferedSessionOwner(retainedOwner);
1336
+ throw error;
1337
+ }
1338
+ }
1339
+ async prepareRuntimeTurnState(task) {
1340
+ const { record, retainedOwner } = await this.acquireRuntimeTurnState(task);
1006
1341
  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);
1342
+ const acpxState = cloneSessionAcpxState(record.acpx);
1343
+ try {
1344
+ const promptStartedAt = isoNow();
1345
+ const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
1346
+ trimConversationForRuntime(conversation);
1347
+ record.lastPromptAt = promptStartedAt;
1348
+ record.lastUsedAt = promptStartedAt;
1349
+ record.acpx = acpxState;
1350
+ applyConversation(record, conversation);
1351
+ await this.options.sessionStore.save(record);
1352
+ return {
1353
+ record,
1354
+ retainedOwner,
1355
+ conversation,
1356
+ acpxState,
1357
+ promptMessageId
1358
+ };
1359
+ } catch (error) {
1360
+ this.restoreBufferedSessionOwner(retainedOwner);
1361
+ throw error;
1362
+ }
1363
+ }
1364
+ async acquireRuntimeTurnState(task) {
1365
+ const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
1366
+ let record = await this.requireRecord(recordId);
1367
+ const retainedOwner = await this.readRetainedSessionOwner(record, { consume: false });
1368
+ if (!retainedOwner) return { record };
1369
+ const projection = retainedOwner.projection;
1370
+ if (projection) record = structuredClone(projection.record);
1371
+ retainedOwner.bufferSessionUpdates = true;
1372
+ return {
1373
+ record,
1374
+ retainedOwner
1375
+ };
1376
+ }
1377
+ restoreBufferedSessionOwner(owner) {
1378
+ if (!owner) return;
1379
+ owner.bufferSessionUpdates = false;
1380
+ this.drainPendingSessionUpdates(owner);
1381
+ }
1382
+ resolveRuntimeTurnOwner(task, record, client, retainedOwner) {
1383
+ return retainedOwner ?? this.createSessionOwner({
1384
+ client,
1385
+ sessionKey: record.name ?? task.input.handle.sessionKey,
1386
+ mode: task.input.sessionMode
1387
+ });
1388
+ }
1389
+ createRunningRuntimeTurn(input) {
1390
+ const { record, conversation, acpxState, client, owner, pendingClient, promptMessageId } = input;
1018
1391
  const turn = {
1019
1392
  record,
1020
1393
  conversation,
1021
1394
  acpxState,
1022
1395
  liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
1023
1396
  client,
1397
+ owner,
1024
1398
  pendingClient,
1399
+ connectionUpdates: pendingClient ? void 0 : [],
1025
1400
  promptMessageId,
1026
1401
  activeSessionId: record.acpSessionId
1027
1402
  };
1403
+ return turn;
1404
+ }
1405
+ activateRuntimeTurn(task, turn) {
1406
+ const { owner, record } = turn;
1407
+ this.removeRetainedSessionOwner(owner);
1408
+ owner.recordId = record.acpxRecordId;
1028
1409
  task.state.activeController = this.buildRuntimeTurnController(task, turn);
1029
1410
  this.activeControllers.set(record.acpxRecordId, task.state.activeController);
1030
- this.installRuntimeTurnEventHandlers(task, turn);
1031
- return turn;
1411
+ owner.projection = void 0;
1412
+ owner.activeTurn = {
1413
+ task,
1414
+ turn
1415
+ };
1416
+ owner.bufferSessionUpdates = false;
1417
+ this.drainPendingSessionUpdates(owner);
1032
1418
  }
1033
1419
  createTurnClient(record) {
1034
1420
  return this.createClient({
@@ -1038,7 +1424,9 @@ var AcpRuntimeManager = class {
1038
1424
  mcpServers: [...this.options.mcpServers ?? []],
1039
1425
  permissionMode: this.options.permissionMode,
1040
1426
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1427
+ permissionPolicy: this.options.permissionPolicy,
1041
1428
  onPermissionRequest: this.options.onPermissionRequest,
1429
+ elicitationModes: this.options.elicitationModes,
1042
1430
  verbose: this.options.verbose,
1043
1431
  sessionOptions: sessionOptionsFromRecord(record)
1044
1432
  });
@@ -1111,29 +1499,6 @@ var AcpRuntimeManager = class {
1111
1499
  applyConfigOptionResponseToTurn(turn, response);
1112
1500
  applyDesiredConfigOptionToTurn(turn, configId, value);
1113
1501
  }
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
1502
  emitRuntimeTurnEvent(task, payload) {
1138
1503
  const parsed = parsePromptEventLine(JSON.stringify(payload));
1139
1504
  if (!parsed) return;
@@ -1145,9 +1510,17 @@ var AcpRuntimeManager = class {
1145
1510
  resumed: false,
1146
1511
  loadError: void 0
1147
1512
  } : await this.connectRuntimeTurnClient(task, turn);
1148
- turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
1513
+ this.drainRuntimeConnectionUpdates(turn);
1149
1514
  return loaded;
1150
1515
  }
1516
+ drainRuntimeConnectionUpdates(turn) {
1517
+ if (!turn.connectionUpdates) return;
1518
+ turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
1519
+ for (const notification of turn.connectionUpdates) turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
1520
+ turn.connectionUpdates = void 0;
1521
+ trimConversationForRuntime(turn.conversation);
1522
+ turn.liveCheckpoint.request();
1523
+ }
1151
1524
  async connectRuntimeTurnClient(task, turn) {
1152
1525
  return await connectAndLoadSession({
1153
1526
  client: turn.client,
@@ -1188,10 +1561,7 @@ var AcpRuntimeManager = class {
1188
1561
  cancelRuntimeTurnBeforePrompt(task) {
1189
1562
  if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
1190
1563
  task.state.pendingCancel = false;
1191
- task.settleResult({
1192
- status: "cancelled",
1193
- stopReason: "cancelled"
1194
- });
1564
+ task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
1195
1565
  return true;
1196
1566
  }
1197
1567
  async applyPendingRuntimeTurnCancel(task, turn) {
@@ -1211,9 +1581,10 @@ var AcpRuntimeManager = class {
1211
1581
  await this.options.sessionStore.save(turn.record);
1212
1582
  }
1213
1583
  failRuntimeTurn(task, error) {
1584
+ task.promptStarted.reject(error);
1214
1585
  task.sessionReady.reject(error);
1215
1586
  const normalized = normalizeOutputError(error, { origin: "runtime" });
1216
- task.settleResult({
1587
+ return {
1217
1588
  status: "failed",
1218
1589
  error: {
1219
1590
  message: normalized.message,
@@ -1221,13 +1592,36 @@ var AcpRuntimeManager = class {
1221
1592
  ...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
1222
1593
  ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
1223
1594
  }
1224
- });
1595
+ };
1225
1596
  }
1226
1597
  async finalizeRuntimeTurn(task, turn) {
1227
1598
  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(() => {});
1599
+ const abortHandlerAttempt = await settleAttempt(() => task.input.signal?.removeEventListener("abort", task.abortHandler));
1600
+ const recordAttempt = await settleAttempt(async () => turn ? await this.finalizeRuntimeTurnRecord(turn) : false);
1601
+ let failure = firstFailedAttempt([abortHandlerAttempt, recordAttempt]);
1602
+ let pooled = recordAttempt.ok ? recordAttempt.value : false;
1603
+ if (failure) {
1604
+ this.discardRetainedRuntimeTurnOwner(turn);
1605
+ pooled = false;
1606
+ }
1607
+ const closeAttempt = await settleAttempt(async () => this.closeRuntimeTurnClient(turn, pooled));
1608
+ this.cleanupRuntimeTurn(task, turn);
1609
+ failure ??= firstFailedAttempt([closeAttempt]);
1610
+ if (failure) throw failure.error;
1611
+ }
1612
+ discardRetainedRuntimeTurnOwner(turn) {
1613
+ if (turn) {
1614
+ this.removeRetainedSessionOwner(turn.owner);
1615
+ turn.owner.activeTurn = void 0;
1616
+ }
1617
+ }
1618
+ async closeRuntimeTurnClient(turn, pooled) {
1619
+ if (!turn || pooled) return;
1620
+ turn.owner.activeTurn = void 0;
1621
+ const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => turn.client.close())]);
1622
+ if (failure) throw failure.error;
1623
+ }
1624
+ cleanupRuntimeTurn(task, turn) {
1231
1625
  if (turn) {
1232
1626
  this.activeControllers.delete(turn.record.acpxRecordId);
1233
1627
  this.closingActiveRecords.delete(turn.record.acpxRecordId);
@@ -1235,17 +1629,20 @@ var AcpRuntimeManager = class {
1235
1629
  task.queue.close();
1236
1630
  }
1237
1631
  async finalizeRuntimeTurnRecord(turn) {
1632
+ this.drainRuntimeConnectionUpdates(turn);
1238
1633
  applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1239
1634
  turn.record.acpx = turn.acpxState;
1240
1635
  applyConversation(turn.record, turn.conversation);
1241
1636
  turn.record.lastUsedAt = isoNow();
1242
- await turn.liveCheckpoint.flush().catch(() => {});
1637
+ await turn.liveCheckpoint.flush();
1243
1638
  const closed = await this.refreshClosedState(turn.record);
1244
- await this.options.sessionStore.save(turn.record).catch(() => {});
1639
+ await this.options.sessionStore.save(turn.record);
1245
1640
  if (closed) return false;
1246
- return await this.retainPersistentClientAfterTurn({
1641
+ return await this.retainPersistentSessionOwnerAfterTurn({
1247
1642
  record: turn.record,
1248
- client: turn.client
1643
+ owner: turn.owner,
1644
+ conversation: turn.conversation,
1645
+ acpxState: turn.acpxState
1249
1646
  });
1250
1647
  }
1251
1648
  async *runTurn(input) {
@@ -1254,7 +1651,10 @@ var AcpRuntimeManager = class {
1254
1651
  yield legacyTerminalEventFromTurnResult(await turn.result);
1255
1652
  }
1256
1653
  async getStatus(handle) {
1257
- const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1654
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1655
+ const owner = this.retainedSessionOwners.get(recordId);
1656
+ if (owner) await this.flushSessionOwner(owner);
1657
+ const record = await this.requireRecord(recordId);
1258
1658
  return {
1259
1659
  summary: statusSummary(record),
1260
1660
  acpxRecordId: record.acpxRecordId,
@@ -1272,6 +1672,10 @@ var AcpRuntimeManager = class {
1272
1672
  };
1273
1673
  }
1274
1674
  async setMode(handle, mode, sessionMode = "persistent") {
1675
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1676
+ await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode));
1677
+ }
1678
+ async setModeWithOwnership(handle, mode, sessionMode) {
1275
1679
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1276
1680
  const controller = this.activeControllers.get(record.acpxRecordId);
1277
1681
  let targetRecord = record;
@@ -1283,6 +1687,10 @@ var AcpRuntimeManager = class {
1283
1687
  await this.options.sessionStore.save(targetRecord);
1284
1688
  }
1285
1689
  async setConfigOption(handle, key, value, sessionMode = "persistent") {
1690
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1691
+ await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode));
1692
+ }
1693
+ async setConfigOptionWithOwnership(handle, key, value, sessionMode) {
1286
1694
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1287
1695
  const controller = this.activeControllers.get(record.acpxRecordId);
1288
1696
  if (controller) {
@@ -1303,43 +1711,80 @@ var AcpRuntimeManager = class {
1303
1711
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
1304
1712
  }
1305
1713
  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);
1714
+ const recordId = handle.acpxRecordId ?? handle.sessionKey;
1715
+ const record = await this.resolveRuntimeRecordForClose(recordId);
1716
+ this.markActiveRuntimeRecordClosing(record);
1308
1717
  await this.cancel(handle);
1309
- if (options.discardPersistentState) {
1718
+ await this.closeRuntimeRecordOwnership(record, options.discardPersistentState === true);
1719
+ record.closed = true;
1720
+ record.closedAt = isoNow();
1721
+ await this.options.sessionStore.save(record);
1722
+ }
1723
+ async resolveRuntimeRecordForClose(recordId) {
1724
+ const retainedOwner = this.retainedSessionOwners.get(recordId);
1725
+ if (retainedOwner) await this.flushSessionOwner(retainedOwner);
1726
+ return retainedOwner?.projection?.record ?? await this.requireRecord(recordId);
1727
+ }
1728
+ markActiveRuntimeRecordClosing(record) {
1729
+ if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
1730
+ }
1731
+ async closeRuntimeRecordOwnership(record, discardPersistentState) {
1732
+ if (discardPersistentState) {
1310
1733
  await this.closeBackendSession(record);
1311
1734
  record.acpx = {
1312
1735
  ...record.acpx,
1313
1736
  reset_on_next_ensure: true
1314
1737
  };
1315
- } else await this.closePendingPersistentClient(record.acpxRecordId);
1316
- record.closed = true;
1317
- record.closedAt = isoNow();
1318
- await this.options.sessionStore.save(record);
1738
+ } else await this.closeRetainedSessionOwner(record.acpxRecordId);
1319
1739
  }
1320
1740
  async closeBackendSession(record) {
1321
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
1322
- const client = pendingClient ?? this.createClient({
1741
+ const connection = await this.acquireBackendCloseConnection(record);
1742
+ try {
1743
+ await this.requestBackendSessionClose(record, connection);
1744
+ } catch (error) {
1745
+ this.handleBackendSessionCloseError(record, error);
1746
+ } finally {
1747
+ await this.finalizeBackendCloseConnection(connection);
1748
+ }
1749
+ }
1750
+ async finalizeBackendCloseConnection(connection) {
1751
+ const failure = firstFailedAttempt([
1752
+ await settleAttempt(async () => {
1753
+ if (connection.owner) await this.flushSessionOwner(connection.owner);
1754
+ }),
1755
+ await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
1756
+ await settleAttempt(async () => connection.client.close())
1757
+ ]);
1758
+ if (failure) throw failure.error;
1759
+ }
1760
+ async acquireBackendCloseConnection(record) {
1761
+ const owner = await this.readRetainedSessionOwner(record, { consume: true });
1762
+ if (owner) return {
1763
+ client: owner.client,
1764
+ owner
1765
+ };
1766
+ return { client: this.createClient({
1323
1767
  agentCommand: record.agentCommand,
1324
1768
  agentArgv: record.agentArgv,
1325
1769
  cwd: record.cwd,
1326
1770
  mcpServers: [...this.options.mcpServers ?? []],
1327
1771
  permissionMode: this.options.permissionMode,
1328
1772
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1773
+ permissionPolicy: this.options.permissionPolicy,
1329
1774
  onPermissionRequest: this.options.onPermissionRequest,
1775
+ elicitationModes: this.options.elicitationModes,
1330
1776
  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
- }
1777
+ }) };
1778
+ }
1779
+ async requestBackendSessionClose(record, connection) {
1780
+ if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
1781
+ if (!connection.client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
1782
+ await withTimeout(connection.client.closeSession(record.acpSessionId), this.options.timeoutMs);
1783
+ }
1784
+ handleBackendSessionCloseError(record, error) {
1785
+ if (isAcpResourceNotFoundError(error)) return;
1786
+ if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
1787
+ throw error;
1343
1788
  }
1344
1789
  async requireRecord(sessionId) {
1345
1790
  const record = await this.options.sessionStore.load(sessionId);
@@ -1506,6 +1951,7 @@ function createProbeClient(options, agentCommand, deps) {
1506
1951
  mcpServers: [...options.mcpServers ?? []],
1507
1952
  permissionMode: options.permissionMode,
1508
1953
  nonInteractivePermissions: options.nonInteractivePermissions,
1954
+ permissionPolicy: options.permissionPolicy,
1509
1955
  verbose: options.verbose
1510
1956
  };
1511
1957
  return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
@@ -1614,10 +2060,14 @@ var AcpxRuntime = class {
1614
2060
  sessionMode: state.mode,
1615
2061
  requestId: input.requestId,
1616
2062
  timeoutMs: input.timeoutMs,
1617
- signal: input.signal
2063
+ signal: input.signal,
2064
+ onElicitation: input.onElicitation
1618
2065
  }));
1619
2066
  return {
1620
2067
  requestId: input.requestId,
2068
+ get promptStarted() {
2069
+ return turnPromise.then((turn) => turn.promptStarted);
2070
+ },
1621
2071
  events: { async *[Symbol.asyncIterator]() {
1622
2072
  yield* (await turnPromise).events;
1623
2073
  } },
@@ -1642,7 +2092,8 @@ var AcpxRuntime = class {
1642
2092
  sessionMode: state.mode,
1643
2093
  requestId: input.requestId,
1644
2094
  timeoutMs: input.timeoutMs,
1645
- signal: input.signal
2095
+ signal: input.signal,
2096
+ onElicitation: input.onElicitation
1646
2097
  });
1647
2098
  }
1648
2099
  async getCapabilities(input) {