acpx 0.5.3 → 0.6.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 (47) hide show
  1. package/README.md +12 -4
  2. package/dist/{cli-ChWsO-bb.js → cli-Ddxpnz9X.js} +4 -4
  3. package/dist/{cli-ChWsO-bb.js.map → cli-Ddxpnz9X.js.map} +1 -1
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +147 -75
  7. package/dist/cli.js.map +1 -1
  8. package/dist/{client-D-4_aZf2.d.ts → client-2fTFutRH.d.ts} +4 -2
  9. package/dist/client-2fTFutRH.d.ts.map +1 -0
  10. package/dist/{flags-ceSqz2T6.js → flags-yXzUm7Aq.js} +25 -6
  11. package/dist/flags-yXzUm7Aq.js.map +1 -0
  12. package/dist/{flows-_KmnuUXd.js → flows-CDsfbaA2.js} +13 -6
  13. package/dist/flows-CDsfbaA2.js.map +1 -0
  14. package/dist/flows.d.ts +2 -8
  15. package/dist/flows.d.ts.map +1 -1
  16. package/dist/flows.js +1 -1
  17. package/dist/{ipc-BM335WFg.js → ipc-BruTG5Fb.js} +50 -19
  18. package/dist/ipc-BruTG5Fb.js.map +1 -0
  19. package/dist/{output-C4QhjpM6.js → output-DmHvT8vm.js} +141 -12
  20. package/dist/output-DmHvT8vm.js.map +1 -0
  21. package/dist/{perf-metrics-D0um6IR6.js → perf-metrics-C2pXfxvR.js} +12 -2
  22. package/dist/perf-metrics-C2pXfxvR.js.map +1 -0
  23. package/dist/{prompt-turn-CXMtXBl-.js → prompt-turn-BY5SwU1F.js} +256 -80
  24. package/dist/prompt-turn-BY5SwU1F.js.map +1 -0
  25. package/dist/{render-Br-kVPK_.js → render-yqwtaOX4.js} +35 -3
  26. package/dist/{render-Br-kVPK_.js.map → render-yqwtaOX4.js.map} +1 -1
  27. package/dist/runtime.d.ts +84 -10
  28. package/dist/runtime.d.ts.map +1 -1
  29. package/dist/runtime.js +425 -190
  30. package/dist/runtime.js.map +1 -1
  31. package/dist/{session-BtwAKtJ3.js → session-BwgaPK8-.js} +119 -81
  32. package/dist/session-BwgaPK8-.js.map +1 -0
  33. package/dist/session-options-pCbHn_n7.d.ts +13 -0
  34. package/dist/session-options-pCbHn_n7.d.ts.map +1 -0
  35. package/dist/{types-yxf-gcOE.d.ts → types-CVBeQyi3.d.ts} +9 -1
  36. package/dist/types-CVBeQyi3.d.ts.map +1 -0
  37. package/package.json +21 -21
  38. package/skills/acpx/SKILL.md +9 -4
  39. package/dist/client-D-4_aZf2.d.ts.map +0 -1
  40. package/dist/flags-ceSqz2T6.js.map +0 -1
  41. package/dist/flows-_KmnuUXd.js.map +0 -1
  42. package/dist/ipc-BM335WFg.js.map +0 -1
  43. package/dist/output-C4QhjpM6.js.map +0 -1
  44. package/dist/perf-metrics-D0um6IR6.js.map +0 -1
  45. package/dist/prompt-turn-CXMtXBl-.js.map +0 -1
  46. package/dist/session-BtwAKtJ3.js.map +0 -1
  47. package/dist/types-yxf-gcOE.d.ts.map +0 -1
package/dist/runtime.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as isAcpResourceNotFoundError, S as extractAcpError, g as textPrompt, x as normalizeOutputError } from "./perf-metrics-D0um6IR6.js";
2
- import { B as serializeSessionRecordForDisk, G as DEFAULT_AGENT_NAME, J as resolveAgentCommand, K as listBuiltInAgents, M as parseSessionRecord, P as defaultSessionEventLog, W as withTimeout, _ as recordSessionUpdate, a as applyConversation, f as cloneSessionAcpxState, g as recordPromptSubmission, h as recordClientOperation, i as connectAndLoadSession, l as setDesiredModeId, m as createSessionConversation, n as withConnectedSession, o as applyLifecycleSnapshotToRecord, p as cloneSessionConversation, s as reconcileAgentSessionId, t as runPromptTurn, v as trimConversationForRuntime, y as AcpClient, z as assertPersistedKeyPolicy } from "./prompt-turn-CXMtXBl-.js";
1
+ import { C as isAcpResourceNotFoundError, S as extractAcpError, g as textPrompt, x as normalizeOutputError } from "./perf-metrics-C2pXfxvR.js";
2
+ import { I as parseSessionRecord, J as withTimeout, Q as resolveAgentCommand, R as defaultSessionEventLog, S as AcpClient, U as assertPersistedKeyPolicy, W as serializeSessionRecordForDisk, X as listBuiltInAgents, Y as DEFAULT_AGENT_NAME, _ as createSessionConversation, a as connectAndLoadSession, b as recordSessionUpdate, c as reconcileAgentSessionId, d as setDesiredConfigOption, f as setDesiredModeId, g as cloneSessionConversation, h as cloneSessionAcpxState, n as withConnectedSession, o as applyConversation, s as applyLifecycleSnapshotToRecord, t as runPromptTurn, v as recordClientOperation, x as trimConversationForRuntime, y as recordPromptSubmission } from "./prompt-turn-BY5SwU1F.js";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs/promises";
5
5
  import { randomUUID } from "node:crypto";
