acpx 0.12.1 → 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.
Files changed (35) hide show
  1. package/README.md +64 -375
  2. package/dist/{cli-pKoIEH5O.js → cli-_7Sei6dD.js} +5 -4
  3. package/dist/cli-_7Sei6dD.js.map +1 -0
  4. package/dist/cli.d.ts +2 -2
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +72 -19
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-CFo_9MIo.d.ts → client-CxNllqui.d.ts} +17 -3
  9. package/dist/client-CxNllqui.d.ts.map +1 -0
  10. package/dist/{flags-CjeFX5ki.js → flags-Ds6FzmHG.js} +36 -9
  11. package/dist/flags-Ds6FzmHG.js.map +1 -0
  12. package/dist/{flows-Cym6H3vg.js → flows-fzH0GKYF.js} +14 -4
  13. package/dist/flows-fzH0GKYF.js.map +1 -0
  14. package/dist/flows.d.ts +6 -2
  15. package/dist/flows.d.ts.map +1 -1
  16. package/dist/flows.js +1 -1
  17. package/dist/{live-checkpoint-DQp4JSHD.js → live-checkpoint-BSIrfgVo.js} +560 -196
  18. package/dist/live-checkpoint-BSIrfgVo.js.map +1 -0
  19. package/dist/{output-fNqTAsFQ.js → output-BZQE0gI1.js} +74 -11
  20. package/dist/output-BZQE0gI1.js.map +1 -0
  21. package/dist/runtime.d.ts +106 -19
  22. package/dist/runtime.d.ts.map +1 -1
  23. package/dist/runtime.js +631 -158
  24. package/dist/runtime.js.map +1 -1
  25. package/dist/{session-options-C4uYkKDy.d.ts → session-options-DwRDODlr.d.ts} +28 -3
  26. package/dist/session-options-DwRDODlr.d.ts.map +1 -0
  27. package/package.json +19 -19
  28. package/skills/acpx/SKILL.md +5 -2
  29. package/dist/cli-pKoIEH5O.js.map +0 -1
  30. package/dist/client-CFo_9MIo.d.ts.map +0 -1
  31. package/dist/flags-CjeFX5ki.js.map +0 -1
  32. package/dist/flows-Cym6H3vg.js.map +0 -1
  33. package/dist/live-checkpoint-DQp4JSHD.js.map +0 -1
  34. package/dist/output-fNqTAsFQ.js.map +0 -1
  35. package/dist/session-options-C4uYkKDy.d.ts.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,7 +1,8 @@
