acpx 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{agent-registry-DU6mhZBL.js → agent-registry-DdKB-zia.js} +20 -17
  2. package/dist/agent-registry-DdKB-zia.js.map +1 -0
  3. package/dist/agent-registry.d.ts.map +1 -1
  4. package/dist/agent-registry.js +1 -1
  5. package/dist/{cli-CiyfEC7L.js → cli-8mVHeUXh.js} +5 -5
  6. package/dist/{cli-CiyfEC7L.js.map → cli-8mVHeUXh.js.map} +1 -1
  7. package/dist/cli.js +122 -94
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{client-C6NUFqr-.d.ts → client-DeNKZZcu.d.ts} +19 -13
  10. package/dist/client-DeNKZZcu.d.ts.map +1 -0
  11. package/dist/{flows-CTOdIk-m.js → flows-WOQNzCMC.js} +5 -5
  12. package/dist/{flows-CTOdIk-m.js.map → flows-WOQNzCMC.js.map} +1 -1
  13. package/dist/flows.d.ts +1 -1
  14. package/dist/flows.js +1 -1
  15. package/dist/{invocation-options-DbCcOAzM.js → invocation-options-B6d7TI-y.js} +7 -6
  16. package/dist/{invocation-options-DbCcOAzM.js.map → invocation-options-B6d7TI-y.js.map} +1 -1
  17. package/dist/{ipc-D3akHMEq.js → ipc-BJrvfNXr.js} +307 -90
  18. package/dist/ipc-BJrvfNXr.js.map +1 -0
  19. package/dist/{output-BPPjvJTI.js → output-CL18YmsG.js} +9 -6
  20. package/dist/output-CL18YmsG.js.map +1 -0
  21. package/dist/{queue-owner-runtime--IbbndGz.js → queue-owner-runtime-Cr0dWJuL.js} +1008 -463
  22. package/dist/queue-owner-runtime-Cr0dWJuL.js.map +1 -0
  23. package/dist/runtime.d.ts +129 -39
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +378 -176
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/{session-watch--UX5dh00.js → session-watch-B8Q_TsGf.js} +4 -4
  28. package/dist/{session-watch--UX5dh00.js.map → session-watch-B8Q_TsGf.js.map} +1 -1
  29. package/dist/{watch-BbBxO-kb.js → watch-Dp73ph8h.js} +2 -2
  30. package/dist/{watch-BbBxO-kb.js.map → watch-Dp73ph8h.js.map} +1 -1
  31. package/package.json +3 -3
  32. package/skills/acpx/SKILL.md +27 -7
  33. package/dist/agent-registry-DU6mhZBL.js.map +0 -1
  34. package/dist/client-C6NUFqr-.d.ts.map +0 -1
  35. package/dist/ipc-D3akHMEq.js.map +0 -1
  36. package/dist/output-BPPjvJTI.js.map +0 -1
  37. package/dist/queue-owner-runtime--IbbndGz.js.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,7 +1,7 @@
1
- import { C as textPrompt, D as findSession, F as resolveSessionRecord, L as assertPersistedKeyPolicy, P as readSessionRecord, R as parseSessionRecord, gt as extractAcpError, mt as writePrivateJsonFile, t as tryCancelOnRunningOwner, vt as isAcpResourceNotFoundError, z as serializeSessionRecordForDisk } from "./ipc-D3akHMEq.js";
2
- import { A as setDesiredModeId, B as recordClientOperation, C as withConnectedSession, D as applyInitialModelSelection, E as applyConfigOptionsToRecord, F as createInitialSessionRecord, G as AcpClient, H as recordSessionUpdate, I as reconcileAgentSessionId, J as RequestedModelUnsupportedError, K as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, L as cloneSessionAcpxState, M as sessionOptionsFromRecord, N as applyConversation, O as applyModelSelection, P as applyLifecycleSnapshotToRecord, R as cloneSessionConversation, T as applyConfigOptionSelection, U as trimConversationForRuntime, V as recordPromptSubmission, W as advertisedModelState, Y as isRequestedModelUnsupportedError, _ as cancelSessionPrompt, c as LiveSessionCheckpoint, ct as normalizeOutputError, h as ensureSession, it as assertControlAuthority, j as persistSessionOptions, k as applyRequestedModelIfAdvertised, l as runPromptTurn, ot as withTimeout, q as REQUESTED_MODEL_UNSUPPORTED_REASONS, r as sendSession, s as DISCARD_OUTPUT_FORMATTER, v as closeSession, w as connectAndLoadSession } from "./queue-owner-runtime--IbbndGz.js";
3
- import { D as normalizeAgentCommandInput, a as createAgentRegistry, i as DEFAULT_AGENT_NAME } from "./agent-registry-DU6mhZBL.js";
4
- import { t as watchSession } from "./watch-BbBxO-kb.js";
1
+ import { B as serializeSessionRecordForDisk, F as readSessionRecord, I as resolveSessionRecord, O as findSession, R as assertPersistedKeyPolicy, bt as isAcpResourceNotFoundError, ht as writePrivateJsonFile, t as tryCancelOnRunningOwner, vt as extractAcpError, w as textPrompt, z as parseSessionRecord } from "./ipc-BJrvfNXr.js";
2
+ import { A as applyInitialModelSelection, B as cloneSessionAcpxState, C as setSessionModeOnOwner, D as connectAndLoadSession, E as withConnectedSession, F as sessionOptionsFromRecord, G as recordSessionUpdate, I as applyConversation, J as AcpClient, K as trimConversationForRuntime, L as applyLifecycleSnapshotToRecord, M as applyRequestedModelIfAdvertised, N as setDesiredModeId, O as applyConfigOptionSelection, P as persistSessionOptions, Q as isRequestedModelUnsupportedError, R as createInitialSessionRecord, T as setSessionModelOnOwner, U as recordClientOperation, V as cloneSessionConversation, W as recordPromptSubmission, X as REQUESTED_MODEL_UNSUPPORTED_REASONS, Y as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, Z as RequestedModelUnsupportedError, _ as cancelSessionPrompt, c as LiveSessionCheckpoint, dt as normalizeOutputError, h as ensureSession, j as applyModelSelection, k as applyConfigOptionsToRecord, l as runPromptTurn, lt as withTimeout, ot as TimeoutError, q as advertisedModelState, r as sendSession, s as DISCARD_OUTPUT_FORMATTER, st as assertControlAuthority, v as closeSession, x as setSessionConfigOptionOnOwner, z as reconcileAgentSessionId } from "./queue-owner-runtime-Cr0dWJuL.js";
3
+ import { D as normalizeAgentCommandInput, a as createAgentRegistry, i as DEFAULT_AGENT_NAME } from "./agent-registry-DdKB-zia.js";
4
+ import { t as watchSession } from "./watch-Dp73ph8h.js";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import path from "node:path";
7
7
  import fs from "node:fs/promises";
@@ -22,6 +22,38 @@ function isAcpRuntimeError(value) {
22
22
  return value instanceof AcpRuntimeError;
23
23
  }
24
24
  //#endregion