@@ -31,9 +31,6 @@ function asString(value) {
31
31
  function asOptionalString(value) {
32
32
  return asTrimmedString(value) || void 0;
33
33
  }
34
- function asOptionalBoolean(value) {
35
- return typeof value === "boolean" ? value : void 0;
36
- }
37
34
  function deriveAgentFromSessionKey(sessionKey, fallbackAgent) {
38
35
  const match = sessionKey.match(/^agent:([^:]+):/i);
39
36
  return (match?.[1] ? asTrimmedString(match[1]) : "") || fallbackAgent;
@@ -133,13 +130,54 @@ function createTextDeltaEvent(params) {
133
130
  ...params.tag ? { tag: params.tag } : {}
134
131
  };
135
132
  }
133
+ function readFirstString(record, keys) {
134
+ for (const key of keys) {
135
+ const value = asOptionalString(record[key]);
136
+ if (value) return value;
137
+ }
138
+ }
139
+ function readFirstStringArray(record, keys) {
140
+ for (const key of keys) {
141
+ const value = record[key];
142
+ if (!Array.isArray(value)) continue;
143
+ const entries = value.map((entry) => asOptionalString(entry)).filter((entry) => entry !== void 0);
144
+ if (entries.length > 0) return entries;
145
+ }
146
+ }
147
+ function summarizeToolInput(rawInput) {
148
+ if (rawInput == null) return;
149
+ if (typeof rawInput === "string" || typeof rawInput === "number" || typeof rawInput === "boolean") return String(rawInput);
150
+ if (!isRecord(rawInput)) return;
151
+ const command = readFirstString(rawInput, [
152
+ "command",
153
+ "cmd",
154
+ "program"
155
+ ]);
156
+ const args = readFirstStringArray(rawInput, ["args", "arguments"]);
157
+ if (command) return [command, ...args ?? []].join(" ");
158
+ return readFirstString(rawInput, [
159
+ "path",
160
+ "file",
161
+ "filePath",
162
+ "filepath",
163
+ "target",
164
+ "uri",
165
+ "url",
166
+ "query",
167
+ "pattern",
168
+ "text",
169
+ "search"
170
+ ]);
171
+ }
136
172
  function createToolCallEvent(params) {
137
173
  const title = asTrimmedString(params.payload.title) || "tool call";
138
174
  const status = asTrimmedString(params.payload.status);
175
+ const inputSummary = summarizeToolInput(params.payload.rawInput);
139
176
  const toolCallId = asOptionalString(params.payload.toolCallId);
177
+ const summaryText = status ? `${title} (${status})` : title;
140
178
  return {
141
179
  type: "tool_call",
142
- text: status ? `${title} (${status})` : title,
180
+ text: inputSummary ? `${summaryText}: ${inputSummary}` : summaryText,
143
181
  tag: params.tag,
144
182
  ...toolCallId ? { toolCallId } : {},
145
183
  ...status ? { status } : {},
@@ -236,16 +274,8 @@ function parsePromptEventLine(line) {
236
274
  ...tag ? { tag } : {}
237
275
  };
238
276
  }
239
- case "done": return {
240
- type: "done",
241
- stopReason: asOptionalString(payload.stopReason)
242
- };
243
- case "error": return {
244
- type: "error",
245
- message: asTrimmedString(payload.message) || "acpx runtime error",
246
- code: asOptionalString(payload.code),
247
- retryable: asOptionalBoolean(payload.retryable)
248
- };
277
+ case "done":
278
+ case "error": return null;
249
279
  default: return null;
250
280
  }
251
281
  }
@@ -272,6 +302,12 @@ function createDeferred() {
272
302
  reject
273
303
  };
274
304
  }