1
- import { A as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, At as listBuiltInAgents, D as applyLifecycleSnapshotToRecord, E as applyConversation, F as modelStateFromConfigOptions, It as normalizeOutputError, Lt as extractAcpError, M as RequestedModelUnsupportedError, Mt as resolveAgentCommand, O as reconcileAgentSessionId, Ot as withTimeout, P as isRequestedModelUnsupportedError, Rt as isAcpResourceNotFoundError, S as advertisedModelState, T as sessionOptionsFromRecord, Z as assertPersistedKeyPolicy, _ as createSessionConversation, a as applyRequestedModelIfAdvertised, b as recordSessionUpdate, c as setCurrentModelId, d as setDesiredModelId, f as syncAdvertisedModelState, g as cloneSessionConversation, h as cloneSessionAcpxState, i as connectAndLoadSession, j as REQUESTED_MODEL_UNSUPPORTED_REASONS, k as AcpClient, kt as DEFAULT_AGENT_NAME, l as setDesiredConfigOption, m as applyConfigOptionsToState, n as runPromptTurn, o as currentModelIdFromSetModelResponse, ot as parseSessionRecord, p as applyConfigOptionsToRecord, r as withConnectedSession, s as clearDesiredConfigOption, st as serializeSessionRecordForDisk, t as LiveSessionCheckpoint, u as setDesiredModeId, ut as defaultSessionEventLog, v as recordClientOperation, w as persistSessionOptions, wt as textPrompt, x as trimConversationForRuntime, y as recordPromptSubmission } from "./live-checkpoint-DQp4JSHD.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 = [];
@@ -561,6 +625,7 @@ function createInitialRecord(params) {
561
625
  acpSessionId: params.sessionId,
562
626
  agentSessionId: params.agentSessionId,
563
627
  agentCommand: params.agentCommand,
628
+ agentArgv: params.agentArgv,
564
629
  cwd: params.cwd,
565
630
  name: params.sessionName,
566
631
  createdAt: now,
@@ -737,7 +802,10 @@ var AcpRuntimeManager = class {
737
802
  options;
738
803
  deps;
739
804
  activeControllers = /* @__PURE__ */ new Map();
740
- pendingPersistentClients = /* @__PURE__ */ new Map();
805
+ retainedSessionOwners = /* @__PURE__ */ new Map();
806
+ pendingOneShotRecordIds = /* @__PURE__ */ new Map();
807
+ ensureSessionLocks = /* @__PURE__ */ new Map();
808
+ runtimeOperationLocks = /* @__PURE__ */ new Map();
741
809
  closingActiveRecords = /* @__PURE__ */ new Set();
742
810
  constructor(options, deps = {}) {
743
811
  this.options = options;
@@ -746,22 +814,123 @@ var AcpRuntimeManager = class {
746
814
  createClient(options) {
747
815
  return this.deps.clientFactory?.(options) ?? new AcpClient(options);
748
816
  }
749
- async readPendingPersistentClient(record, options) {
750
- const pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
751
- if (!pendingClient) return;
752
- if (!pendingClient.hasReusableSession(record.acpSessionId)) {
753
- this.pendingPersistentClients.delete(record.acpxRecordId);
754
- 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);
755
859
  return;
756
860
  }
757
- if (options.consume) this.pendingPersistentClients.delete(record.acpxRecordId);
758
- 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
+ });
759
876
  }
760
- async closePendingPersistentClient(recordId) {
761
- const pendingClient = this.pendingPersistentClients.get(recordId);
762
- if (!pendingClient) return;
763
- this.pendingPersistentClients.delete(recordId);
764
- 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 {}
765
934
  }
766
935
  async refreshClosedState(record) {
767
936
  if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
@@ -774,32 +943,43 @@ var AcpRuntimeManager = class {
774
943
  };
775
944
  return true;
776
945
  }
777
- async retainPersistentClientAfterTurn(input) {
778
- const { record, client } = input;
779
- if (!!record.acpxRecordId.includes(":oneshot:") || record.closed || !client.hasReusableSession(record.acpSessionId)) return false;
780
- const previousClient = this.pendingPersistentClients.get(record.acpxRecordId);
781
- this.pendingPersistentClients.set(record.acpxRecordId, client);
782
- 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
+ }
783
959
  return true;
784
960
  }
961
+ canRetainPersistentSessionOwner(owner, record) {
962
+ return owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
963
+ }
785
964
  async withRuntimeControlSession(record, sessionMode, run) {
786
- const pendingClient = await this.readPendingPersistentClient(record, { consume: false });
787
- if (pendingClient) {
788
- const value = await run({
789
- client: pendingClient,
790
- sessionId: record.acpSessionId,
791
- record
792
- });
793
- record.lastUsedAt = isoNow();
794
- record.closed = false;
795
- record.closedAt = void 0;
796
- record.protocolVersion = pendingClient.initializeResult?.protocolVersion;
797
- record.agentCapabilities = pendingClient.initializeResult?.agentCapabilities;
798
- applyLifecycleSnapshotToRecord(record, pendingClient.getAgentLifecycleSnapshot());
799
- return {
800
- value,
801
- record
802
- };
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
+ }
803
983
  }