25
+ //#region src/runtime/engine/controls.ts
26
+ /** Controls acpx implements for every backend it exposes. */
27
+ const ACPX_CAPABILITIES = { controls: [
28
+ "session/set_mode",
29
+ "session/set_model",
30
+ "session/set_config_option",
31
+ "session/status"
32
+ ] };
33
+ function advertisedConfigOptionIds(record) {
34
+ const configOptions = record?.acpx?.config_options;
35
+ if (!configOptions) return;
36
+ return new Set(configOptions.map((option) => option.id).filter((id) => typeof id === "string" && id.trim().length > 0));
37
+ }
38
+ /** Reports the config option keys the session last advertised, if any. */
39
+ function capabilitiesFromRecord(record) {
40
+ const advertisedIds = advertisedConfigOptionIds(record);
41
+ if (!advertisedIds || advertisedIds.size === 0) return ACPX_CAPABILITIES;
42
+ return {
43
+ ...ACPX_CAPABILITIES,
44
+ configOptionKeys: [...advertisedIds]
45
+ };
46
+ }
47
+ function resolveSupportedConfigOptionId(record, configId) {
48
+ const advertisedIds = advertisedConfigOptionIds(record);
49
+ if (!advertisedIds) return configId;
50
+ if (advertisedIds.has(configId)) return configId;
51
+ if (configId === "thinking" && advertisedIds.has("effort")) return "effort";
52
+ const supported = [...advertisedIds].toSorted();
53
+ const supportedText = supported.length > 0 ? supported.join(", ") : "none";
54
+ throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
55
+ }
56
+ //#endregion
25
57
  //#region src/runtime/public/shared.ts
26
58
  function isRecord(value) {
27
59
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -547,6 +579,61 @@ function updateStatusEvent(payload, tag) {
547
579
  }
548
580
  //#endregion
549
581
  //#region src/runtime/public/probe.ts
582
+ /**
583
+ * Inspect session model metadata without a runtime store or prompt. The agent
584
+ * may create native session history; only its owned transport is closed.
585
+ */
586
+ async function inspectAgentModels(options) {
587
+ options.signal?.throwIfAborted();
588
+ const timeoutMs = options.timeoutMs ?? 12e4;
589
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) throw new RangeError("Model inspection timeoutMs must be positive and at most 2147483647");
590
+ const deadline = new AbortController();
591
+ const signal = options.signal ? AbortSignal.any([options.signal, deadline.signal]) : deadline.signal;
592
+ const assertActive = () => signal.throwIfAborted();
593
+ const client = new AcpClient({
594
+ ...normalizeAgentCommandInput(options.agentCommand),
595
+ cwd: options.cwd,
596
+ agentProcessEnv: options.agentProcessEnv,
597
+ permissionMode: "deny-all",
598
+ nonInteractivePermissions: "deny",
599
+ fs: false,
600
+ terminal: false,
601
+ processLifecycle: {
602
+ onBeforeSpawn: assertActive,
603
+ onSpawned: assertActive
604
+ }
605
+ });
606
+ const timer = setTimeout(() => deadline.abort(new TimeoutError(timeoutMs)), timeoutMs);
607
+ let onAbort;
608
+ const aborted = new Promise((_resolve, reject) => {
609
+ onAbort = () => reject(signal.reason);
610
+ signal.addEventListener("abort", onAbort, { once: true });
611
+ });
612
+ const inspection = (async () => {
613
+ assertActive();
614
+ await client.start();
615
+ assertActive();
616
+ const { models } = await client.createSession();
617
+ assertActive();
618
+ return models ? {
619
+ currentModelId: models.currentModelId,
620
+ availableModelIds: models.availableModels.map((model) => model.modelId),
621
+ availableModels: models.availableModels
622
+ } : void 0;
623
+ })();
624
+ try {
625
+ return await Promise.race([inspection, aborted]);
626
+ } finally {
627
+ clearTimeout(timer);
628
+ signal.removeEventListener("abort", onAbort);
629
+ try {
630
+ if (signal.aborted) await client.close();
631
+ } finally {
632
+ await inspection.catch(() => {});
633
+ await client.close();
634
+ }
635
+ }
636
+ }
550
637
  function isPrimitiveDetail(value) {
551
638
  return value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol";
552
639
  }