305
+ function applyConfigOptionsToRecord(record, configOptions) {
306
+ if (!configOptions) return;
307
+ const acpxState = cloneSessionAcpxState(record.acpx) ?? {};
308
+ acpxState.config_options = structuredClone(configOptions);
309
+ record.acpx = acpxState;
310
+ }
275
311
  var AsyncEventQueue = class {
276
312
  items = [];
277
313
  waits = [];
@@ -290,6 +326,9 @@ var AsyncEventQueue = class {
290
326
  this.closed = true;
291
327
  for (const waiter of this.waits.splice(0)) waiter.resolve(null);
292
328
  }
329
+ clear() {
330
+ this.items.length = 0;
331
+ }
293
332
  async next() {
294
333
  if (this.items.length > 0) return this.items.shift() ?? null;
295
334
  if (this.closed) return null;
@@ -360,6 +399,18 @@ function createRecordId(sessionKey, mode) {
360
399
  function resumePolicyForSessionMode(mode) {
361
400
  return mode === "persistent" ? "same-session-only" : "allow-new";
362
401
  }
402
+ function legacyTerminalEventFromTurnResult(result) {
403
+ if (result.status === "failed") return {
404
+ type: "error",
405
+ message: result.error.message,
406
+ ...result.error.code ? { code: result.error.code } : {},
407
+ ...result.error.retryable === void 0 ? {} : { retryable: result.error.retryable }
408
+ };
409
+ return {
410
+ type: "done",
411
+ ...result.stopReason ? { stopReason: result.stopReason } : {}
412
+ };
413
+ }
363
414
  function statusSummary(record) {
364
415
  return [
365
416
  `session=${record.acpxRecordId}`,
@@ -372,6 +423,7 @@ function statusSummary(record) {
372
423
  var AcpRuntimeManager = class {
373
424
  activeControllers = /* @__PURE__ */ new Map();
374
425
  pendingPersistentClients = /* @__PURE__ */ new Map();
426
+ closingActiveRecords = /* @__PURE__ */ new Set();
375
427
  constructor(options, deps = {}) {
376
428
  this.options = options;
377
429
  this.deps = deps;
@@ -379,6 +431,79 @@ var AcpRuntimeManager = class {
379
431
  createClient(options) {
380
432
  return this.deps.clientFactory?.(options) ?? new AcpClient(options);
381
433
  }
434
+ async readPendingPersistentClient(record, options) {
435
+ const pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
436
+ if (!pendingClient) return;
437
+ if (!pendingClient.hasReusableSession(record.acpSessionId)) {
438
+ this.pendingPersistentClients.delete(record.acpxRecordId);
439
+ await pendingClient.close().catch(() => {});
440
+ return;
441
+ }
442
+ if (options.consume) this.pendingPersistentClients.delete(record.acpxRecordId);
443
+ return pendingClient;
444
+ }
445
+ async closePendingPersistentClient(recordId) {
446
+ const pendingClient = this.pendingPersistentClients.get(recordId);
447
+ if (!pendingClient) return;
448
+ this.pendingPersistentClients.delete(recordId);
449
+ await pendingClient.close().catch(() => {});
450
+ }
451
+ async refreshClosedState(record) {
452
+ if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
453
+ const latest = await this.options.sessionStore.load(record.acpxRecordId).catch(() => void 0);
454
+ record.closed = true;
455
+ record.closedAt = latest?.closedAt ?? record.closedAt ?? isoNow();
456
+ if (latest?.acpx) record.acpx = {
457
+ ...record.acpx,
458
+ ...latest.acpx
459
+ };
460
+ return true;
461
+ }
462
+ async retainPersistentClientAfterTurn(input) {
463
+ const { record, client } = input;
464
+ if (!!record.acpxRecordId.includes(":oneshot:") || record.closed || !client.hasReusableSession(record.acpSessionId)) return false;
465
+ const previousClient = this.pendingPersistentClients.get(record.acpxRecordId);
466
+ this.pendingPersistentClients.set(record.acpxRecordId, client);
467
+ if (previousClient && previousClient !== client) await previousClient.close().catch(() => {});
468
+ return true;
469
+ }
470
+ async withRuntimeControlSession(record, sessionMode, run) {
471
+ const pendingClient = await this.readPendingPersistentClient(record, { consume: false });
472
+ if (pendingClient) {
473
+ const value = await run({
474
+ client: pendingClient,
475
+ sessionId: record.acpSessionId,
476
+ record
477
+ });
478
+ record.lastUsedAt = isoNow();
479
+ record.closed = false;
480
+ record.closedAt = void 0;
481
+ record.protocolVersion = pendingClient.initializeResult?.protocolVersion;
482
+ record.agentCapabilities = pendingClient.initializeResult?.agentCapabilities;
483
+ applyLifecycleSnapshotToRecord(record, pendingClient.getAgentLifecycleSnapshot());
484
+ return {
485
+ value,
486
+ record
487
+ };
488
+ }
489
+ const result = await withConnectedSession({
490
+ sessionRecordId: record.acpxRecordId,
491
+ loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
492
+ saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
493
+ createClient: (options) => this.createClient(options),
494
+ mcpServers: [...this.options.mcpServers ?? []],
495
+ permissionMode: this.options.permissionMode,
496
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
497
+ verbose: this.options.verbose,
498
+ timeoutMs: this.options.timeoutMs,
499
+ resumePolicy: resumePolicyForSessionMode(sessionMode),
500
+ run
501
+ });
502
+ return {
503
+ value: result.value,
504
+ record: result.record
505
+ };
506
+ }
382
507
  async ensureSession(input) {
383
508
  const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
384
509
  const agentCommand = this.options.agentRegistry.resolve(input.agent);
@@ -390,6 +515,7 @@ var AcpRuntimeManager = class {
390
515
  })) {
391
516
  existing.closed = false;
392
517
  existing.closedAt = void 0;
518
+ this.closingActiveRecords.delete(existing.acpxRecordId);
393
519
  await this.options.sessionStore.save(existing);
394
520
  return existing;
395
521
  }
@@ -423,6 +549,7 @@ var AcpRuntimeManager = class {
423
549
  cwd,
424
550
  agentSessionId
425
551
  });
552
+ this.closingActiveRecords.delete(record.acpxRecordId);
426
553
  record.protocolVersion = client.initializeResult?.protocolVersion;
427
554
  record.agentCapabilities = client.initializeResult?.agentCapabilities;
428
555
  applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
@@ -438,86 +565,143 @@ var AcpRuntimeManager = class {
438
565
  if (!keepClientOpen) await client.close();
439
566
  }
440
567
  }
441
- async *runTurn(input) {
442
- const record = await this.requireRecord(input.handle.acpxRecordId ?? input.handle.sessionKey);
443
- const conversation = cloneSessionConversation(record);
444
- let acpxState = cloneSessionAcpxState(record.acpx);
568
+ startTurn(input) {
445
569
  const promptInput = toPromptInput(input.text, input.attachments);
446
- const promptMessageId = recordPromptSubmission(conversation, promptInput, isoNow());
447
- trimConversationForRuntime(conversation);
448
570
  const queue = new AsyncEventQueue();
449
- let pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
450
- if (pendingClient) {
451
- this.pendingPersistentClients.delete(record.acpxRecordId);
452
- if (!pendingClient.hasReusableSession(record.acpSessionId)) {
453
- await pendingClient.close().catch(() => {});
454
- pendingClient = void 0;
455
- }
456
- }
457
- const client = pendingClient ?? this.createClient({
458
- agentCommand: record.agentCommand,
459
- cwd: record.cwd,
460
- mcpServers: [...this.options.mcpServers ?? []],
461
- permissionMode: this.options.permissionMode,
462
- nonInteractivePermissions: this.options.nonInteractivePermissions,
463
- verbose: this.options.verbose
464
- });
465
- let activeSessionId = record.acpSessionId;
466
- let sawDone = false;
467
- let pendingCancel = false;
468
- let turnActive = true;
571
+ const result = createDeferred();
469
572
  const sessionReady = createDeferred();
470
573
  sessionReady.promise.catch(() => {});
471
- const applyPendingCancel = async () => {
472
- if (!pendingCancel || !client.hasActivePrompt()) return false;
473
- const cancelled = await client.requestCancelActivePrompt();
474
- if (cancelled) pendingCancel = false;
475
- return cancelled;
574
+ let resultSettled = false;
575
+ let pendingCancel = false;
576
+ let turnActive = true;
577
+ let streamClosed = false;
578
+ let activeController = null;
579
+ const settleResult = (next) => {
580
+ if (resultSettled) return;
581
+ resultSettled = true;
582
+ result.resolve(next);
476
583
  };
477
- const activeController = {
478
- hasActivePrompt: () => client.hasActivePrompt(),
479
- requestCancelActivePrompt: async () => {
480
- if (client.hasActivePrompt()) return await client.requestCancelActivePrompt();
481
- if (!turnActive) return false;
482
- pendingCancel = true;
483
- return true;
484
- },
485
- setSessionMode: async (modeId) => {
486
- if (!client.hasActivePrompt()) await sessionReady.promise;
487
- await client.setSessionMode(activeSessionId, modeId);
488
- },
489
- setSessionModel: async (modelId) => {
490
- if (!client.hasActivePrompt()) await sessionReady.promise;
491
- await client.setSessionModel(activeSessionId, modelId);
492
- },
493
- setSessionConfigOption: async (configId, value) => {
494
- if (!client.hasActivePrompt()) await sessionReady.promise;
495
- return await client.setSessionConfigOption(activeSessionId, configId, value);
496
- }
584
+ const closeStream = () => {
585
+ if (streamClosed) return;
586
+ streamClosed = true;
587
+ queue.clear();
588
+ queue.close();
497
589
  };
498
- const emitParsed = (payload) => {
499
- const parsed = parsePromptEventLine(JSON.stringify(payload));
500
- if (!parsed) return;
501
- if (parsed.type === "done") sawDone = true;
502
- queue.push(parsed);
590
+ const requestCancel = async () => {
591
+ if (activeController) return await activeController.requestCancelActivePrompt();
592
+ if (!turnActive) return false;
593
+ pendingCancel = true;
594
+ return true;
503
595
  };
504
596
  const abortHandler = () => {
505
- activeController.requestCancelActivePrompt();
597
+ requestCancel();
506
598
  };
507
599
  if (input.signal) {
508
600
  if (input.signal.aborted) {
509
- queue.close();
510
- return;
601
+ closeStream();
602
+ settleResult({
603
+ status: "cancelled",
604
+ stopReason: "cancelled"
605
+ });
606
+ return {
607
+ requestId: input.requestId,
608
+ events: queue.iterate(),
609
+ result: result.promise,
610
+ cancel: async () => {},
611
+ closeStream: async () => {}
612
+ };
511
613
  }
512
614
  input.signal.addEventListener("abort", abortHandler, { once: true });
513
615
  }
514
- this.activeControllers.set(record.acpxRecordId, activeController);
515
616
  (async () => {
617
+ let record = null;
618
+ let conversation = null;
619
+ let acpxState;
620
+ let client = null;
516
621
  try {
517
- client.setEventHandlers({
622
+ record = await this.requireRecord(input.handle.acpxRecordId ?? input.handle.sessionKey);
623
+ conversation = cloneSessionConversation(record);
624
+ acpxState = cloneSessionAcpxState(record.acpx);
625
+ const promptStartedAt = isoNow();
626
+ const promptMessageId = recordPromptSubmission(conversation, promptInput, promptStartedAt);
627
+ trimConversationForRuntime(conversation);
628
+ record.lastPromptAt = promptStartedAt;
629
+ record.lastUsedAt = promptStartedAt;
630
+ record.acpx = acpxState;
631
+ applyConversation(record, conversation);
632
+ await this.options.sessionStore.save(record);
633
+ const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
634
+ client = pendingClient ?? this.createClient({
635
+ agentCommand: record.agentCommand,
636
+ cwd: record.cwd,
637
+ mcpServers: [...this.options.mcpServers ?? []],
638
+ permissionMode: this.options.permissionMode,
639
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
640
+ verbose: this.options.verbose
641
+ });
642
+ const runtimeClient = client;
643
+ const runtimeConversation = conversation;
644
+ const runtimeRecord = record;
645
+ let activeSessionId = record.acpSessionId;
646
+ const applyPendingCancel = async () => {
647
+ if (!pendingCancel || !runtimeClient.hasActivePrompt()) return false;
648
+ const cancelled = await runtimeClient.requestCancelActivePrompt();
649
+ if (cancelled) pendingCancel = false;
650
+ return cancelled;
651
+ };
652
+ activeController = {
653
+ hasActivePrompt: () => runtimeClient.hasActivePrompt(),
654
+ requestCancelActivePrompt: async () => {
655
+ if (runtimeClient.hasActivePrompt()) return await runtimeClient.requestCancelActivePrompt();
656
+ if (!turnActive) return false;
657
+ pendingCancel = true;
658
+ return true;
659
+ },
660
+ setSessionMode: async (modeId) => {
661
+ if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
662
+ await runtimeClient.setSessionMode(activeSessionId, modeId);
663
+ const nextState = cloneSessionAcpxState(acpxState) ?? {};
664
+ nextState.desired_mode_id = modeId;
665
+ acpxState = nextState;
666
+ },
667
+ setSessionModel: async (modelId) => {
668
+ if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
669
+ await runtimeClient.setSessionModel(activeSessionId, modelId);
670
+ },
671
+ setSessionConfigOption: async (configId, value) => {
672
+ if (!runtimeClient.hasActivePrompt()) await sessionReady.promise;
673
+ const response = await runtimeClient.setSessionConfigOption(activeSessionId, configId, value);
674
+ if (response?.configOptions) {
675
+ const nextState = cloneSessionAcpxState(acpxState) ?? {};
676
+ nextState.config_options = structuredClone(response.configOptions);
677
+ acpxState = nextState;
678
+ }
679
+ if (configId === "mode") {
680
+ const nextState = cloneSessionAcpxState(acpxState) ?? {};
681
+ nextState.desired_mode_id = value;
682
+ acpxState = nextState;
683
+ } else if (configId !== "model") {
684
+ const nextState = cloneSessionAcpxState(acpxState) ?? {};
685
+ nextState.desired_config_options = {
686
+ ...nextState.desired_config_options,
687
+ [configId]: value
688
+ };
689
+ acpxState = nextState;
690
+ }
691
+ return response;
692
+ }
693
+ };
694
+ const emitParsed = (payload) => {
695
+ if (streamClosed) return;
696
+ const parsed = parsePromptEventLine(JSON.stringify(payload));
697
+ if (!parsed) return;
698
+ queue.push(parsed);
699
+ };
700
+ this.activeControllers.set(runtimeRecord.acpxRecordId, activeController);
701
+ runtimeClient.setEventHandlers({
518
702
  onSessionUpdate: (notification) => {
519
- acpxState = recordSessionUpdate(conversation, acpxState, notification);
520
- trimConversationForRuntime(conversation);
703
+ acpxState = recordSessionUpdate(runtimeConversation, acpxState, notification);
704
+ trimConversationForRuntime(runtimeConversation);
521
705
  emitParsed({
522
706
  jsonrpc: "2.0",
523
707
  method: "session/update",
@@ -525,8 +709,8 @@ var AcpRuntimeManager = class {
525
709
  });
526
710
  },
527
711
  onClientOperation: (operation) => {
528
- acpxState = recordClientOperation(conversation, acpxState, operation);
529
- trimConversationForRuntime(conversation);
712
+ acpxState = recordClientOperation(runtimeConversation, acpxState, operation);
713
+ trimConversationForRuntime(runtimeConversation);
530
714
  emitParsed({
531
715
  type: "client_operation",
532
716
  ...operation
@@ -538,13 +722,14 @@ var AcpRuntimeManager = class {
538
722
  resumed: false,
539
723
  loadError: void 0
540
724
  } : await connectAndLoadSession({
541
- client,
542
- record,
725
+ client: runtimeClient,
726
+ record: runtimeRecord,
543
727
  resumePolicy: resumePolicyForSessionMode(input.sessionMode),
544
728
  timeoutMs: this.options.timeoutMs,
545
729
  activeController,
546
730
  onClientAvailable: (controller) => {
547
- this.activeControllers.set(record.acpxRecordId, controller);
731
+ activeController = controller;
732
+ this.activeControllers.set(runtimeRecord.acpxRecordId, controller);
548
733
  },
549
734
  onConnectedRecord: (connectedRecord) => {
550
735
  connectedRecord.lastPromptAt = isoNow();
@@ -554,68 +739,96 @@ var AcpRuntimeManager = class {
554
739
  }
555
740
  });
556
741
  sessionReady.resolve();
557
- record.lastRequestId = input.requestId;
558
- record.lastPromptAt = isoNow();
559
- record.closed = false;
560
- record.closedAt = void 0;
561
- record.lastUsedAt = isoNow();
742
+ runtimeRecord.lastRequestId = input.requestId;
743
+ runtimeRecord.lastPromptAt = isoNow();
744
+ runtimeRecord.closed = false;
745
+ runtimeRecord.closedAt = void 0;
746
+ runtimeRecord.lastUsedAt = isoNow();
562
747
  if (resumed || loadError) emitParsed({
563
748
  type: "status",
564
749
  text: loadError ? `load fallback: ${loadError}` : "session resumed"
565
750
  });
566
751
  if (pendingCancel || input.signal?.aborted) {
567
752
  pendingCancel = false;
568
- if (!sawDone) queue.push({
569
- type: "done",
753
+ settleResult({
754
+ status: "cancelled",
570
755
  stopReason: "cancelled"
571
756
  });
572
757
  return;
573
758
  }
574
759
  await applyPendingCancel();
575
760
  const response = await runPromptTurn({
576
- client,
761
+ client: runtimeClient,
577
762
  sessionId,
578
763
  prompt: promptInput,
579
764
  timeoutMs: input.timeoutMs ?? this.options.timeoutMs,
580
- conversation,
765
+ conversation: runtimeConversation,
581
766
  promptMessageId
582
767
  });
583
- record.acpSessionId = activeSessionId;
584
- reconcileAgentSessionId(record, record.agentSessionId);
585
- record.protocolVersion = client.initializeResult?.protocolVersion;
586
- record.agentCapabilities = client.initializeResult?.agentCapabilities;
587
- record.acpx = acpxState;
588
- applyConversation(record, conversation);
589
- applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
590
- await this.options.sessionStore.save(record);
591
- if (!sawDone) queue.push({
592
- type: "done",
593
- stopReason: response.stopReason
768
+ runtimeRecord.acpSessionId = activeSessionId;
769
+ reconcileAgentSessionId(runtimeRecord, runtimeRecord.agentSessionId);
770
+ runtimeRecord.protocolVersion = runtimeClient.initializeResult?.protocolVersion;
771
+ runtimeRecord.agentCapabilities = runtimeClient.initializeResult?.agentCapabilities;
772
+ runtimeRecord.acpx = acpxState;
773
+ applyConversation(runtimeRecord, runtimeConversation);
774
+ applyLifecycleSnapshotToRecord(runtimeRecord, runtimeClient.getAgentLifecycleSnapshot());
775
+ await this.options.sessionStore.save(runtimeRecord);
776
+ settleResult({
777
+ status: response.stopReason === "cancelled" ? "cancelled" : "completed",
778
+ ...response.stopReason ? { stopReason: response.stopReason } : {}
594
779
  });
595
780
  } catch (error) {
596
781
  sessionReady.reject(error);
597
782
  const normalized = normalizeOutputError(error, { origin: "runtime" });
598
- queue.push({
599
- type: "error",
600
- message: normalized.message,
601
- code: normalized.code,
602
- retryable: normalized.retryable
783
+ settleResult({
784
+ status: "failed",
785
+ error: {
786
+ message: normalized.message,
787
+ ...normalized.code ? { code: normalized.code } : {},
788
+ ...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
789
+ }
603
790
  });
604
791
  } finally {
605
792
  turnActive = false;
606
793
  if (input.signal) input.signal.removeEventListener("abort", abortHandler);
607
- this.activeControllers.delete(record.acpxRecordId);
608
- client.clearEventHandlers();
609
- applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
610
- record.acpx = acpxState;
611
- applyConversation(record, conversation);
612
- record.lastUsedAt = isoNow();
613
- await this.options.sessionStore.save(record).catch(() => {});
614
- await client.close().catch(() => {});
794
+ client?.clearEventHandlers();
795
+ let pooled = false;
796
+ if (record && conversation) {
797
+ applyLifecycleSnapshotToRecord(record, client?.getAgentLifecycleSnapshot() ?? { running: false });
798
+ record.acpx = acpxState;
799
+ applyConversation(record, conversation);
800
+ record.lastUsedAt = isoNow();
801
+ const closed = await this.refreshClosedState(record);
802
+ await this.options.sessionStore.save(record).catch(() => {});
803
+ if (!closed && client) pooled = await this.retainPersistentClientAfterTurn({
804
+ record,
805
+ client
806
+ });
807
+ }
808
+ if (!pooled) await client?.close().catch(() => {});
809
+ if (record) {
810
+ this.activeControllers.delete(record.acpxRecordId);
811
+ this.closingActiveRecords.delete(record.acpxRecordId);
812
+ }
615
813
  queue.close();
616
814
  }
617
815
  })();
618
- yield* queue.iterate();
816
+ return {
817
+ requestId: input.requestId,
818
+ events: queue.iterate(),
819
+ result: result.promise,
820
+ cancel: async () => {
821
+ await requestCancel();
822
+ },
823
+ closeStream: async () => {
824
+ closeStream();
825
+ }
826
+ };
827
+ }
828
+ async *runTurn(input) {
829
+ const turn = this.startTurn(input);
830
+ yield* turn.events;
831
+ yield legacyTerminalEventFromTurnResult(await turn.result);
619
832
  }
620
833
  async getStatus(handle) {
621
834
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
@@ -627,7 +840,8 @@ var AcpRuntimeManager = class {
627
840
  details: {
628
841
  cwd: record.cwd,
629
842
  lastUsedAt: record.lastUsedAt,
630
- closed: record.closed === true
843
+ closed: record.closed === true,
844
+ ...record.acpx?.config_options !== void 0 ? { configOptions: structuredClone(record.acpx.config_options) } : {}
631
845
  }
632
846
  };
633
847
  }
@@ -636,20 +850,8 @@ var AcpRuntimeManager = class {
636
850
  const controller = this.activeControllers.get(record.acpxRecordId);
637
851
  let targetRecord = record;
638
852
  if (controller) await controller.setSessionMode(mode);
639
- else targetRecord = (await withConnectedSession({
640
- sessionRecordId: record.acpxRecordId,
641
- loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
642
- saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
643
- createClient: (options) => this.createClient(options),
644
- mcpServers: [...this.options.mcpServers ?? []],
645
- permissionMode: this.options.permissionMode,
646
- nonInteractivePermissions: this.options.nonInteractivePermissions,
647
- verbose: this.options.verbose,
648
- timeoutMs: this.options.timeoutMs,
649
- resumePolicy: resumePolicyForSessionMode(sessionMode),
650
- run: async ({ client, sessionId }) => {
651
- await client.setSessionMode(sessionId, mode);
652
- }
853
+ else targetRecord = (await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId }) => {
854
+ await client.setSessionMode(sessionId, mode);
653
855
  })).record;
654
856
  setDesiredModeId(targetRecord, mode);
655
857
  await this.options.sessionStore.save(targetRecord);
@@ -658,24 +860,16 @@ var AcpRuntimeManager = class {
658
860
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
659
861
  const controller = this.activeControllers.get(record.acpxRecordId);
660
862
  let targetRecord = record;
661
- if (controller) await controller.setSessionConfigOption(key, value);
662
- else targetRecord = (await withConnectedSession({
663
- sessionRecordId: record.acpxRecordId,
664
- loadRecord: async (sessionRecordId) => await this.requireRecord(sessionRecordId),
665
- saveRecord: async (connectedRecord) => await this.options.sessionStore.save(connectedRecord),
666
- createClient: (options) => this.createClient(options),
667
- mcpServers: [...this.options.mcpServers ?? []],
668
- permissionMode: this.options.permissionMode,
669
- nonInteractivePermissions: this.options.nonInteractivePermissions,
670
- verbose: this.options.verbose,
671
- timeoutMs: this.options.timeoutMs,
672
- resumePolicy: resumePolicyForSessionMode(sessionMode),
673
- run: async ({ client, sessionId, record: connectedRecord }) => {
674
- await client.setSessionConfigOption(sessionId, key, value);
675
- if (key === "mode") setDesiredModeId(connectedRecord, value);
676
- }
863
+ if (controller) {
864
+ const response = await controller.setSessionConfigOption(key, value);
865
+ applyConfigOptionsToRecord(targetRecord, response?.configOptions);
866
+ } else targetRecord = (await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
867
+ applyConfigOptionsToRecord(connectedRecord, (await client.setSessionConfigOption(sessionId, key, value))?.configOptions);
868
+ if (key === "mode") setDesiredModeId(connectedRecord, value);
869
+ else setDesiredConfigOption(connectedRecord, key, value);
677
870
  })).record;
678
871
  if (key === "mode") setDesiredModeId(targetRecord, value);
872
+ else setDesiredConfigOption(targetRecord, key, value);
679
873
  await this.options.sessionStore.save(targetRecord);
680
874
  }
681
875
  async cancel(handle) {
@@ -683,6 +877,7 @@ var AcpRuntimeManager = class {
683
877
  }
684
878
  async close(handle, options = {}) {
685
879
  const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
880
+ if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
686
881
  await this.cancel(handle);
687
882
  if (options.discardPersistentState) {
688
883
  await this.closeBackendSession(record);
@@ -690,17 +885,14 @@ var AcpRuntimeManager = class {
690
885
  ...record.acpx,
691
886
  reset_on_next_ensure: true
692
887
  };
693
- }
888
+ } else await this.closePendingPersistentClient(record.acpxRecordId);
694
889
  record.closed = true;
695
890
  record.closedAt = isoNow();
696
891
  await this.options.sessionStore.save(record);
697
892
  }
698
893
  async closeBackendSession(record) {
699
- const pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
700
- if (pendingClient) this.pendingPersistentClients.delete(record.acpxRecordId);
701
- const reusablePendingClient = pendingClient?.hasReusableSession(record.acpSessionId) === true ? pendingClient : void 0;
702
- if (pendingClient && !reusablePendingClient) await pendingClient.close().catch(() => {});
703
- const client = reusablePendingClient ?? this.createClient({
894
+ const pendingClient = await this.readPendingPersistentClient(record, { consume: true });
895
+ const client = pendingClient ?? this.createClient({
704
896
  agentCommand: record.agentCommand,
705
897
  cwd: record.cwd,
706
898
  mcpServers: [...this.options.mcpServers ?? []],
@@ -709,7 +901,7 @@ var AcpRuntimeManager = class {
709
901
  verbose: this.options.verbose
710
902
  });
711
903
  try {
712
- if (!reusablePendingClient) await withTimeout(client.start(), this.options.timeoutMs);
904
+ if (!pendingClient) await withTimeout(client.start(), this.options.timeoutMs);
713
905
  if (!client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
714
906
  await withTimeout(client.closeSession(record.acpSessionId), this.options.timeoutMs);
715
907
  } catch (error) {
@@ -807,6 +999,28 @@ function writeHandleState(handle, state) {
807
999
  }
808
1000
  //#endregion
809
1001
  //#region src/runtime/public/probe.ts
1002
+ function formatRuntimeDetail(value) {
1003
+ if (value instanceof Error) return value.message || value.name;
1004
+ if (typeof value === "string") return value;
1005
+ if (value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") return String(value);
1006
+ if (typeof value === "function") return value.name ? `[Function ${value.name}]` : "[Function]";
1007
+ const seen = /* @__PURE__ */ new WeakSet();
1008
+ try {
1009
+ return JSON.stringify(value, (_key, nested) => {
1010
+ if (nested instanceof Error) return nested.message || nested.name;
1011
+ if (nested && typeof nested === "object") {
1012
+ if (seen.has(nested)) return "[Circular]";
1013
+ seen.add(nested);
1014
+ }
1015
+ return nested;
1016
+ }) ?? "undefined";
1017
+ } catch {
1018
+ return "unserializable object";
1019
+ }
1020
+ }
1021
+ function normalizeRuntimeDetails(details) {
1022
+ return details?.map((detail) => formatRuntimeDetail(detail));
1023
+ }
810
1024
  async function probeRuntime(options, deps = {}) {
811
1025
  const agentName = options.probeAgent?.trim() || "codex";
812
1026
  const agentCommand = options.agentRegistry.resolve(agentName);
@@ -845,7 +1059,7 @@ async function probeRuntime(options, deps = {}) {
845
1059
  `agent=${agentName}`,
846
1060
  `command=${agentCommand}`,
847
1061
  `cwd=${options.cwd}`,
848
- error instanceof Error ? error.message : String(error)
1062
+ formatRuntimeDetail(error)
849
1063
  ]
850
1064
  };
851
1065
  } finally {
@@ -882,7 +1096,8 @@ var AcpxRuntime = class {
882
1096
  return this.healthy;
883
1097
  }
884
1098
  async probeAvailability() {
885
- this.healthy = (await this.runProbe()).ok;
1099
+ const report = await this.runProbe();
1100
+ this.healthy = report.ok;
886
1101
  }
887
1102
  async doctor() {
888
1103
  const report = await this.runProbe();
@@ -891,7 +1106,7 @@ var AcpxRuntime = class {
891
1106
  ok: report.ok,
892
1107
  code: report.ok ? void 0 : "ACP_BACKEND_UNAVAILABLE",
893
1108
  message: report.message,
894
- details: report.details
1109
+ details: normalizeRuntimeDetails(report.details)
895
1110
  };
896
1111
  }
897
1112
  async ensureSession(input) {
@@ -926,13 +1141,38 @@ var AcpxRuntime = class {
926
1141
  });
927
1142
  return handle;
928
1143
  }
1144
+ startTurn(input) {
1145
+ const { handle, state } = this.resolveManagerHandle(input.handle);
1146
+ const turnPromise = this.getManager().then((manager) => manager.startTurn({
1147
+ handle,
1148
+ text: input.text,
1149
+ attachments: input.attachments,
1150
+ mode: input.mode,
1151
+ sessionMode: state.mode,
1152
+ requestId: input.requestId,
1153
+ timeoutMs: input.timeoutMs,
1154
+ signal: input.signal
1155
+ }));
1156
+ return {
1157
+ requestId: input.requestId,
1158
+ events: { async *[Symbol.asyncIterator]() {
1159
+ yield* (await turnPromise).events;
1160
+ } },
1161
+ get result() {
1162
+ return turnPromise.then((turn) => turn.result);
1163
+ },
1164
+ cancel(inputArgs) {
1165
+ return turnPromise.then((turn) => turn.cancel(inputArgs));
1166
+ },
1167
+ closeStream(inputArgs) {
1168
+ return turnPromise.then((turn) => turn.closeStream(inputArgs));
1169
+ }
1170
+ };
1171
+ }
929
1172
  async *runTurn(input) {
930
- const state = this.resolveHandleState(input.handle);
1173
+ const { handle, state } = this.resolveManagerHandle(input.handle);
931
1174
  yield* (await this.getManager()).runTurn({
932
- handle: {
933
- ...input.handle,
934
- acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
935
- },
1175
+ handle,
936
1176
  text: input.text,
937
1177
  attachments: input.attachments,
938
1178
  mode: input.mode,
@@ -942,43 +1182,28 @@ var AcpxRuntime = class {
942
1182
  signal: input.signal
943
1183
  });
944
1184
  }
945
- getCapabilities() {
1185
+ getCapabilities(_input) {
946
1186
  return ACPX_CAPABILITIES;
947
1187
  }
948
1188
  async getStatus(input) {
949
- const state = this.resolveHandleState(input.handle);
950
- return await (await this.getManager()).getStatus({
951
- ...input.handle,
952
- acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
953
- });
1189
+ const { handle } = this.resolveManagerHandle(input.handle);
1190
+ return await (await this.getManager()).getStatus(handle);
954
1191
  }
955
1192
  async setMode(input) {
956
- const state = this.resolveHandleState(input.handle);
957
- await (await this.getManager()).setMode({
958
- ...input.handle,
959
- acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
960
- }, input.mode, state.mode);
1193
+ const { handle, state } = this.resolveManagerHandle(input.handle);
1194
+ await (await this.getManager()).setMode(handle, input.mode, state.mode);
961
1195
  }
962
1196
  async setConfigOption(input) {
963
- const state = this.resolveHandleState(input.handle);
964
- await (await this.getManager()).setConfigOption({
965
- ...input.handle,
966
- acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
967
- }, input.key, input.value, state.mode);
1197
+ const { handle, state } = this.resolveManagerHandle(input.handle);
1198
+ await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode);
968
1199
  }
969
1200
  async cancel(input) {
970
- const state = this.resolveHandleState(input.handle);
971
- await (await this.getManager()).cancel({
972
- ...input.handle,
973
- acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
974
- });
1201
+ const { handle } = this.resolveManagerHandle(input.handle);
1202
+ await (await this.getManager()).cancel(handle);
975
1203
  }
976
1204
  async close(input) {
977
- const state = this.resolveHandleState(input.handle);
978
- await (await this.getManager()).close({
979
- ...input.handle,
980
- acpxRecordId: state.acpxRecordId ?? input.handle.acpxRecordId ?? input.handle.sessionKey
981
- }, { discardPersistentState: input.discardPersistentState });
1205
+ const { handle } = this.resolveManagerHandle(input.handle);
1206
+ await (await this.getManager()).close(handle, { discardPersistentState: input.discardPersistentState });
982
1207
  }
983
1208
  async getManager() {
984
1209
  if (this.manager) return this.manager;
@@ -991,6 +1216,16 @@ var AcpxRuntime = class {
991
1216
  async runProbe() {
992
1217
  return await (this.testOptions?.probeRunner?.(this.options) ?? probeRuntime(this.options));
993
1218
  }
1219
+ resolveManagerHandle(handle) {
1220
+ const state = this.resolveHandleState(handle);
1221
+ return {
1222
+ handle: {
1223
+ ...handle,
1224
+ acpxRecordId: state.acpxRecordId ?? handle.acpxRecordId ?? handle.sessionKey
1225
+ },
1226
+ state
1227
+ };
1228
+ }
994
1229
  resolveHandleState(handle) {
995
1230
  const decoded = decodeAcpxRuntimeHandleState(handle.runtimeSessionName);
996
1231
  if (decoded) return {