804
984
  const result = await withConnectedSession({
805
985
  sessionRecordId: record.acpxRecordId,
@@ -809,7 +989,9 @@ var AcpRuntimeManager = class {
809
989
  mcpServers: [...this.options.mcpServers ?? []],
810
990
  permissionMode: this.options.permissionMode,
811
991
  nonInteractivePermissions: this.options.nonInteractivePermissions,
992
+ permissionPolicy: this.options.permissionPolicy,
812
993
  onPermissionRequest: this.options.onPermissionRequest,
994
+ elicitationModes: this.options.elicitationModes,
813
995
  verbose: this.options.verbose,
814
996
  timeoutMs: this.options.timeoutMs,
815
997
  resumePolicy: resumePolicyForSessionMode(sessionMode),
@@ -820,32 +1002,117 @@ var AcpRuntimeManager = class {
820
1002
  record: result.record
821
1003
  };
822
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
+ }
823
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) {
824
1048
  const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
825
- const agentCommand = this.options.agentRegistry.resolve(input.agent);
826
- const existing = await this.options.sessionStore.load(input.sessionKey);
827
- if (input.mode === "persistent" && existing && shouldReuseExistingRecord(existing, {
1049
+ const { agentCommand, agentArgv } = normalizeAgentCommandInput(this.options.agentRegistry.resolve(input.agent));
1050
+ const agent = {
828
1051
  cwd,
829
1052
  agentCommand,
830
- resumeSessionId: input.resumeSessionId
831
- })) {
832
- existing.closed = false;
833
- existing.closedAt = void 0;
834
- this.closingActiveRecords.delete(existing.acpxRecordId);
835
- await this.options.sessionStore.save(existing);
836
- 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;
837
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;
838
1097
  const client = this.createClient({
839
1098
  agentCommand,
1099
+ agentArgv,
840
1100
  cwd,
841
1101
  mcpServers: [...this.options.mcpServers ?? []],
842
1102
  permissionMode: this.options.permissionMode,
843
1103
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1104
+ permissionPolicy: this.options.permissionPolicy,
844
1105
  onPermissionRequest: this.options.onPermissionRequest,
1106
+ elicitationModes: this.options.elicitationModes,
845
1107
  verbose: this.options.verbose,
846
1108
  sessionOptions: input.sessionOptions
847
1109
  });
848
- let keepClientOpen = false;
1110
+ const owner = this.createSessionOwner({
1111
+ client,
1112
+ sessionKey: input.sessionKey,
1113
+ mode: input.mode
1114
+ });
1115
+ let retained = false;
849
1116
  try {
850
1117
  await client.start();
851
1118
  const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
@@ -853,22 +1120,29 @@ var AcpRuntimeManager = class {
853
1120
  input,
854
1121
  client,
855
1122
  agentCommand,
1123
+ agentArgv,
856
1124
  cwd,
857
1125
  session
858
1126
  });
859
- keepClientOpen = await this.keepPersistentClient(input.mode, record.acpxRecordId, client);
1127
+ this.attachIdleProjection(owner, record);
1128
+ await this.retainInitializedSessionOwner(owner, record);
1129
+ retained = true;
860
1130
  return record;
861
1131
  } finally {
862
- if (!keepClientOpen) await client.close();
1132
+ if (!retained) {
1133
+ client.clearEventHandlers();
1134
+ await client.close();
1135
+ }
863
1136
  }
864
1137
  }
865
1138
  async createAndSaveRuntimeRecord(params) {
866
- const { input, client, agentCommand, cwd, session } = params;
1139
+ const { input, client, agentCommand, agentArgv, cwd, session } = params;
867
1140
  const record = createInitialRecord({
868
1141
  recordId: createRecordId(input.sessionKey, input.mode),
869
1142
  sessionName: input.sessionKey,
870
1143
  sessionId: session.sessionId,
871
1144
  agentCommand,
1145
+ agentArgv,
872
1146
  cwd,
873
1147
  agentSessionId: session.agentSessionId
874
1148
  });
@@ -892,17 +1166,28 @@ var AcpRuntimeManager = class {
892
1166
  await this.options.sessionStore.save(record);
893
1167
  return record;
894
1168
  }
895
- async keepPersistentClient(mode, recordId, client) {
896
- if (mode !== "persistent") return false;
897
- const previousClient = this.pendingPersistentClients.get(recordId);
898
- this.pendingPersistentClients.set(recordId, client);
899
- await previousClient?.close().catch(() => {});
900
- 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
+ }
901
1178
  }
902
1179
  startTurn(input) {
903
- 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
+ }
904
1187
  const queue = new AsyncEventQueue();
905
1188
  const result = createDeferred();
1189
+ const promptStarted = createDeferred();
1190
+ promptStarted.promise.catch(() => {});
906
1191
  const sessionReady = createDeferred();
907
1192
  sessionReady.promise.catch(() => {});
908
1193
  let resultSettled = false;
@@ -934,13 +1219,17 @@ var AcpRuntimeManager = class {
934
1219
  };
935
1220
  if (input.signal) {
936
1221
  if (input.signal.aborted) {
1222
+ promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
937
1223
  closeStream();
938
- settleResult({
939
- status: "cancelled",
940
- stopReason: "cancelled"
1224
+ this.closeRetainedOneShotHandle(input.handle).catch(() => {}).then(() => {
1225
+ settleResult({
1226
+ status: "cancelled",
1227
+ stopReason: "cancelled"
1228
+ });
941
1229
  });
942
1230
  return {
943
1231
  requestId: input.requestId,
1232
+ promptStarted: promptStarted.promise,
944
1233
  events: queue.iterate(),
945
1234
  result: result.promise,
946
1235
  cancel: async () => {},
@@ -953,6 +1242,7 @@ var AcpRuntimeManager = class {
953
1242
  input,
954
1243
  promptInput,
955
1244
  queue,
1245
+ promptStarted,
956
1246
  sessionReady,
957
1247
  state,
958
1248
  settleResult,
@@ -960,6 +1250,7 @@ var AcpRuntimeManager = class {
960
1250
  });
961
1251
  return {
962
1252
  requestId: input.requestId,
1253
+ promptStarted: promptStarted.promise,
963
1254
  events: queue.iterate(),
964
1255
  result: result.promise,
965
1256
  cancel: async () => {
@@ -970,70 +1261,172 @@ var AcpRuntimeManager = class {
970
1261
  }
971
1262
  };
972
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
+ }
973
1268
  async runRuntimeTurnTask(task) {
974
1269
  let turn;
1270
+ let terminalResult;
975
1271
  try {
976
1272
  turn = await this.prepareRuntimeTurn(task);
977
1273
  const { sessionId, resumed, loadError } = await this.connectRuntimeTurn(task, turn);
978
1274
  await this.resolveRuntimeTurnReady(task, turn, resumed, loadError);
979
- if (this.cancelRuntimeTurnBeforePrompt(task)) return;
980
- await this.applyPendingRuntimeTurnCancel(task, turn);
981
- 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({
982
1301
  client: turn.client,
983
1302
  sessionId,
984
1303
  prompt: task.promptInput,
985
1304
  timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
986
1305
  conversation: turn.conversation,
987
- promptMessageId: turn.promptMessageId
1306
+ promptMessageId: turn.promptMessageId,
1307
+ onPromptRequestStarted: () => task.promptStarted.resolve(),
1308
+ onElicitation: task.input.onElicitation
988
1309
  });
989
- await this.saveCompletedRuntimeTurn(turn, response.stopReason);
990
- task.settleResult({
991
- status: response.stopReason === "cancelled" ? "cancelled" : "completed",
992
- ...response.stopReason ? { stopReason: response.stopReason } : {}
993
- });
994
- } catch (error) {
995
- this.failRuntimeTurn(task, error);
996
1310
  } finally {
997
- await this.finalizeRuntimeTurn(task, turn);
1311
+ turn.client.endPromptElicitation?.(sessionId);
998
1312
  }
999
1313
  }
1000
1314
  async prepareRuntimeTurn(task) {
1001
- 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);
1002
1341
  const conversation = cloneSessionConversation(record);
1003
- let acpxState = cloneSessionAcpxState(record.acpx);
1004
- const promptStartedAt = isoNow();
1005
- const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
1006
- trimConversationForRuntime(conversation);
1007
- record.lastPromptAt = promptStartedAt;
1008
- record.lastUsedAt = promptStartedAt;
1009
- record.acpx = acpxState;
1010
- applyConversation(record, conversation);
1011
- await this.options.sessionStore.save(record);
1012
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
1013
- 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;
1014
1391
  const turn = {
1015
1392
  record,
1016
1393
  conversation,
1017
1394
  acpxState,
1018
1395
  liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
1019
1396
  client,
1397
+ owner,
1020
1398
  pendingClient,
1399
+ connectionUpdates: pendingClient ? void 0 : [],
1021
1400
  promptMessageId,
1022
1401
  activeSessionId: record.acpSessionId
1023
1402
  };
1403
+ return turn;
1404
+ }
1405
+ activateRuntimeTurn(task, turn) {
1406
+ const { owner, record } = turn;
1407
+ this.removeRetainedSessionOwner(owner);
1408
+ owner.recordId = record.acpxRecordId;
1024
1409
  task.state.activeController = this.buildRuntimeTurnController(task, turn);
1025
1410
  this.activeControllers.set(record.acpxRecordId, task.state.activeController);
1026
- this.installRuntimeTurnEventHandlers(task, turn);
1027
- return turn;
1411
+ owner.projection = void 0;
1412
+ owner.activeTurn = {
1413
+ task,
1414
+ turn
1415
+ };
1416
+ owner.bufferSessionUpdates = false;
1417
+ this.drainPendingSessionUpdates(owner);
1028
1418
  }
1029
1419
  createTurnClient(record) {
1030
1420
  return this.createClient({
1031
1421
  agentCommand: record.agentCommand,
1422
+ agentArgv: record.agentArgv,
1032
1423
  cwd: record.cwd,
1033
1424
  mcpServers: [...this.options.mcpServers ?? []],
1034
1425
  permissionMode: this.options.permissionMode,
1035
1426
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1427
+ permissionPolicy: this.options.permissionPolicy,
1036
1428
  onPermissionRequest: this.options.onPermissionRequest,
1429
+ elicitationModes: this.options.elicitationModes,
1037
1430
  verbose: this.options.verbose,
1038
1431
  sessionOptions: sessionOptionsFromRecord(record)
1039
1432
  });
@@ -1106,29 +1499,6 @@ var AcpRuntimeManager = class {
1106
1499
  applyConfigOptionResponseToTurn(turn, response);
1107
1500
  applyDesiredConfigOptionToTurn(turn, configId, value);
1108
1501
  }
1109
- installRuntimeTurnEventHandlers(task, turn) {
1110
- turn.client.setEventHandlers({
1111
- onSessionUpdate: (notification) => {
1112
- turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
1113
- trimConversationForRuntime(turn.conversation);
1114
- turn.liveCheckpoint.request();
1115
- this.emitRuntimeTurnEvent(task, {
1116
- jsonrpc: "2.0",
1117
- method: "session/update",
1118
- params: notification
1119
- });
1120
- },
1121
- onClientOperation: (operation) => {
1122
- turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
1123
- trimConversationForRuntime(turn.conversation);
1124
- turn.liveCheckpoint.request();
1125
- this.emitRuntimeTurnEvent(task, {
1126
- type: "client_operation",
1127
- ...operation
1128
- });
1129
- }
1130
- });
1131
- }
1132
1502
  emitRuntimeTurnEvent(task, payload) {
1133
1503
  const parsed = parsePromptEventLine(JSON.stringify(payload));
1134
1504
  if (!parsed) return;
@@ -1140,9 +1510,17 @@ var AcpRuntimeManager = class {
1140
1510
  resumed: false,
1141
1511
  loadError: void 0
1142
1512
  } : await this.connectRuntimeTurnClient(task, turn);
1143
- turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
1513
+ this.drainRuntimeConnectionUpdates(turn);
1144
1514
  return loaded;
1145
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
+ }
1146
1524
  async connectRuntimeTurnClient(task, turn) {
1147
1525
  return await connectAndLoadSession({
1148
1526
  client: turn.client,
@@ -1183,10 +1561,7 @@ var AcpRuntimeManager = class {
1183
1561
  cancelRuntimeTurnBeforePrompt(task) {
1184
1562
  if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
1185
1563
  task.state.pendingCancel = false;
1186
- task.settleResult({
1187
- status: "cancelled",
1188
- stopReason: "cancelled"
1189
- });
1564
+ task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
1190
1565
  return true;
1191
1566
  }
1192
1567
  async applyPendingRuntimeTurnCancel(task, turn) {
@@ -1206,9 +1581,10 @@ var AcpRuntimeManager = class {
1206
1581
  await this.options.sessionStore.save(turn.record);
1207
1582
  }
1208
1583
  failRuntimeTurn(task, error) {
1584
+ task.promptStarted.reject(error);
1209
1585
  task.sessionReady.reject(error);
1210
1586
  const normalized = normalizeOutputError(error, { origin: "runtime" });
1211
- task.settleResult({
1587
+ return {
1212
1588
  status: "failed",
1213
1589
  error: {
1214
1590
  message: normalized.message,
@@ -1216,13 +1592,36 @@ var AcpRuntimeManager = class {
1216
1592
  ...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
1217
1593
  ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
1218
1594
  }
1219
- });
1595
+ };
1220
1596
  }
1221
1597
  async finalizeRuntimeTurn(task, turn) {
1222
1598
  task.state.turnActive = false;
1223
- task.input.signal?.removeEventListener("abort", task.abortHandler);
1224
- turn?.client.clearEventHandlers();
1225
- 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) {
1226
1625
  if (turn) {
1227
1626
  this.activeControllers.delete(turn.record.acpxRecordId);
1228
1627
  this.closingActiveRecords.delete(turn.record.acpxRecordId);
@@ -1230,17 +1629,20 @@ var AcpRuntimeManager = class {
1230
1629
  task.queue.close();
1231
1630
  }
1232
1631
  async finalizeRuntimeTurnRecord(turn) {
1632
+ this.drainRuntimeConnectionUpdates(turn);
1233
1633
  applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1234
1634
  turn.record.acpx = turn.acpxState;
1235
1635
  applyConversation(turn.record, turn.conversation);
1236
1636
  turn.record.lastUsedAt = isoNow();
1237
- await turn.liveCheckpoint.flush().catch(() => {});
1637
+ await turn.liveCheckpoint.flush();
1238
1638
  const closed = await this.refreshClosedState(turn.record);
1239
- await this.options.sessionStore.save(turn.record).catch(() => {});
1639
+ await this.options.sessionStore.save(turn.record);
1240
1640
  if (closed) return false;
1241
- return await this.retainPersistentClientAfterTurn({
1641
+ return await this.retainPersistentSessionOwnerAfterTurn({
1242
1642
  record: turn.record,
1243
- client: turn.client
1643
+ owner: turn.owner,
1644
+ conversation: turn.conversation,
1645
+ acpxState: turn.acpxState
1244
1646
  });
1245
1647
  }
1246
1648
  async *runTurn(input) {
@@ -1249,7 +1651,10 @@ var AcpRuntimeManager = class {
1249
1651
  yield legacyTerminalEventFromTurnResult(await turn.result);
1250
1652
  }
1251
1653
  async getStatus(handle) {
1252
- 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);
1253
1658
  return {
1254
1659
  summary: statusSummary(record),
1255
1660
  acpxRecordId: record.acpxRecordId,
@@ -1267,6 +1672,10 @@ var AcpRuntimeManager = class {
1267
1672
  };
1268
1673
  }
1269
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) {
1270
1679
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1271
1680
  const controller = this.activeControllers.get(record.acpxRecordId);
1272
1681
  let targetRecord = record;
@@ -1278,6 +1687,10 @@ var AcpRuntimeManager = class {
1278
1687
  await this.options.sessionStore.save(targetRecord);
1279
1688
  }
1280
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) {
1281
1694
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1282
1695
  const controller = this.activeControllers.get(record.acpxRecordId);
1283
1696
  if (controller) {
@@ -1298,42 +1711,80 @@ var AcpRuntimeManager = class {
1298
1711
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
1299
1712
  }
1300
1713
  async close(handle, options = {}) {
1301
- const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1302
- 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);
1303
1717
  await this.cancel(handle);
1304
- 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) {
1305
1733
  await this.closeBackendSession(record);
1306
1734
  record.acpx = {
1307
1735
  ...record.acpx,
1308
1736
  reset_on_next_ensure: true
1309
1737
  };
1310
- } else await this.closePendingPersistentClient(record.acpxRecordId);
1311
- record.closed = true;
1312
- record.closedAt = isoNow();
1313
- await this.options.sessionStore.save(record);
1738
+ } else await this.closeRetainedSessionOwner(record.acpxRecordId);
1314
1739
  }
1315
1740
  async closeBackendSession(record) {
1316
- const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
1317
- 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({
1318
1767
  agentCommand: record.agentCommand,
1768
+ agentArgv: record.agentArgv,
1319
1769
  cwd: record.cwd,
1320
1770
  mcpServers: [...this.options.mcpServers ?? []],
1321
1771
  permissionMode: this.options.permissionMode,
1322
1772
  nonInteractivePermissions: this.options.nonInteractivePermissions,
1773
+ permissionPolicy: this.options.permissionPolicy,
1323
1774
  onPermissionRequest: this.options.onPermissionRequest,
1775
+ elicitationModes: this.options.elicitationModes,
1324
1776
  verbose: this.options.verbose
1325
- });
1326
- try {
1327
- if (!pendingClient) await withTimeout(client.start(), this.options.timeoutMs);
1328
- if (!client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
1329
- await withTimeout(client.closeSession(record.acpSessionId), this.options.timeoutMs);
1330
- } catch (error) {
1331
- if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
1332
- if (isAcpResourceNotFoundError(error)) return;
1333
- throw error;
1334
- } finally {
1335
- await client.close().catch(() => {});
1336
- }
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;
1337
1788
  }
1338
1789
  async requireRecord(sessionId) {
1339
1790
  const record = await this.options.sessionStore.load(sessionId);
@@ -1382,7 +1833,7 @@ var FileSessionStore = class {
1382
1833
  const persisted = serializeSessionRecordForDisk(record);
1383
1834
  assertPersistedKeyPolicy(persisted);
1384
1835
  const file = this.filePath(record.acpxRecordId);
1385
- const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
1836
+ const tempFile = createAtomicWriteTempPath(file);
1386
1837
  const payload = JSON.stringify(persisted, null, 2);
1387
1838
  await fs.writeFile(tempFile, `${payload}\n`, "utf8");
1388
1839
  await fs.rename(tempFile, file);
@@ -1464,7 +1915,7 @@ function normalizeRuntimeDetails(details) {
1464
1915
  }
1465
1916
  async function probeRuntime(options, deps = {}) {
1466
1917
  const agentName = options.probeAgent?.trim() || "codex";
1467
- const agentCommand = options.agentRegistry.resolve(agentName);
1918
+ const agentCommand = normalizeAgentCommandInput(options.agentRegistry.resolve(agentName));
1468
1919
  const client = createProbeClient(options, agentCommand, deps);
1469
1920
  try {
1470
1921
  await client.start();
@@ -1473,7 +1924,7 @@ async function probeRuntime(options, deps = {}) {
1473
1924
  message: "embedded ACP runtime ready",
1474
1925
  details: [
1475
1926
  `agent=${agentName}`,
1476
- `command=${agentCommand}`,
1927
+ `command=${agentCommand.agentCommand}`,
1477
1928
  `cwd=${options.cwd}`,
1478
1929
  ...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
1479
1930
  ]
@@ -1484,7 +1935,7 @@ async function probeRuntime(options, deps = {}) {
1484
1935
  message: "embedded ACP runtime probe failed",
1485
1936
  details: [
1486
1937
  `agent=${agentName}`,
1487
- `command=${agentCommand}`,
1938
+ `command=${agentCommand.agentCommand}`,
1488
1939
  `cwd=${options.cwd}`,
1489
1940
  formatRuntimeDetail(error)
1490
1941
  ]
@@ -1495,11 +1946,12 @@ async function probeRuntime(options, deps = {}) {
1495
1946
  }
1496
1947
  function createProbeClient(options, agentCommand, deps) {
1497
1948
  const clientOptions = {
1498
- agentCommand,
1949
+ ...agentCommand,
1499
1950
  cwd: options.cwd,
1500
1951
  mcpServers: [...options.mcpServers ?? []],
1501
1952
  permissionMode: options.permissionMode,
1502
1953
  nonInteractivePermissions: options.nonInteractivePermissions,
1954
+ permissionPolicy: options.permissionPolicy,
1503
1955
  verbose: options.verbose
1504
1956
  };
1505
1957
  return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
@@ -1513,15 +1965,31 @@ const ACPX_CAPABILITIES = { controls: [
1513
1965
  "session/status"
1514
1966
  ] };
1515
1967
  function createAgentRegistry(params) {
1968
+ const overrides = normalizeRegistryOverrides(params?.overrides);
1516
1969
  return {
1517
1970
  resolve(agentName) {
1518
- return resolveAgentCommand(agentName, params?.overrides);
1971
+ const normalizedAgentName = normalizeAgentName(agentName);
1972
+ return overrides[normalizedAgentName] ?? overrides[resolveCanonicalAgentName(agentName)] ?? resolveAgentArgv(agentName) ?? resolveAgentCommand(agentName);
1519
1973
  },
1520
1974
  list() {
1521
- return listBuiltInAgents(params?.overrides);
1975
+ return listBuiltInAgents(overrides);
1522
1976
  }
1523
1977
  };
1524
1978
  }
1979
+ function normalizeRegistryOverrides(values) {
1980
+ const normalized = {};
1981
+ for (const [name, value] of Object.entries(values ?? {})) {
1982
+ const normalizedName = normalizeAgentName(name);
1983
+ if (!normalizedName) continue;
1984
+ const normalizedValue = normalizeRegistryOverride(value);
1985
+ if (normalizedValue) normalized[normalizedName] = normalizedValue;
1986
+ }
1987
+ return normalized;
1988
+ }
1989
+ function normalizeRegistryOverride(value) {
1990
+ if (typeof value === "string") return value.trim() || void 0;
1991
+ return value.length > 0 && value[0]?.length ? [...value] : void 0;
1992
+ }
1525
1993
  var AcpxRuntime = class {
1526
1994
  options;
1527
1995
  testOptions;
@@ -1592,10 +2060,14 @@ var AcpxRuntime = class {
1592
2060
  sessionMode: state.mode,
1593
2061
  requestId: input.requestId,
1594
2062
  timeoutMs: input.timeoutMs,
1595
- signal: input.signal
2063
+ signal: input.signal,
2064
+ onElicitation: input.onElicitation
1596
2065
  }));
1597
2066
  return {
1598
2067
  requestId: input.requestId,
2068
+ get promptStarted() {
2069
+ return turnPromise.then((turn) => turn.promptStarted);
2070
+ },
1599
2071
  events: { async *[Symbol.asyncIterator]() {
1600
2072
  yield* (await turnPromise).events;
1601
2073
  } },
@@ -1620,7 +2092,8 @@ var AcpxRuntime = class {
1620
2092
  sessionMode: state.mode,
1621
2093
  requestId: input.requestId,
1622
2094
  timeoutMs: input.timeoutMs,
1623
- signal: input.signal
2095
+ signal: input.signal,
2096
+ onElicitation: input.onElicitation
1624
2097
  });
1625
2098
  }
1626
2099
  async getCapabilities(input) {