@@ -863,19 +950,13 @@ function createRecordId(sessionKey, mode) {
863
950
  function resumePolicyForSessionMode(mode) {
864
951
  return mode === "persistent" ? "same-session-only" : "allow-new";
865
952
  }
866
- function advertisedConfigOptionIds(record) {
867
- const configOptions = record.acpx?.config_options;
868
- if (!configOptions) return;
869
- return new Set(configOptions.map((option) => option.id).filter((id) => typeof id === "string" && id.trim().length > 0));
870
- }
871
- function resolveSupportedConfigOptionId(record, configId) {
872
- const advertisedIds = advertisedConfigOptionIds(record);
873
- if (!advertisedIds) return configId;
874
- if (advertisedIds.has(configId)) return configId;
875
- if (configId === "thinking" && advertisedIds.has("effort")) return "effort";
876
- const supported = [...advertisedIds].toSorted();
877
- const supportedText = supported.length > 0 ? supported.join(", ") : "none";
878
- throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
953
+ async function startRuntimeClient(client, agent, timeoutMs) {
954
+ try {
955
+ await withTimeout(client.start(), timeoutMs);
956
+ } catch (error) {
957
+ if (!(error instanceof TimeoutError)) throw error;
958
+ throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACP agent ${path.basename(agent.agentArgv?.[0] ?? agent.agentCommand.split(/\s+/)[0])} did not complete ACP initialization within ${timeoutMs}ms. Check that the configured command starts an ACP server.`, { cause: error });
959
+ }
879
960
  }
880
961
  async function createOrLoadRuntimeSession(client, resumeSessionId, cwd) {
881
962
  if (resumeSessionId) {
@@ -906,6 +987,7 @@ var AcpRuntimeManager = class {
906
987
  options;
907
988
  deps;
908
989
  activeControllers = /* @__PURE__ */ new Map();
990
+ controlSessionRecords = /* @__PURE__ */ new Map();
909
991
  retainedSessionOwners = /* @__PURE__ */ new Map();
910
992
  retiringSessionOwners = /* @__PURE__ */ new Set();
911
993
  pendingOneShotRecordIds = /* @__PURE__ */ new Map();
@@ -1022,6 +1104,7 @@ var AcpRuntimeManager = class {
1022
1104
  createSessionOwner(input) {
1023
1105
  const owner = {
1024
1106
  ...input,
1107
+ checkpoint: new LiveSessionCheckpoint({ save: async () => this.saveSessionOwner(owner) }),
1025
1108
  bufferSessionUpdates: false,
1026
1109
  pendingSessionUpdates: []
1027
1110
  };
@@ -1031,13 +1114,26 @@ var AcpRuntimeManager = class {
1031
1114
  });
1032
1115
  return owner;
1033
1116
  }
1117
+ async saveSessionOwner(owner) {
1118
+ if (!owner.recordId) return;
1119
+ const active = owner.activeTurn?.turn;
1120
+ const target = active ?? owner.projection;
1121
+ if (!target) return;
1122
+ const { record, conversation } = target;
1123
+ if (active?.connected) record.acpx = active.acpxState;
1124
+ record.lastUsedAt = isoNow();
1125
+ applyConversation(record, conversation);
1126
+ applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
1127
+ await this.refreshClosedState(record);
1128
+ await this.options.sessionStore.save(record);
1129
+ }
1034
1130
  routeOwnedSessionUpdate(owner, notification) {
1035
1131
  const active = owner.activeTurn;
1036
1132
  if (active) {
1037
1133
  const { task, turn } = active;
1038
1134
  if (turn.connected) {
1039
1135
  turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
1040
- turn.liveCheckpoint.request();
1136
+ owner.checkpoint.request();
1041
1137
  } else turn.record.acpx = recordSessionUpdate(turn.conversation, turn.record.acpx, notification);
1042
1138
  trimConversationForRuntime(turn.conversation);
1043
1139
  this.emitRuntimeTurnEvent(task, {
@@ -1058,7 +1154,7 @@ var AcpRuntimeManager = class {
1058
1154
  }
1059
1155
  projection.record.acpx = recordSessionUpdate(projection.conversation, projection.record.acpx, notification);
1060
1156
  trimConversationForRuntime(projection.conversation);
1061
- projection.checkpoint.request();
1157
+ owner.checkpoint.request();
1062
1158
  }
1063
1159
  routeOwnedClientOperation(owner, operation) {
1064
1160
  const active = owner.activeTurn;
@@ -1066,7 +1162,7 @@ var AcpRuntimeManager = class {
1066
1162
  const { task, turn } = active;
1067
1163
  if (turn.connected) {
1068
1164
  turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
1069
- turn.liveCheckpoint.request();
1165
+ owner.checkpoint.request();
1070
1166
  } else turn.record.acpx = recordClientOperation(turn.conversation, turn.record.acpx, operation);
1071
1167
  trimConversationForRuntime(turn.conversation);
1072
1168
  this.emitRuntimeTurnEvent(task, {
@@ -1078,15 +1174,7 @@ var AcpRuntimeManager = class {
1078
1174
  record.acpx = acpxState;
1079
1175
  owner.projection = {
1080
1176
  record,
1081
- conversation,
1082
- checkpoint: new LiveSessionCheckpoint({ save: async () => {
1083
- if (!owner.recordId) return;
1084
- record.lastUsedAt = isoNow();
1085
- applyConversation(record, conversation);
1086
- applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
1087
- await this.refreshClosedState(record);
1088
- await this.options.sessionStore.save(record);
1089
- } })
1177
+ conversation
1090
1178
  };
1091
1179
  owner.activeTurn = void 0;
1092
1180
  this.drainPendingSessionUpdates(owner);
@@ -1096,7 +1184,7 @@ var AcpRuntimeManager = class {
1096
1184
  }
1097
1185
  async flushSessionOwner(owner) {
1098
1186
  await owner.client.waitForSessionUpdatesIdle?.().catch(() => {});
1099
- await owner.projection?.checkpoint.flush();
1187
+ await owner.checkpoint.flush();
1100
1188
  }
1101
1189
  removeRetainedSessionOwner(owner) {
1102
1190
  if (owner.recordId && this.retainedSessionOwners.get(owner.recordId) === owner) this.retainedSessionOwners.delete(owner.recordId);
@@ -1127,7 +1215,7 @@ var AcpRuntimeManager = class {
1127
1215
  await this.closeClient(owner.client).catch(() => {
1128
1216
  this.retiringSessionOwners.add(owner);
1129
1217
  });
1130
- await owner.projection?.checkpoint.flush().catch(() => {});
1218
+ await owner.checkpoint.flush().catch(() => {});
1131
1219
  try {
1132
1220
  owner.client.clearEventHandlers();
1133
1221
  } catch {}
@@ -1150,19 +1238,15 @@ var AcpRuntimeManager = class {
1150
1238
  const latest = await this.options.sessionStore.load(record.acpxRecordId).catch(() => void 0);
1151
1239
  record.closed = true;
1152
1240
  record.closedAt = latest?.closedAt ?? record.closedAt ?? isoNow();
1153
- if (latest?.acpx) record.acpx = {
1241
+ if (latest?.acpx?.reset_on_next_ensure === true) record.acpx = {
1154
1242
  ...record.acpx,
1155
- ...latest.acpx
1243
+ reset_on_next_ensure: true
1156
1244
  };
1157
1245
  return true;
1158
1246
  }
1159
1247
  async retainPersistentSessionOwnerAfterTurn(input) {
1160
- const { record, owner, conversation, acpxState } = input;
1161
- if (!this.canRetainPersistentSessionOwner(owner, record)) {
1162
- owner.activeTurn = void 0;
1163
- return false;
1164
- }
1165
- this.attachIdleProjection(owner, record, conversation, acpxState);
1248
+ const { record, owner } = input;
1249
+ if (!this.canRetainPersistentSessionOwner(owner, record)) return false;
1166
1250
  const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
1167
1251
  this.retainedSessionOwners.set(record.acpxRecordId, owner);
1168
1252
  if (previousOwner && previousOwner !== owner) {
@@ -1184,6 +1268,21 @@ var AcpRuntimeManager = class {
1184
1268
  agentArgv: agentArgv ? [...agentArgv] : void 0
1185
1269
  }) : servers ?? []];
1186
1270
  }
1271
+ resolveSessionPermissions(session) {
1272
+ const { sessionKey, cwd, agentCommand, agentArgv } = session;
1273
+ const { permissionMode = this.options.permissionMode, nonInteractivePermissions = this.options.nonInteractivePermissions, permissionPolicy = this.options.permissionPolicy, onPermissionRequest = this.options.onPermissionRequest } = this.options.sessionPermissions?.({
1274
+ sessionKey,
1275
+ cwd,
1276
+ agentCommand,
1277
+ agentArgv: agentArgv ? [...agentArgv] : void 0
1278
+ }) ?? {};
1279
+ return {
1280
+ permissionMode,
1281
+ nonInteractivePermissions,
1282
+ permissionPolicy,
1283
+ onPermissionRequest
1284
+ };
1285
+ }
1187
1286
  async withRuntimeControlSession(record, sessionMode, run, replacingConfigOption, authority) {
1188
1287
  await this.retrySessionCleanup(record.acpxRecordId);
1189
1288
  assertControlAuthority(authority);
@@ -1198,20 +1297,27 @@ var AcpRuntimeManager = class {
1198
1297
  record: ownedRecord
1199
1298
  });
1200
1299
  this.refreshOwnedRecordLifecycle(owner, ownedRecord);
1201
- return {
1202
- value,
1203
- record: ownedRecord
1204
- };
1300
+ await owner.checkpoint.checkpoint();
1301
+ return value;
1205
1302
  } finally {
1206
1303
  await this.flushSessionOwner(owner);
1207
1304
  }
1208
1305
  }
1209
1306
  let controlClient;
1307
+ let controlledRecord = record;
1308
+ const checkpoint = new LiveSessionCheckpoint({ save: async () => this.options.sessionStore.save(controlledRecord) });
1210
1309
  try {
1211
- const result = await withConnectedSession({
1310
+ const result = await settleAttempt(async () => withConnectedSession({
1212
1311
  sessionRecordId: record.acpxRecordId,
1213
- loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
1214
- saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
1312
+ loadRecord: async (sessionRecordId) => {
1313
+ controlledRecord = await this.requireRecord(sessionRecordId);
1314
+ this.controlSessionRecords.set(sessionRecordId, {
1315
+ record: controlledRecord,
1316
+ checkpoint
1317
+ });
1318
+ return controlledRecord;
1319
+ },
1320
+ saveRecord: async () => checkpoint.checkpoint(),
1215
1321
  createClient: (options) => controlClient = this.createClient({
1216
1322
  ...options,
1217
1323
  processLifecycle: this.options.processLifecycle,
@@ -1224,10 +1330,10 @@ var AcpRuntimeManager = class {
1224
1330
  ...record,
1225
1331
  sessionKey: record.name ?? record.acpxRecordId
1226
1332
  }),
1227
- permissionMode: this.options.permissionMode,
1228
- nonInteractivePermissions: this.options.nonInteractivePermissions,
1229
- permissionPolicy: this.options.permissionPolicy,
1230
- onPermissionRequest: this.options.onPermissionRequest,
1333
+ ...this.resolveSessionPermissions({
1334
+ ...record,
1335
+ sessionKey: record.name ?? record.acpxRecordId
1336
+ }),
1231
1337
  elicitationModes: this.options.elicitationModes,
1232
1338
  verbose: this.options.verbose,
1233
1339
  timeoutMs: this.options.timeoutMs,
@@ -1235,12 +1341,14 @@ var AcpRuntimeManager = class {
1235
1341
  replacingConfigOption,
1236
1342
  authority,
1237
1343
  run
1238
- });
1239
- return {
1240
- value: result.value,
1241
- record: result.record
1242
- };
1344
+ }));
1345
+ this.controlSessionRecords.delete(record.acpxRecordId);
1346
+ const flushed = await settleAttempt(async () => checkpoint.flush());
1347
+ if (!result.ok) throw result.error;
1348
+ if (!flushed.ok) throw flushed.error;
1349
+ return result.value.value;
1243
1350
  } finally {
1351
+ this.controlSessionRecords.delete(record.acpxRecordId);
1244
1352
  if (controlClient) this.liveClients.delete(controlClient);
1245
1353
  }
1246
1354
  }
@@ -1262,6 +1370,13 @@ var AcpRuntimeManager = class {
1262
1370
  async withManagerLock(locks, key, run, authority) {
1263
1371
  this.assertOpen();
1264
1372
  assertControlAuthority(authority);
1373
+ return await this.serializeManagerOperation(locks, key, async () => {
1374
+ this.assertOpen();
1375
+ assertControlAuthority(authority);
1376
+ return await run();
1377
+ });
1378
+ }
1379
+ async serializeManagerOperation(locks, key, run) {
1265
1380
  const previous = locks.get(key) ?? Promise.resolve();
1266
1381
  let release;
1267
1382
  const gate = new Promise((resolve) => {
@@ -1271,8 +1386,6 @@ var AcpRuntimeManager = class {
1271
1386
  locks.set(key, tail);
1272
1387
  await previous;
1273
1388
  try {
1274
- this.assertOpen();
1275
- assertControlAuthority(authority);
1276
1389
  return await run();
1277
1390
  } finally {
1278
1391
  release();
@@ -1287,8 +1400,26 @@ var AcpRuntimeManager = class {
1287
1400
  agentCommand,
1288
1401
  agentArgv
1289
1402
  };
1403
+ if (input.mode !== "persistent") return await this.acquireRuntimeSession(input, agent);
1404
+ const compatible = await this.reuseCompatiblePersistentSession(input, agent);
1405
+ if (compatible) return compatible;
1406
+ this.assertRuntimeSessionIdle(input.sessionKey);
1407
+ return await this.withManagerLock(this.runtimeOperationLocks, input.sessionKey, async () => this.acquireRuntimeSession(input, agent));
1408
+ }
1409
+ async reuseCompatiblePersistentSession(input, agent) {
1410
+ const controlled = this.controlSessionRecords.get(input.sessionKey);
1411
+ const record = controlled ? controlled.record : await this.findSession(input.sessionKey);
1412
+ if (!record || this.closingActiveRecords.has(input.sessionKey) || !shouldReuseExistingRecord(record, {
1413
+ ...agent,
1414
+ resumeSessionId: input.resumeSessionId
1415
+ })) return;
1416
+ if (!record.closed) return record;
1417
+ if (!controlled || this.sessionTasks.has(input.sessionKey)) return;
1418
+ return await this.reuseRuntimeSession({ record }, controlled.checkpoint);
1419
+ }
1420
+ async acquireRuntimeSession(input, agent) {
1290
1421
  const existing = await this.loadExistingRuntimeSession(input);
1291
- if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing.record);
1422
+ if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing);
1292
1423
  await this.closeConflictingPersistentSession(input, existing?.owner);
1293
1424
  return await this.createOwnedRuntimeSession(input, agent);
1294
1425
  }
@@ -1319,17 +1450,26 @@ var AcpRuntimeManager = class {
1319
1450
  resumeSessionId: input.resumeSessionId
1320
1451
  })) return false;
1321
1452
  if (input.mode === "persistent") return true;
1322
- return Boolean(existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), input.sessionOptions));
1453
+ return Boolean(existing.owner && !existing.record.closed && this.pendingOneShotRecordIds.get(input.sessionKey) === existing.record.acpxRecordId && this.retainedSessionOwners.get(existing.record.acpxRecordId) === existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), input.sessionOptions));
1454
+ }
1455
+ assertRuntimeSessionIdle(recordId) {
1456
+ if (this.sessionTasks.has(recordId) || this.runtimeOperationLocks.has(recordId)) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `Cannot ensure session ${recordId} with incompatible or closing state while its work is unfinished. Wait for existing work to finish or use a different session key.`);
1323
1457
  }
1324
- async reuseRuntimeSession(record) {
1458
+ async reuseRuntimeSession({ record, owner }, checkpoint = owner?.checkpoint) {
1459
+ if (!record.closed && !this.closingActiveRecords.has(record.acpxRecordId)) return record;
1325
1460
  record.closed = false;
1326
1461
  record.closedAt = void 0;
1327
1462
  this.closingActiveRecords.delete(record.acpxRecordId);
1328
- await this.options.sessionStore.save(record);
1463
+ if (checkpoint) await checkpoint.checkpoint();
1464
+ else await this.options.sessionStore.save(record);
1329
1465
  return record;
1330
1466
  }
1331
1467
  async closeConflictingPersistentSession(input, owner) {
1332
- if (input.mode === "persistent" && owner?.recordId) await this.closeRetainedSessionOwner(owner.recordId);
1468
+ if (input.mode === "persistent" && owner?.recordId) {
1469
+ this.removeRetainedSessionOwner(owner);
1470
+ this.retiringSessionOwners.add(owner);
1471
+ await this.retrySessionCleanup(owner.recordId);
1472
+ }
1333
1473
  }
1334
1474
  async createOwnedRuntimeSession(input, agent) {
1335
1475
  const { cwd, agentCommand, agentArgv } = agent;
@@ -1341,10 +1481,10 @@ var AcpRuntimeManager = class {
1341
1481
  ...agent,
1342
1482
  sessionKey: input.sessionKey
1343
1483
  }),
1344
- permissionMode: this.options.permissionMode,
1345
- nonInteractivePermissions: this.options.nonInteractivePermissions,
1346
- permissionPolicy: this.options.permissionPolicy,
1347
- onPermissionRequest: this.options.onPermissionRequest,
1484
+ ...this.resolveSessionPermissions({
1485
+ ...agent,
1486
+ sessionKey: input.sessionKey
1487
+ }),
1348
1488
  elicitationModes: this.options.elicitationModes,
1349
1489
  processLifecycle: this.options.processLifecycle,
1350
1490
  processLaunchScope: {
@@ -1361,7 +1501,7 @@ var AcpRuntimeManager = class {
1361
1501
  });
1362
1502
  let retained = false;
1363
1503
  try {
1364
- await client.start();
1504
+ await startRuntimeClient(client, agent, this.options.timeoutMs);
1365
1505
  this.assertOpen();
1366
1506
  const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd);
1367
1507
  const record = await this.prepareInitialRuntimeRecord({
@@ -1415,7 +1555,7 @@ var AcpRuntimeManager = class {
1415
1555
  }
1416
1556
  async retainInitializedSessionOwner(owner, record) {
1417
1557
  owner.recordId = record.acpxRecordId;
1418
- await owner.projection?.checkpoint.checkpoint();
1558
+ await owner.checkpoint.checkpoint();
1419
1559
  this.assertOpen();
1420
1560
  const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
1421
1561
  this.retainedSessionOwners.set(record.acpxRecordId, owner);
@@ -1465,7 +1605,7 @@ var AcpRuntimeManager = class {
1465
1605
  return true;
1466
1606
  };
1467
1607
  const abortHandler = () => {
1468
- requestCancel();
1608
+ requestCancel().catch(() => {});
1469
1609
  };
1470
1610
  if (input.signal && !input.signal.aborted) input.signal.addEventListener("abort", abortHandler, { once: true });
1471
1611
  this.queueSessionTask(input.handle.acpxRecordId ?? input.handle.sessionKey, async () => this.runRuntimeTurnTask({
@@ -1493,7 +1633,9 @@ var AcpRuntimeManager = class {
1493
1633
  }
1494
1634
  async closeRetainedOneShotHandle(handle) {
1495
1635
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
1496
- if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
1636
+ await this.serializeManagerOperation(this.runtimeOperationLocks, recordId, async () => {
1637
+ if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
1638
+ });
1497
1639
  }
1498
1640
  async runRuntimeTurnTask(task) {
1499
1641
  let turn;
@@ -1515,9 +1657,8 @@ var AcpRuntimeManager = class {
1515
1657
  stopReason: "cancelled"
1516
1658
  };
1517
1659
  else {
1518
- await this.applyPendingRuntimeTurnCancel(task, turn);
1519
1660
  const response = await this.runRuntimePrompt(task, turn, sessionId);
1520
- await this.saveCompletedRuntimeTurn(turn);
1661
+ this.updateCompletedRuntimeTurn(turn);
1521
1662
  terminalResult = {
1522
1663
  status: response.stopReason === "cancelled" ? "cancelled" : "completed",
1523
1664
  ...response.stopReason ? { stopReason: response.stopReason } : {},
@@ -1526,9 +1667,9 @@ var AcpRuntimeManager = class {
1526
1667
  }
1527
1668
  }
1528
1669
  } catch (error) {
1529
- terminalResult = this.failRuntimeTurn(task, error);
1670
+ terminalResult = this.handleRuntimeTurnFailure(task, error);
1530
1671
  }
1531
- const finalization = await settleAttempt(async () => this.finalizeRuntimeTurn(task, turn));
1672
+ const finalization = await settleAttempt(async () => this.serializeManagerOperation(this.runtimeOperationLocks, task.input.handle.acpxRecordId ?? task.input.handle.sessionKey, async () => this.finalizeRuntimeTurn(task, turn)));
1532
1673
  if (!finalization.ok) {
1533
1674
  task.settleResult(this.failRuntimeTurn(task, finalization.error));
1534
1675
  throw finalization.error;
@@ -1536,21 +1677,18 @@ var AcpRuntimeManager = class {
1536
1677
  task.settleResult(terminalResult);
1537
1678
  }
1538
1679
  async runRuntimePrompt(task, turn, sessionId) {
1539
- try {
1540
- return await runPromptTurn({
1541
- client: turn.client,
1542
- sessionId,
1543
- prompt: task.promptInput,
1544
- timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
1545
- conversation: turn.conversation,
1546
- promptMessageId: turn.promptMessageId,
1547
- onPromptRequestWritten: () => task.promptStarted.resolve(),
1548
- onElicitation: task.input.onElicitation,
1549
- onPermissionRequest: task.input.onPermissionRequest
1550
- });
1551
- } finally {
1552
- turn.client.endPromptElicitation?.(sessionId);
1553
- }
1680
+ return await runPromptTurn({
1681
+ client: turn.client,
1682
+ sessionId,
1683
+ prompt: task.promptInput,
1684
+ timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
1685
+ conversation: turn.conversation,
1686
+ promptMessageId: turn.promptMessageId,
1687
+ onPromptRequestWritten: () => task.promptStarted.resolve(),
1688
+ onElicitation: task.input.onElicitation,
1689
+ onPermissionRequest: task.input.onPermissionRequest,
1690
+ authority: task.input
1691
+ });
1554
1692
  }
1555
1693
  async prepareRuntimeTurn(task) {
1556
1694
  const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
@@ -1610,13 +1748,19 @@ var AcpRuntimeManager = class {
1610
1748
  let record = await this.requireRecord(recordId);
1611
1749
  const retainedOwner = await this.readRetainedSessionOwner(record, { consume: false });
1612
1750
  if (!retainedOwner) return { record };
1613
- const projection = retainedOwner.projection;
1614
- if (projection) record = structuredClone(projection.record);
1615
1751
  retainedOwner.bufferSessionUpdates = true;
1616
- return {
1617
- record,
1618
- retainedOwner
1619
- };
1752
+ try {
1753
+ await retainedOwner.checkpoint.flush();
1754
+ const projection = retainedOwner.projection;
1755
+ if (projection) record = structuredClone(projection.record);
1756
+ return {
1757
+ record,
1758
+ retainedOwner
1759
+ };
1760
+ } catch (error) {
1761
+ this.restoreBufferedSessionOwner(retainedOwner);
1762
+ throw error;
1763
+ }
1620
1764
  }
1621
1765
  restoreBufferedSessionOwner(owner) {
1622
1766
  if (!owner) return;
@@ -1632,18 +1776,16 @@ var AcpRuntimeManager = class {
1632
1776
  }
1633
1777
  createRunningRuntimeTurn(input) {
1634
1778
  const { record, conversation, acpxState, client, owner, connected, promptMessageId } = input;
1635
- const turn = {
1779
+ return {
1636
1780
  record,
1637
1781
  conversation,
1638
1782
  acpxState,
1639
- liveCheckpoint: this.createRuntimeTurnCheckpoint(record, conversation, () => turn.acpxState),
1640
1783
  client,
1641
1784
  owner,
1642
1785
  connected,
1643
1786
  promptMessageId,
1644
1787
  activeSessionId: record.acpSessionId
1645
1788
  };
1646
- return turn;
1647
1789
  }
1648
1790
  activateRuntimeTurn(task, turn) {
1649
1791
  const { owner, record } = turn;
@@ -1668,10 +1810,10 @@ var AcpRuntimeManager = class {
1668
1810
  ...record,
1669
1811
  sessionKey: record.name ?? record.acpxRecordId
1670
1812
  }),
1671
- permissionMode: this.options.permissionMode,
1672
- nonInteractivePermissions: this.options.nonInteractivePermissions,
1673
- permissionPolicy: this.options.permissionPolicy,
1674
- onPermissionRequest: this.options.onPermissionRequest,
1813
+ ...this.resolveSessionPermissions({
1814
+ ...record,
1815
+ sessionKey: record.name ?? record.acpxRecordId
1816
+ }),
1675
1817
  elicitationModes: this.options.elicitationModes,
1676
1818
  processLifecycle: this.options.processLifecycle,
1677
1819
  processLaunchScope: {
@@ -1682,15 +1824,6 @@ var AcpRuntimeManager = class {
1682
1824
  sessionOptions: sessionOptionsFromRecord(record)
1683
1825
  });
1684
1826
  }
1685
- createRuntimeTurnCheckpoint(record, conversation, readAcpxState) {
1686
- return new LiveSessionCheckpoint({ save: async () => {
1687
- record.lastUsedAt = isoNow();
1688
- record.acpx = readAcpxState();
1689
- applyConversation(record, conversation);
1690
- await this.refreshClosedState(record);
1691
- await this.options.sessionStore.save(record);
1692
- } });
1693
- }
1694
1827
  buildRuntimeTurnController(task, turn) {
1695
1828
  return {
1696
1829
  hasActivePrompt: () => turn.client.hasActivePrompt(),
@@ -1701,12 +1834,14 @@ var AcpRuntimeManager = class {
1701
1834
  const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
1702
1835
  nextState.desired_mode_id = modeId;
1703
1836
  turn.acpxState = nextState;
1837
+ await turn.owner.checkpoint.checkpoint();
1704
1838
  },
1705
1839
  setSessionModel: async (modelId, authority) => {
1706
1840
  await this.waitForRuntimeControlSession(task, turn);
1707
1841
  const models = advertisedModelState(turn.acpxState);
1708
1842
  const response = await turn.client.setSessionModel(turn.activeSessionId, modelId, models, authority);
1709
1843
  turn.acpxState = applyModelSelection(turn.acpxState, modelId, response);
1844
+ await turn.owner.checkpoint.checkpoint();
1710
1845
  return response;
1711
1846
  },
1712
1847
  setSessionConfigOption: async (configId, value, authority) => {
@@ -1731,9 +1866,10 @@ var AcpRuntimeManager = class {
1731
1866
  ...turn.record,
1732
1867
  acpx: turn.acpxState ?? void 0
1733
1868
  }, configId);
1734
- const modelConfigId = advertisedModelState(turn.acpxState)?.configId;
1735
- const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value, authority);
1736
- turn.acpxState = applyConfigOptionSelection(turn.acpxState, resolvedConfigId, value, response, modelConfigId);
1869
+ const models = advertisedModelState(turn.acpxState);
1870
+ const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value, models, authority);
1871
+ turn.acpxState = applyConfigOptionSelection(turn.acpxState, resolvedConfigId, value, response, models?.configId);
1872
+ await turn.owner.checkpoint.checkpoint();
1737
1873
  return {
1738
1874
  configId: resolvedConfigId,
1739
1875
  response
@@ -1782,7 +1918,7 @@ var AcpRuntimeManager = class {
1782
1918
  turn.record.closed = false;
1783
1919
  turn.record.closedAt = void 0;
1784
1920
  turn.record.lastUsedAt = isoNow();
1785
- await turn.liveCheckpoint.checkpoint();
1921
+ await turn.owner.checkpoint.checkpoint();
1786
1922
  }
1787
1923
  cancelRuntimeTurnBeforePrompt(task) {
1788
1924
  if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
@@ -1790,21 +1926,20 @@ var AcpRuntimeManager = class {
1790
1926
  task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
1791
1927
  return true;
1792
1928
  }
1793
- async applyPendingRuntimeTurnCancel(task, turn) {
1794
- if (!task.state.pendingCancel || !turn.client.hasActivePrompt()) return false;
1795
- const cancelled = await turn.client.requestCancelActivePrompt();
1796
- if (cancelled) task.state.pendingCancel = false;
1797
- return cancelled;
1798
- }
1799
- async saveCompletedRuntimeTurn(turn) {
1929
+ updateCompletedRuntimeTurn(turn) {
1800
1930
  turn.record.acpSessionId = turn.activeSessionId;
1801
1931
  reconcileAgentSessionId(turn.record, turn.record.agentSessionId);
1802
1932
  turn.record.protocolVersion = turn.client.initializeResult?.protocolVersion;
1803
1933
  turn.record.agentCapabilities = turn.client.initializeResult?.agentCapabilities;
1804
- turn.record.acpx = turn.acpxState;
1805
- applyConversation(turn.record, turn.conversation);
1806
- applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1807
- await this.options.sessionStore.save(turn.record);
1934
+ }
1935
+ handleRuntimeTurnFailure(task, error) {
1936
+ if (!task.input.signal?.aborted || error !== task.input.signal.reason) return this.failRuntimeTurn(task, error);
1937
+ task.promptStarted.reject(error);
1938
+ task.sessionReady.reject(error);
1939
+ return {
1940
+ status: "cancelled",
1941
+ stopReason: "cancelled"
1942
+ };
1808
1943
  }
1809
1944
  failRuntimeTurn(task, error) {
1810
1945
  task.promptStarted.reject(error);
@@ -1838,13 +1973,24 @@ var AcpRuntimeManager = class {
1838
1973
  discardRetainedRuntimeTurnOwner(turn) {
1839
1974
  if (turn) {
1840
1975
  this.removeRetainedSessionOwner(turn.owner);
1841
- turn.owner.activeTurn = void 0;
1976
+ this.attachFinalRuntimeProjection(turn);
1842
1977
  }
1843
1978
  }
1979
+ attachFinalRuntimeProjection(turn) {
1980
+ const current = turn.connected ? turn.acpxState : turn.record.acpx;
1981
+ const state = turn.record.acpx?.reset_on_next_ensure === true ? {
1982
+ ...current,
1983
+ reset_on_next_ensure: true
1984
+ } : current;
1985
+ this.attachIdleProjection(turn.owner, turn.record, turn.conversation, state);
1986
+ }
1844
1987
  async closeRuntimeTurnClient(turn, pooled) {
1845
1988
  if (!turn || pooled) return;
1846
- turn.owner.activeTurn = void 0;
1847
- const failure = firstFailedAttempt([await settleAttempt(() => turn.client.clearEventHandlers()), await settleAttempt(async () => this.closeClient(turn.client))]);
1989
+ const failure = firstFailedAttempt([
1990
+ await settleAttempt(() => turn.client.clearEventHandlers()),
1991
+ await settleAttempt(async () => this.closeClient(turn.client)),
1992
+ await settleAttempt(async () => this.flushSessionOwner(turn.owner))
1993
+ ]);
1848
1994
  if (failure) {
1849
1995
  this.retiringSessionOwners.add(turn.owner);
1850
1996
  throw failure.error;
@@ -1859,19 +2005,12 @@ var AcpRuntimeManager = class {
1859
2005
  }
1860
2006
  async finalizeRuntimeTurnRecord(turn) {
1861
2007
  if (!turn.connected) turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
1862
- applyLifecycleSnapshotToRecord(turn.record, turn.client.getAgentLifecycleSnapshot());
1863
- turn.record.acpx = turn.acpxState;
1864
- applyConversation(turn.record, turn.conversation);
1865
- turn.record.lastUsedAt = isoNow();
1866
- await turn.liveCheckpoint.flush();
1867
- const closed = await this.refreshClosedState(turn.record);
1868
- await this.options.sessionStore.save(turn.record);
1869
- if (closed || !turn.connected) return false;
2008
+ await turn.owner.checkpoint.checkpoint();
2009
+ this.attachFinalRuntimeProjection(turn);
2010
+ if (turn.record.closed || !turn.connected) return false;
1870
2011
  return await this.retainPersistentSessionOwnerAfterTurn({
1871
2012
  record: turn.record,
1872
- owner: turn.owner,
1873
- conversation: turn.conversation,
1874
- acpxState: turn.acpxState
2013
+ owner: turn.owner
1875
2014
  });
1876
2015
  }
1877
2016
  async *runTurn(input) {
@@ -1897,13 +2036,14 @@ var AcpRuntimeManager = class {
1897
2036
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
1898
2037
  assertControlAuthority(authority);
1899
2038
  const controller = this.activeControllers.get(record.acpxRecordId);
1900
- let targetRecord = record;
1901
- if (controller) await controller.setSessionMode(mode, authority);
1902
- else targetRecord = (await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId }) => {
2039
+ if (controller) {
2040
+ await controller.setSessionMode(mode, authority);
2041
+ return;
2042
+ }
2043
+ await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1903
2044
  await client.setSessionMode(sessionId, mode, authority);
1904
- }, void 0, authority)).record;
1905
- setDesiredModeId(targetRecord, mode);
1906
- await this.options.sessionStore.save(targetRecord);
2045
+ setDesiredModeId(connectedRecord, mode);
2046
+ }, void 0, authority);
1907
2047
  }
1908
2048
  async setModel(handle, model, sessionMode = "persistent", authority) {
1909
2049
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
@@ -1914,16 +2054,13 @@ var AcpRuntimeManager = class {
1914
2054
  assertControlAuthority(authority);
1915
2055
  const controller = this.activeControllers.get(record.acpxRecordId);
1916
2056
  if (controller) {
1917
- const response = await controller.setSessionModel(model, authority);
1918
- record.acpx = applyModelSelection(record.acpx, model, response);
1919
- await this.options.sessionStore.save(record);
2057
+ await controller.setSessionModel(model, authority);
1920
2058
  return;
1921
2059
  }
1922
- const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
2060
+ await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1923
2061
  const response = await client.setSessionModel(sessionId, model, advertisedModelState(connectedRecord.acpx), authority);
1924
2062
  connectedRecord.acpx = applyModelSelection(connectedRecord.acpx, model, response);
1925
2063
  }, { key: "model" }, authority);
1926
- await this.options.sessionStore.save(result.record);
1927
2064
  }
1928
2065
  async setConfigOption(handle, key, value, sessionMode = "persistent", authority) {
1929
2066
  const recordId = handle.acpxRecordId ?? handle.sessionKey;
@@ -1934,23 +2071,19 @@ var AcpRuntimeManager = class {
1934
2071
  assertControlAuthority(authority);
1935
2072
  const controller = this.activeControllers.get(record.acpxRecordId);
1936
2073
  if (controller) {
1937
- const { configId, response } = await controller.setResolvedSessionConfigOption(key, value, authority);
1938
- record.acpx = applyConfigOptionSelection(record.acpx, configId, value, response);
1939
- await this.options.sessionStore.save(record);
2074
+ const { response } = await controller.setResolvedSessionConfigOption(key, value, authority);
1940
2075
  return response;
1941
2076
  }
1942
- const result = await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
2077
+ return await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
1943
2078
  const configId = resolveSupportedConfigOptionId(connectedRecord, key);
1944
- const modelConfigId = advertisedModelState(connectedRecord.acpx)?.configId;
1945
- const response = await client.setSessionConfigOption(sessionId, configId, value, authority);
1946
- connectedRecord.acpx = applyConfigOptionSelection(connectedRecord.acpx, configId, value, response, modelConfigId);
2079
+ const models = advertisedModelState(connectedRecord.acpx);
2080
+ const response = await client.setSessionConfigOption(sessionId, configId, value, models, authority);
2081
+ connectedRecord.acpx = applyConfigOptionSelection(connectedRecord.acpx, configId, value, response, models?.configId);
1947
2082
  return response;
1948
2083
  }, {
1949
2084
  key,
1950
2085
  resolve: (connectedRecord) => resolveSupportedConfigOptionId(connectedRecord, key)
1951
2086
  }, authority);
1952
- await this.options.sessionStore.save(result.record);
1953
- return result.value;
1954
2087
  }
1955
2088
  async cancel(handle) {
1956
2089
  await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
@@ -2023,7 +2156,8 @@ var AcpRuntimeManager = class {
2023
2156
  if (connection.owner) await this.flushSessionOwner(connection.owner);
2024
2157
  }),
2025
2158
  await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
2026
- await settleAttempt(async () => this.closeClient(connection.client))
2159
+ await settleAttempt(async () => this.closeClient(connection.client)),
2160
+ await settleAttempt(async () => connection.owner?.checkpoint.flush())
2027
2161
  ]);
2028
2162
  if (failure) throw failure.error;
2029
2163
  }
@@ -2158,6 +2292,16 @@ function sharedRecordId(handle) {
2158
2292
  if (handle.backend !== SHARED_BACKEND || !handle.acpxRecordId) return invalidOption("A shared runtime requires a handle from a shared session.");
2159
2293
  return handle.acpxRecordId;
2160
2294
  }
2295
+ /**
2296
+ * Shared controls are owner-only. Falling back to a direct connection would
2297
+ * start a second adapter connection for a session another process owns, and
2298
+ * that path may create a fresh provider session when the saved one no longer
2299
+ * loads. Shared turns already refuse that trade; controls refuse it too.
2300
+ */
2301
+ function requireOwnerDispatch(result, control) {
2302
+ if (result === void 0) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", `No running owner holds this shared session, so ${control} was not sent. Submit a turn to start an owner, or apply the change with the equivalent acpx command, which connects directly instead of starting one.`);
2303
+ return result;
2304
+ }
2161
2305
  function turnFailure(error) {
2162
2306
  const normalized = normalizeOutputError(error, {
2163
2307
  defaultCode: "RUNTIME",
@@ -2183,11 +2327,13 @@ var SharedAcpRuntime = class {
2183
2327
  options;
2184
2328
  disconnect = new AbortController();
2185
2329
  pending = /* @__PURE__ */ new Set();
2330
+ controlChains = /* @__PURE__ */ new Map();
2186
2331
  constructor(options) {
2187
2332
  for (const name of [
2188
2333
  "sessionStore",
2189
2334
  "agentProcessEnv",
2190
2335
  "mcpServers",
2336
+ "sessionPermissions",
2191
2337
  "processLifecycle",
2192
2338
  "onPermissionRequest",
2193
2339
  "elicitationModes"
@@ -2239,7 +2385,7 @@ var SharedAcpRuntime = class {
2239
2385
  }
2240
2386
  startTurn(input) {
2241
2387
  this.assertOpen();
2242
- if (input.mode !== "prompt" || input.onPermissionRequest || input.onElicitation) invalidOption("Shared turns support prompt mode and the client's static permission policy; in-process callbacks and steering are unsupported.");
2388
+ if (input.mode !== "prompt" || input.assertActive || input.onPermissionRequest || input.onElicitation) invalidOption("Shared turns support prompt mode and the client's static permission policy; in-process callbacks and steering are unsupported.");
2243
2389
  if (!input.requestId.trim()) invalidOption("A shared turn requires a requestId.");
2244
2390
  const sessionId = sharedRecordId(input.handle);
2245
2391
  const prompt = toPromptInput(input.text, input.attachments);
@@ -2332,6 +2478,72 @@ var SharedAcpRuntime = class {
2332
2478
  this.assertOpen();
2333
2479
  return runtimeStatusFromRecord(await resolveSessionRecord(sharedRecordId(input.handle)));
2334
2480
  }
2481
+ async getCapabilities(input) {
2482
+ this.assertOpen();
2483
+ if (!input?.handle) return capabilitiesFromRecord(void 0);
2484
+ return capabilitiesFromRecord(await readSessionRecord(sharedRecordId(input.handle)));
2485
+ }
2486
+ /**
2487
+ * Preserves this client's control order, including caller-side persistence
2488
+ * when talking to older owners. Updated owners checkpoint accepted controls
2489
+ * themselves; serialization here cannot coordinate other legacy callers.
2490
+ */
2491
+ chainControl(sessionId, run) {
2492
+ const result = (this.controlChains.get(sessionId) ?? Promise.resolve()).then(run, run);
2493
+ const chained = result.then(() => void 0, () => void 0);
2494
+ this.controlChains.set(sessionId, chained);
2495
+ chained.then(() => {
2496
+ if (this.controlChains.get(sessionId) === chained) this.controlChains.delete(sessionId);
2497
+ });
2498
+ return result;
2499
+ }
2500
+ /**
2501
+ * Checks this client's authority locally, then hands the control to the
2502
+ * running owner. The check gates this client's dispatch only; it cannot gate
2503
+ * controls other clients of the same owner send, and it cannot recall a
2504
+ * control the owner already accepted.
2505
+ */
2506
+ async dispatchControl(input, control, send) {
2507
+ this.assertOpen();
2508
+ const sessionId = sharedRecordId(input.handle);
2509
+ const assertAuthority = () => {
2510
+ this.assertOpen();
2511
+ assertControlAuthority(input);
2512
+ };
2513
+ return await this.track(this.chainControl(sessionId, async () => {
2514
+ assertAuthority();
2515
+ return requireOwnerDispatch(await send(sessionId, assertAuthority), control);
2516
+ }));
2517
+ }
2518
+ async setMode(input) {
2519
+ await this.dispatchControl(input, "session/set_mode", async (sessionId, assertAuthority) => setSessionModeOnOwner({
2520
+ sessionId,
2521
+ modeId: input.mode,
2522
+ timeoutMs: this.options.timeoutMs,
2523
+ assertDispatch: assertAuthority
2524
+ }));
2525
+ }
2526
+ async setModel(input) {
2527
+ await this.dispatchControl(input, "session/set_model", async (sessionId, assertAuthority) => setSessionModelOnOwner({
2528
+ sessionId,
2529
+ modelId: input.model,
2530
+ timeoutMs: this.options.timeoutMs,
2531
+ assertDispatch: assertAuthority
2532
+ }));
2533
+ }
2534
+ async setConfigOption(input) {
2535
+ return (await this.dispatchControl(input, "session/set_config_option", async (sessionId, assertAuthority) => {
2536
+ const configId = resolveSupportedConfigOptionId(await resolveSessionRecord(sessionId), input.key);
2537
+ assertAuthority();
2538
+ return await setSessionConfigOptionOnOwner({
2539
+ sessionId,
2540
+ configId,
2541
+ value: input.value,
2542
+ timeoutMs: this.options.timeoutMs,
2543
+ assertDispatch: assertAuthority
2544
+ });
2545
+ })).response;
2546
+ }
2335
2547
  watchSession(input) {
2336
2548
  this.assertOpen();
2337
2549
  const signal = input.signal ? AbortSignal.any([input.signal, this.disconnect.signal]) : this.disconnect.signal;
@@ -2366,12 +2578,6 @@ function createSharedAcpRuntime(options) {
2366
2578
  //#endregion
2367
2579
  //#region src/runtime.ts
2368
2580
  const ACPX_BACKEND_ID = "acpx";
2369
- const ACPX_CAPABILITIES = { controls: [
2370
- "session/set_mode",
2371
- "session/set_model",
2372
- "session/set_config_option",
2373
- "session/status"
2374
- ] };
2375
2581
  var AcpxRuntime = class {
2376
2582
  options;
2377
2583
  testOptions;
@@ -2471,6 +2677,7 @@ var AcpxRuntime = class {
2471
2677
  requestId: input.requestId,
2472
2678
  timeoutMs: input.timeoutMs,
2473
2679
  signal: input.signal,
2680
+ assertActive: input.assertActive,
2474
2681
  onElicitation: input.onElicitation,
2475
2682
  onPermissionRequest: input.onPermissionRequest
2476
2683
  }));
@@ -2504,6 +2711,7 @@ var AcpxRuntime = class {
2504
2711
  requestId: input.requestId,
2505
2712
  timeoutMs: input.timeoutMs,
2506
2713
  signal: input.signal,
2714
+ assertActive: input.assertActive,
2507
2715
  onElicitation: input.onElicitation,
2508
2716
  onPermissionRequest: input.onPermissionRequest
2509
2717
  });
@@ -2511,13 +2719,7 @@ var AcpxRuntime = class {
2511
2719
  async getCapabilities(input) {
2512
2720
  if (!input?.handle) return ACPX_CAPABILITIES;
2513
2721
  const { handle } = this.resolveManagerHandle(input.handle);
2514
- const record = await this.options.sessionStore.load(handle.acpxRecordId ?? handle.sessionKey);
2515
- if (!record?.acpx?.config_options) return ACPX_CAPABILITIES;
2516
- const configOptionKeys = Array.from(new Set(record.acpx.config_options.map((option) => option.id).filter((id) => typeof id === "string" && id.trim().length > 0)));
2517
- return {
2518
- ...ACPX_CAPABILITIES,
2519
- ...configOptionKeys.length > 0 ? { configOptionKeys } : {}
2520
- };
2722
+ return capabilitiesFromRecord(await this.options.sessionStore.load(handle.acpxRecordId ?? handle.sessionKey));
2521
2723
  }
2522
2724
  async getStatus(input) {
2523
2725
  const { handle } = this.resolveManagerHandle(input.handle);
@@ -2616,6 +2818,6 @@ function createRuntimeStore(options) {
2616
2818
  return createFileSessionStore(options);
2617
2819
  }
2618
2820
  //#endregion
2619
- export { ACPX_BACKEND_ID, AcpRuntimeError, AcpxRuntime, DEFAULT_AGENT_NAME, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, REQUESTED_MODEL_UNSUPPORTED_REASONS, RequestedModelUnsupportedError, SharedAcpRuntime, createAcpRuntime, createAgentRegistry, createFileSessionStore, createRuntimeStore, createSharedAcpRuntime, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, isAcpRuntimeError, isRequestedModelUnsupportedError };
2821
+ export { ACPX_BACKEND_ID, AcpRuntimeError, AcpxRuntime, DEFAULT_AGENT_NAME, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, REQUESTED_MODEL_UNSUPPORTED_REASONS, RequestedModelUnsupportedError, SharedAcpRuntime, createAcpRuntime, createAgentRegistry, createFileSessionStore, createRuntimeStore, createSharedAcpRuntime, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, inspectAgentModels, isAcpRuntimeError, isRequestedModelUnsupportedError };
2620
2822
 
2621
2823
  //# sourceMappingURL=runtime.js.map