@axhub/genie 0.2.11 → 0.2.12

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 (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-Clb2COtW.js +274 -0
  3. package/dist/assets/ImagePlaygroundPage-DqhMSbM8.js +106 -0
  4. package/dist/assets/ImagePlaygroundPage-MEn3NN80.css +1 -0
  5. package/dist/assets/ReviewApp-CDcLYe-u.js +1 -0
  6. package/dist/assets/{_basePickBy-BDnj7-0Z.js → _basePickBy-jUZsM51q.js} +1 -1
  7. package/dist/assets/{_baseUniq-Bl0JKOyl.js → _baseUniq-BXglE6_v.js} +1 -1
  8. package/dist/assets/{arc-DY-4Kev3.js → arc-D-oFCFBv.js} +1 -1
  9. package/dist/assets/{architectureDiagram-2XIMDMQ5-qw7crNVd.js → architectureDiagram-2XIMDMQ5-DC8bAnQt.js} +1 -1
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-B9xg7ep3.js → blockDiagram-WCTKOSBZ-C4semIRc.js} +1 -1
  11. package/dist/assets/{c4Diagram-IC4MRINW-H9xp3ytb.js → c4Diagram-IC4MRINW-FHj1QO3y.js} +1 -1
  12. package/dist/assets/channel-BF4woPXX.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-B3EVDUxI.js → chunk-4BX2VUAB-D-LjsQ_s.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-CGv945ef.js → chunk-55IACEB6-DI3j_d7A.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-uAT4CKWM.js → chunk-FMBD7UC4-BEVnaLFN.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-Cbvlpkf7.js → chunk-JSJVCQXG-CSxpcErk.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-CcqIuGat.js → chunk-KX2RTZJC-BbuhDN4h.js} +1 -1
  18. package/dist/assets/{chunk-NQ4KR5QH-CgrcsRuX.js → chunk-NQ4KR5QH-C3x61XQa.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-Cx0APOoV.js → chunk-QZHKN3VN-DxWOFtPh.js} +1 -1
  20. package/dist/assets/{chunk-WL4C6EOR-BbZirvBk.js → chunk-WL4C6EOR-Bt2OauD2.js} +1 -1
  21. package/dist/assets/classDiagram-VBA2DB6C-D2kHlnQ7.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-D2kHlnQ7.js +1 -0
  23. package/dist/assets/clone-CqBvwCJW.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-CrvmGFLD.js → cose-bilkent-S5V4N54A-Dexadrue.js} +1 -1
  25. package/dist/assets/{dagre-KLK3FWXG-C-W6VPjS.js → dagre-KLK3FWXG-F9U4X2xC.js} +1 -1
  26. package/dist/assets/{diagram-E7M64L7V-IP2q3bL0.js → diagram-E7M64L7V-B3V17aH3.js} +1 -1
  27. package/dist/assets/{diagram-IFDJBPK2-CQaL-XyV.js → diagram-IFDJBPK2-CdHAmLL1.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-BxBLThfv.js → diagram-P4PSJMXO-CrTNfk8K.js} +1 -1
  29. package/dist/assets/{erDiagram-INFDFZHY-Dyl7bJTt.js → erDiagram-INFDFZHY-vDh9SWK9.js} +1 -1
  30. package/dist/assets/{flowDiagram-PKNHOUZH-B7NFMgFK.js → flowDiagram-PKNHOUZH-DpltMg7L.js} +1 -1
  31. package/dist/assets/{ganttDiagram-A5KZAMGK-hReWSDu2.js → ganttDiagram-A5KZAMGK-COTk2xur.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js → gitGraphDiagram-K3NZZRJ6-BNV7bvvj.js} +1 -1
  33. package/dist/assets/{graph-DNDiJhTn.js → graph-Dkeg9oys.js} +1 -1
  34. package/dist/assets/{highlighted-body-TPN3WLV5-DclLmTou.js → highlighted-body-TPN3WLV5-DaiQEBwR.js} +1 -1
  35. package/dist/assets/index-DgGmiqsP.css +1 -0
  36. package/dist/assets/index-DvA901Vs.js +2 -0
  37. package/dist/assets/{infoDiagram-LFFYTUFH-CqQOOzDA.js → infoDiagram-LFFYTUFH-CZioW3Gt.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-PHBUUO56-CZ0iLiHg.js → ishikawaDiagram-PHBUUO56-BbqR3i1B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-4ABVD52K-DdfYKfNh.js → journeyDiagram-4ABVD52K-wfb-WHzl.js} +1 -1
  40. package/dist/assets/{kanban-definition-K7BYSVSG-C5Vf32u6.js → kanban-definition-K7BYSVSG-B3c4y3VN.js} +1 -1
  41. package/dist/assets/{layout-rvTEu2KS.js → layout-Xr9Z2VGF.js} +1 -1
  42. package/dist/assets/{linear-CD9SiYze.js → linear-JBmzAJtl.js} +1 -1
  43. package/dist/assets/{mermaid-O7DHMXV3-OZ8qWWwa.js → mermaid-O7DHMXV3-fDuyNLKe.js} +230 -222
  44. package/dist/assets/{mindmap-definition-YRQLILUH-CQxrLNVc.js → mindmap-definition-YRQLILUH-B5NTN_jD.js} +1 -1
  45. package/dist/assets/{pieDiagram-SKSYHLDU-XgAUByWg.js → pieDiagram-SKSYHLDU-CuO98GVu.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-337W2JSQ-CH16ls7G.js → quadrantDiagram-337W2JSQ-LL3f4vLf.js} +1 -1
  47. package/dist/assets/{requirementDiagram-Z7DCOOCP-B_kQO06L.js → requirementDiagram-Z7DCOOCP-Di-2O6LH.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-WA2Y5GQK-ofe78CyS.js → sankeyDiagram-WA2Y5GQK-9lHqrXqR.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-2WXFIKYE-Ckbxwny6.js → sequenceDiagram-2WXFIKYE-BQu-SoGr.js} +1 -1
  50. package/dist/assets/{stateDiagram-RAJIS63D-DNtzCk14.js → stateDiagram-RAJIS63D-BUxvd2BC.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-FVOUBMTO-CDVexTiR.js +1 -0
  52. package/dist/assets/{timeline-definition-YZTLITO2-zT6CklKt.js → timeline-definition-YZTLITO2-oP47UEU6.js} +1 -1
  53. package/dist/assets/{treemap-KZPCXAKY-y0U2c3xG.js → treemap-KZPCXAKY-BRjDo2aE.js} +1 -1
  54. package/dist/assets/{vendor-codemirror-CMHSJ_9p.js → vendor-codemirror-BiCeS-y4.js} +1 -1
  55. package/dist/assets/{vendor-react-xmA_f8ig.js → vendor-react-DVlYPmi3.js} +1 -1
  56. package/dist/assets/{vennDiagram-LZ73GAT5-xKj3SjYG.js → vennDiagram-LZ73GAT5-DrRqcDqo.js} +1 -1
  57. package/dist/assets/{xychartDiagram-JWTSCODW-Da_qyEoX.js → xychartDiagram-JWTSCODW-DUXrymAi.js} +1 -1
  58. package/dist/index.html +4 -4
  59. package/package.json +25 -6
  60. package/scripts/refresh-acp-default-capabilities.mjs +160 -0
  61. package/server/acp-runtime/client.js +1137 -181
  62. package/server/acp-runtime/command-overrides.js +48 -0
  63. package/server/acp-runtime/index.js +576 -16
  64. package/server/acp-runtime/registry.js +6 -4
  65. package/server/acp-runtime/session-store.js +235 -92
  66. package/server/database/db.js +12 -3
  67. package/server/external-agent/ws.js +212 -11
  68. package/server/index.js +145 -52
  69. package/server/projects-watcher-config.js +4 -0
  70. package/server/projects.js +466 -125
  71. package/server/routes/cc-connect.js +5 -4
  72. package/server/routes/codex.js +24 -0
  73. package/server/routes/commands.js +144 -1
  74. package/server/routes/runs.js +641 -0
  75. package/server/routes/session-core.js +357 -109
  76. package/server/session-core/eventStore.js +0 -121
  77. package/server/session-core/providerAdapters.js +644 -163
  78. package/server/session-core/providerDiscovery.js +66 -38
  79. package/server/session-core/runRegistry.js +244 -0
  80. package/server/session-core/runtimeState.js +75 -3
  81. package/server/session-core/runtimeWriter.js +132 -10
  82. package/server/utils/codexImagePlayground.js +479 -0
  83. package/server/utils/localTerminal.js +56 -0
  84. package/server/utils/shellCommand.js +70 -0
  85. package/shared/acpCapabilities.js +393 -0
  86. package/shared/acpDefaultCapabilities.generated.json +141 -0
  87. package/shared/conversationEvents.js +425 -121
  88. package/dist/assets/App-VH1wNUHs.js +0 -259
  89. package/dist/assets/ReviewApp-D_9EN4TM.js +0 -1
  90. package/dist/assets/channel-CyNUnRfc.js +0 -1
  91. package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +0 -1
  92. package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +0 -1
  93. package/dist/assets/clone-C341l3d0.js +0 -1
  94. package/dist/assets/index-DBkz_W_P.css +0 -1
  95. package/dist/assets/index-DdRyoXKh.js +0 -2
  96. package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +0 -1
@@ -13,8 +13,16 @@ import {
13
13
  import {
14
14
  CONVERSATION_EVENT_KINDS,
15
15
  createConversationEvent,
16
+ createUserMessageConversationEvent,
16
17
  stripAssistantProtocolTags
17
18
  } from '../../shared/conversationEvents.js';
19
+ import {
20
+ buildAcpCapabilities,
21
+ normalizeAcpAvailableCommands,
22
+ normalizeAcpConfigOptions,
23
+ normalizeAcpModeState,
24
+ normalizeAcpTokenUsage
25
+ } from '../../shared/acpCapabilities.js';
18
26
  import { parseDataUrl } from '../utils/agentImages.js';
19
27
  import { spawnCommand } from '../utils/spawnCommand.js';
20
28
  import { resolveCommandPath } from '../utils/resolveCommandPath.js';
@@ -22,10 +30,30 @@ import { resolveAgentCommand } from './registry.js';
22
30
 
23
31
  const DEFAULT_TERMINAL_OUTPUT_LIMIT = 256 * 1024;
24
32
  const DEFAULT_CLOSE_GRACE_MS = 1000;
33
+ const DEFAULT_CANCEL_GRACE_MS = normalizePromptTimeoutMs(process.env.ACP_CANCEL_GRACE_MS, 3000);
25
34
  const DEFAULT_TERMINAL_MAX_LIFETIME_MS = parseInt(process.env.ACP_TERMINAL_MAX_LIFETIME_MS, 10) || (5 * 60 * 1000);
35
+ const DEFAULT_START_TIMEOUT_MS = normalizePromptTimeoutMs(
36
+ process.env.ACP_START_TIMEOUT_MS ?? process.env.AXHUB_GENIE_ACP_START_TIMEOUT_MS,
37
+ 45 * 1000
38
+ );
39
+ const DEFAULT_SESSION_TIMEOUT_MS = normalizePromptTimeoutMs(
40
+ process.env.ACP_SESSION_TIMEOUT_MS ?? process.env.AXHUB_GENIE_ACP_SESSION_TIMEOUT_MS,
41
+ 45 * 1000
42
+ );
43
+ const DEFAULT_PROMPT_TIMEOUT_MS = normalizePromptTimeoutMs(
44
+ process.env.ACP_PROMPT_INACTIVITY_TIMEOUT_MS
45
+ ?? process.env.AXHUB_GENIE_ACP_PROMPT_INACTIVITY_TIMEOUT_MS
46
+ ?? process.env.ACP_PROMPT_TIMEOUT_MS
47
+ ?? process.env.AXHUB_GENIE_ACP_PROMPT_TIMEOUT_MS,
48
+ 600 * 1000
49
+ );
50
+ const START_TIMEOUT_ERROR_CODE = 'ACP_START_TIMEOUT';
51
+ const SESSION_TIMEOUT_ERROR_CODE = 'ACP_SESSION_TIMEOUT';
52
+ const PROMPT_TIMEOUT_ERROR_CODE = 'ACP_PROMPT_INACTIVITY_TIMEOUT';
26
53
  const FILTERED_CHILD_PROCESS_ENV_KEYS = new Set(['NODE_OPTIONS']);
27
54
 
28
55
  const pendingPermissionRequests = new Map();
56
+ const pendingElicitationRequests = new Map();
29
57
  const loggedMalformedUsageUpdates = new Set();
30
58
 
31
59
  function getDisabledClaudeAcpMcpServers() {
@@ -42,9 +70,60 @@ function getDisabledClaudeAcpMcpServers() {
42
70
  }
43
71
 
44
72
  function cloneJsonValue(value) {
73
+ if (value === undefined) {
74
+ return undefined;
75
+ }
76
+
45
77
  return JSON.parse(JSON.stringify(value));
46
78
  }
47
79
 
80
+ function normalizePromptTimeoutMs(value, fallback) {
81
+ const parsed = Number(value);
82
+ return Number.isFinite(parsed) && parsed >= 0
83
+ ? Math.floor(parsed)
84
+ : fallback;
85
+ }
86
+
87
+ function createPromptTimeoutError(agentKey, timeoutMs, details = {}) {
88
+ const error = new Error(`${agentKey || 'ACP'} ACP prompt inactivity timed out after ${timeoutMs}ms`);
89
+ error.code = PROMPT_TIMEOUT_ERROR_CODE;
90
+ error.details = {
91
+ timeoutMs,
92
+ ...details
93
+ };
94
+ return error;
95
+ }
96
+
97
+ function createLifecycleTimeoutError(agentKey, operation, timeoutMs, code) {
98
+ const error = new Error(`${agentKey || 'ACP'} ACP ${operation} timed out after ${timeoutMs}ms`);
99
+ error.code = code;
100
+ return error;
101
+ }
102
+
103
+ function withAcpOperationTimeout(promise, {
104
+ agentKey = 'ACP',
105
+ operation = 'operation',
106
+ timeoutMs = 0,
107
+ code = START_TIMEOUT_ERROR_CODE
108
+ } = {}) {
109
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
110
+ return promise;
111
+ }
112
+
113
+ let timeoutId = null;
114
+ const timeoutPromise = new Promise((_, reject) => {
115
+ timeoutId = setTimeout(() => {
116
+ reject(createLifecycleTimeoutError(agentKey, operation, timeoutMs, code));
117
+ }, timeoutMs);
118
+ });
119
+
120
+ return Promise.race([promise, timeoutPromise]).finally(() => {
121
+ if (timeoutId) {
122
+ clearTimeout(timeoutId);
123
+ }
124
+ });
125
+ }
126
+
48
127
  function logMalformedUsageUpdate(agentKey, sessionId, used, size) {
49
128
  const cacheKey = `${agentKey || 'unknown'}:${sessionId || 'unknown'}:${String(used)}:${String(size)}`;
50
129
  if (loggedMalformedUsageUpdates.has(cacheKey)) {
@@ -205,6 +284,10 @@ function normalizeText(value) {
205
284
  return typeof value === 'string' ? value : String(value ?? '');
206
285
  }
207
286
 
287
+ function normalizePromptEchoText(value) {
288
+ return String(value || '').replace(/\r\n/g, '\n').trim();
289
+ }
290
+
208
291
  function normalizeComparableValue(value) {
209
292
  try {
210
293
  return JSON.stringify(value ?? null);
@@ -389,6 +472,26 @@ function choosePermissionResponse(request, decision = {}) {
389
472
  return rejectMatch ? createSelectedPermissionResponse(rejectMatch.optionId) : createCancelledPermissionResponse();
390
473
  }
391
474
 
475
+ function normalizeElicitationResponse(decision = {}) {
476
+ const action = String(decision?.action || '').trim().toLowerCase();
477
+
478
+ if (action === 'accept') {
479
+ const content = decision?.content && typeof decision.content === 'object' && !Array.isArray(decision.content)
480
+ ? cloneJsonValue(decision.content)
481
+ : {};
482
+ return {
483
+ action: 'accept',
484
+ content
485
+ };
486
+ }
487
+
488
+ if (action === 'decline') {
489
+ return { action: 'decline' };
490
+ }
491
+
492
+ return { action: 'cancel' };
493
+ }
494
+
392
495
  function readLines(content, line = null, limit = null) {
393
496
  if (line == null && limit == null) {
394
497
  return content;
@@ -512,68 +615,9 @@ function buildToolCallConversationPayload(update = {}, toolName = 'Tool') {
512
615
  return payload;
513
616
  }
514
617
 
515
- function normalizeSessionModeState(modes) {
516
- if (!modes || typeof modes !== 'object') {
517
- return null;
518
- }
519
-
520
- const availableModes = Array.isArray(modes.availableModes)
521
- ? modes.availableModes
522
- .map((mode) => {
523
- if (!mode || typeof mode !== 'object') {
524
- return null;
525
- }
526
-
527
- const id = String(mode.id || '').trim();
528
- if (!id) {
529
- return null;
530
- }
531
-
532
- return {
533
- id,
534
- name: String(mode.name || id),
535
- description: mode.description == null ? null : String(mode.description)
536
- };
537
- })
538
- .filter(Boolean)
539
- : [];
540
-
541
- const currentModeId = String(modes.currentModeId || '').trim();
542
-
543
- if (!currentModeId && availableModes.length === 0) {
544
- return null;
545
- }
546
-
547
- return {
548
- availableModes,
549
- currentModeId: currentModeId || null
550
- };
551
- }
552
-
553
618
  function normalizeAvailableCommandsUpdate(update = {}) {
554
- const availableCommands = Array.isArray(update.availableCommands)
555
- ? update.availableCommands
556
- .map((command) => {
557
- if (!command || typeof command !== 'object') {
558
- return null;
559
- }
560
-
561
- const name = String(command.name || '').trim();
562
- if (!name) {
563
- return null;
564
- }
565
-
566
- return {
567
- name,
568
- description: command.description == null ? '' : String(command.description),
569
- input: command.input ? cloneJsonValue(command.input) : null
570
- };
571
- })
572
- .filter(Boolean)
573
- : [];
574
-
575
619
  return {
576
- availableCommands
620
+ availableCommands: normalizeAcpAvailableCommands(update)
577
621
  };
578
622
  }
579
623
 
@@ -775,7 +819,7 @@ async function resolveNpxFallbackCommand(commandLine, agentKey) {
775
819
 
776
820
  const npmCheck = await resolveCommandPath('npm');
777
821
  if (npmCheck.found) {
778
- return ['npm', 'exec', '--yes', '--', ...parsed.args].join(' ');
822
+ return convertNpxCommandToNpmExec(commandLine);
779
823
  }
780
824
 
781
825
  throw new Error(
@@ -783,6 +827,142 @@ async function resolveNpxFallbackCommand(commandLine, agentKey) {
783
827
  );
784
828
  }
785
829
 
830
+ async function resolveOpencodeCommand(commandLine) {
831
+ const parsed = splitCommandLine(commandLine);
832
+ if (parsed.command !== 'opencode') {
833
+ return commandLine;
834
+ }
835
+
836
+ const opencodeCheck = await resolveCommandPath('opencode');
837
+ if (opencodeCheck.found) {
838
+ return commandLine;
839
+ }
840
+
841
+ return resolveNpxFallbackCommand('npx -y opencode-ai acp', 'opencode');
842
+ }
843
+
844
+ function buildCommandLineFromAdapterArgs(command, args = []) {
845
+ const parts = [command, ...args].map((part) => String(part || '').trim()).filter(Boolean);
846
+ return parts.join(' ');
847
+ }
848
+
849
+ function convertNpxCommandToNpmExec(commandLine) {
850
+ const parsed = splitCommandLine(commandLine);
851
+ if (parsed.command !== 'npx') {
852
+ return commandLine;
853
+ }
854
+
855
+ const normalizedArgs = [...parsed.args];
856
+ while (normalizedArgs[0] === '-y' || normalizedArgs[0] === '--yes') {
857
+ normalizedArgs.shift();
858
+ }
859
+
860
+ return buildCommandLineFromAdapterArgs('npm exec --yes --', normalizedArgs);
861
+ }
862
+
863
+ function isSpawnEnoentError(error) {
864
+ return Boolean(error && typeof error === 'object' && error.code === 'ENOENT');
865
+ }
866
+
867
+ function waitForChildProcessSpawn(childProcess) {
868
+ return new Promise((resolve, reject) => {
869
+ const cleanup = () => {
870
+ childProcess.off?.('spawn', onSpawn);
871
+ childProcess.off?.('error', onError);
872
+ };
873
+
874
+ const onSpawn = () => {
875
+ cleanup();
876
+ resolve(childProcess);
877
+ };
878
+
879
+ const onError = (error) => {
880
+ cleanup();
881
+ reject(error);
882
+ };
883
+
884
+ childProcess.once('spawn', onSpawn);
885
+ childProcess.once('error', onError);
886
+ });
887
+ }
888
+
889
+ function waitForChildProcessClose(childProcess, graceMs = DEFAULT_CLOSE_GRACE_MS) {
890
+ return new Promise((resolve) => {
891
+ if (!childProcess || childProcess.exitCode != null || childProcess.signalCode != null) {
892
+ resolve(true);
893
+ return;
894
+ }
895
+
896
+ let settled = false;
897
+ const finalize = (value) => {
898
+ if (settled) {
899
+ return;
900
+ }
901
+ settled = true;
902
+ resolve(value);
903
+ };
904
+
905
+ const timer = setTimeout(() => finalize(false), graceMs);
906
+ childProcess.once?.('close', () => {
907
+ clearTimeout(timer);
908
+ finalize(true);
909
+ });
910
+ });
911
+ }
912
+
913
+ function terminateChildProcessTree(childProcess, signal = 'SIGTERM') {
914
+ if (!childProcess || childProcess.killed) {
915
+ return false;
916
+ }
917
+
918
+ if (process.platform !== 'win32' && Number.isInteger(childProcess.pid)) {
919
+ try {
920
+ process.kill(-childProcess.pid, signal);
921
+ return true;
922
+ } catch (error) {
923
+ if (error?.code !== 'ESRCH' && error?.code !== 'EINVAL') {
924
+ console.warn(`[ACP] Failed to terminate child process group ${childProcess.pid}: ${error.message}`);
925
+ }
926
+ }
927
+ }
928
+
929
+ try {
930
+ childProcess.kill(signal);
931
+ return true;
932
+ } catch {
933
+ return false;
934
+ }
935
+ }
936
+
937
+ export async function startAcpChildProcess({ commandLine, projectPath, env, spawnImpl = spawnCommand }) {
938
+ const spawnAttempt = (attemptCommandLine) => {
939
+ const { command, args } = splitCommandLine(attemptCommandLine);
940
+ const childProcess = spawnImpl(command, args, {
941
+ cwd: projectPath,
942
+ stdio: ['pipe', 'pipe', 'pipe'],
943
+ windowsHide: true,
944
+ detached: process.platform !== 'win32',
945
+ env
946
+ });
947
+ return waitForChildProcessSpawn(childProcess);
948
+ };
949
+
950
+ try {
951
+ return await spawnAttempt(commandLine);
952
+ } catch (error) {
953
+ if (!isSpawnEnoentError(error)) {
954
+ throw error;
955
+ }
956
+
957
+ const fallbackCommandLine = convertNpxCommandToNpmExec(commandLine);
958
+ if (fallbackCommandLine === commandLine) {
959
+ throw error;
960
+ }
961
+
962
+ return spawnAttempt(fallbackCommandLine);
963
+ }
964
+ }
965
+
786
966
  export async function resolveLaunchCommand(agentKey, overrides = {}) {
787
967
  const commandLine = resolveAgentCommand(agentKey, overrides);
788
968
  if (!commandLine) {
@@ -796,6 +976,10 @@ export async function resolveLaunchCommand(agentKey, overrides = {}) {
796
976
  return resolveGeminiCommand(resolvedCommandLine);
797
977
  }
798
978
 
979
+ if (normalizedAgentKey === 'opencode') {
980
+ return resolveOpencodeCommand(resolvedCommandLine);
981
+ }
982
+
799
983
  return resolvedCommandLine;
800
984
  }
801
985
 
@@ -943,7 +1127,14 @@ export class AcpClient {
943
1127
  projectPath,
944
1128
  agentCommandOverrides = {},
945
1129
  model = null,
946
- permissionMode = 'default'
1130
+ permissionMode = 'default',
1131
+ modeId = null,
1132
+ thoughtLevel = null,
1133
+ promptTimeoutMs = DEFAULT_PROMPT_TIMEOUT_MS,
1134
+ startTimeoutMs = DEFAULT_START_TIMEOUT_MS,
1135
+ sessionTimeoutMs = DEFAULT_SESSION_TIMEOUT_MS,
1136
+ startChildProcess = startAcpChildProcess,
1137
+ createConnection = (handlers, stream) => new ClientSideConnection(handlers, stream)
947
1138
  }) {
948
1139
  this.agentKey = String(agentKey || '').trim().toLowerCase();
949
1140
  this.writer = writer;
@@ -951,26 +1142,54 @@ export class AcpClient {
951
1142
  this.agentCommandOverrides = agentCommandOverrides || {};
952
1143
  this.model = model || null;
953
1144
  this.permissionMode = permissionMode || 'default';
1145
+ this.modeId = typeof modeId === 'string' && modeId.trim() ? modeId.trim() : null;
1146
+ this.thoughtLevel = typeof thoughtLevel === 'string' && thoughtLevel.trim() ? thoughtLevel.trim() : null;
1147
+ this.promptTimeoutMs = normalizePromptTimeoutMs(promptTimeoutMs, DEFAULT_PROMPT_TIMEOUT_MS);
1148
+ this.startTimeoutMs = normalizePromptTimeoutMs(startTimeoutMs, DEFAULT_START_TIMEOUT_MS);
1149
+ this.sessionTimeoutMs = normalizePromptTimeoutMs(sessionTimeoutMs, DEFAULT_SESSION_TIMEOUT_MS);
1150
+ this.startChildProcess = typeof startChildProcess === 'function' ? startChildProcess : startAcpChildProcess;
1151
+ this.createConnection = typeof createConnection === 'function'
1152
+ ? createConnection
1153
+ : ((handlers, stream) => new ClientSideConnection(handlers, stream));
1154
+ this.configOptionIdsByCategory = new Map();
1155
+ this.configOptionIdsByKey = new Map();
954
1156
 
955
1157
  this.childProcess = null;
956
1158
  this.connection = null;
957
1159
  this.sessionId = null;
958
1160
  this.initializeResult = null;
959
1161
  this.suppressSessionUpdates = false;
1162
+ this.isLoadingSessionReplay = false;
960
1163
  this.pendingPermissionIds = new Set();
1164
+ this.pendingElicitationIds = new Set();
961
1165
  this.terminalProcesses = new Map();
962
1166
  this.spawnCleanup = null;
963
1167
  this.turnQueue = Promise.resolve();
964
1168
  this.isPromptInFlight = false;
965
1169
  this.pendingPromptDiagnostic = null;
1170
+ this.pendingPromptTurnCompletion = null;
1171
+ this.activePromptClientRequestId = null;
1172
+ this.activePromptText = null;
1173
+ this.isReplayingPromptHistory = false;
1174
+ this.hasObservedActivePromptEcho = false;
966
1175
  this.hasReportedPromptFatalError = false;
967
1176
  this.agentDiagnosticBuffer = '';
1177
+ this.promptActivity = {
1178
+ timer: null,
1179
+ reject: null,
1180
+ timeoutMs: this.promptTimeoutMs,
1181
+ startedAt: null,
1182
+ lastActivityAt: null,
1183
+ lastActivityKind: null,
1184
+ pauseCount: 0
1185
+ };
968
1186
  this.streamState = {
969
1187
  turnId: null,
970
1188
  assistantSegmentIndex: 0,
971
1189
  assistantMessageId: null,
972
1190
  assistantTextStarted: false,
973
1191
  reasoningMessageId: null,
1192
+ textStreamsClosedForTurn: false,
974
1193
  toolCalls: new Map()
975
1194
  };
976
1195
  }
@@ -997,7 +1216,38 @@ export class AcpClient {
997
1216
  }
998
1217
  }
999
1218
 
1000
- async configureForTurn({ writer, permissionMode = null, model = null } = {}) {
1219
+ noteConfigOptions(configOptions = []) {
1220
+ const normalizedConfigOptions = normalizeAcpConfigOptions(configOptions);
1221
+ for (const option of normalizedConfigOptions) {
1222
+ const configId = String(option.id || option.key || option.category || '').trim();
1223
+ if (!configId) {
1224
+ continue;
1225
+ }
1226
+
1227
+ if (option.key) {
1228
+ this.configOptionIdsByKey.set(option.key, configId);
1229
+ }
1230
+
1231
+ if (option.category) {
1232
+ this.configOptionIdsByCategory.set(option.category, configId);
1233
+ }
1234
+ }
1235
+
1236
+ return normalizedConfigOptions;
1237
+ }
1238
+
1239
+ resolveConfigOptionId(canonicalKey) {
1240
+ const normalizedKey = String(canonicalKey || '').trim();
1241
+ if (!normalizedKey) {
1242
+ return null;
1243
+ }
1244
+
1245
+ return this.configOptionIdsByCategory.get(normalizedKey) ||
1246
+ this.configOptionIdsByKey.get(normalizedKey) ||
1247
+ normalizedKey;
1248
+ }
1249
+
1250
+ async configureForTurn({ writer, permissionMode = null, model = null, modeId = null, thoughtLevel = null } = {}) {
1001
1251
  if (writer !== undefined) {
1002
1252
  this.attachWriter(writer);
1003
1253
  }
@@ -1013,9 +1263,35 @@ export class AcpClient {
1013
1263
  await this.setModel(model);
1014
1264
  }
1015
1265
  }
1266
+
1267
+ const normalizedModeId = typeof modeId === 'string' && modeId.trim() ? modeId.trim() : null;
1268
+ if (normalizedModeId) {
1269
+ const shouldUpdateMode = normalizedModeId !== this.modeId;
1270
+ this.modeId = normalizedModeId;
1271
+ if (shouldUpdateMode && this.connection && this.sessionId) {
1272
+ await this.setMode(normalizedModeId);
1273
+ }
1274
+ }
1275
+
1276
+ const normalizedThoughtLevel = typeof thoughtLevel === 'string' && thoughtLevel.trim()
1277
+ ? thoughtLevel.trim()
1278
+ : null;
1279
+ if (normalizedThoughtLevel) {
1280
+ const shouldUpdateThoughtLevel = normalizedThoughtLevel !== this.thoughtLevel;
1281
+ this.thoughtLevel = normalizedThoughtLevel;
1282
+ if (shouldUpdateThoughtLevel && this.connection && this.sessionId) {
1283
+ await this.setConfigOption(this.resolveConfigOptionId('thought_level'), normalizedThoughtLevel);
1284
+ }
1285
+ }
1016
1286
  }
1017
1287
 
1018
1288
  emitConversationEvent(kind, payload = {}, options = {}) {
1289
+ const replayExtensions = this.isLoadingSessionReplay
1290
+ ? { runtimeReplay: true }
1291
+ : {};
1292
+ const replaySourceType = this.isLoadingSessionReplay
1293
+ ? 'acp-session-loaded-replay'
1294
+ : null;
1019
1295
  const event = createConversationEvent({
1020
1296
  kind,
1021
1297
  provider: this.agentKey,
@@ -1024,11 +1300,12 @@ export class AcpClient {
1024
1300
  payload,
1025
1301
  extensions: {
1026
1302
  runtimeSource: 'acp',
1303
+ ...replayExtensions,
1027
1304
  ...(options.extensions || {})
1028
1305
  },
1029
1306
  rawRef: {
1030
1307
  runtime: 'acp',
1031
- sourceType: options.sourceType || 'acp-session-update'
1308
+ sourceType: replaySourceType || options.sourceType || 'acp-session-update'
1032
1309
  }
1033
1310
  });
1034
1311
 
@@ -1060,13 +1337,19 @@ export class AcpClient {
1060
1337
 
1061
1338
  this.hasReportedPromptFatalError = true;
1062
1339
  const errorMessage = error?.message || 'Agent prompt failed.';
1340
+ const errorCode = error?.code || null;
1341
+ const errorDetails = error?.details && typeof error.details === 'object'
1342
+ ? cloneJsonValue(error.details)
1343
+ : null;
1063
1344
 
1064
1345
  const failedAt = nowIso();
1065
1346
  this.flushOpenTextStreams(failedAt);
1066
1347
  this.emitConversationEvent(
1067
1348
  CONVERSATION_EVENT_KINDS.ERROR,
1068
1349
  {
1069
- message: errorMessage
1350
+ message: errorMessage,
1351
+ ...(errorCode ? { code: errorCode } : {}),
1352
+ ...(errorDetails ? { details: errorDetails } : {})
1070
1353
  },
1071
1354
  {
1072
1355
  timestamp: failedAt,
@@ -1077,7 +1360,9 @@ export class AcpClient {
1077
1360
  CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
1078
1361
  {
1079
1362
  state: 'errored',
1080
- message: errorMessage
1363
+ message: errorMessage,
1364
+ ...(errorCode ? { code: errorCode } : {}),
1365
+ ...(errorDetails ? { details: errorDetails } : {})
1081
1366
  },
1082
1367
  {
1083
1368
  timestamp: failedAt,
@@ -1096,6 +1381,8 @@ export class AcpClient {
1096
1381
  return;
1097
1382
  }
1098
1383
 
1384
+ this.notePromptActivity('stderr_diagnostic');
1385
+
1099
1386
  const nextBuffer = this.agentDiagnosticBuffer
1100
1387
  ? `${this.agentDiagnosticBuffer}\n${diagnosticText}`
1101
1388
  : diagnosticText;
@@ -1126,6 +1413,114 @@ export class AcpClient {
1126
1413
  }
1127
1414
  }
1128
1415
 
1416
+ createPromptTimeoutDetails() {
1417
+ const now = Date.now();
1418
+ const startedAt = this.promptActivity.startedAt;
1419
+ const lastActivityAt = this.promptActivity.lastActivityAt;
1420
+ return {
1421
+ timeoutMs: this.promptActivity.timeoutMs,
1422
+ lastActivityAt: lastActivityAt ? new Date(lastActivityAt).toISOString() : null,
1423
+ lastActivityKind: this.promptActivity.lastActivityKind || null,
1424
+ promptAgeMs: startedAt ? Math.max(0, now - startedAt) : null,
1425
+ pendingPermissionCount: this.pendingPermissionIds.size,
1426
+ pendingElicitationCount: this.pendingElicitationIds.size,
1427
+ stderrTail: this.agentDiagnosticBuffer || ''
1428
+ };
1429
+ }
1430
+
1431
+ clearPromptActivityTimer() {
1432
+ if (this.promptActivity.timer) {
1433
+ clearTimeout(this.promptActivity.timer);
1434
+ this.promptActivity.timer = null;
1435
+ }
1436
+ }
1437
+
1438
+ schedulePromptActivityTimer() {
1439
+ this.clearPromptActivityTimer();
1440
+ const timeoutMs = this.promptActivity.timeoutMs;
1441
+ if (
1442
+ !this.isPromptInFlight ||
1443
+ !this.promptActivity.reject ||
1444
+ !Number.isFinite(timeoutMs) ||
1445
+ timeoutMs <= 0 ||
1446
+ this.promptActivity.pauseCount > 0
1447
+ ) {
1448
+ return;
1449
+ }
1450
+
1451
+ this.promptActivity.timer = setTimeout(() => {
1452
+ this.promptActivity.timer = null;
1453
+ const reject = this.promptActivity.reject;
1454
+ if (reject) {
1455
+ reject(createPromptTimeoutError(this.agentKey, timeoutMs, this.createPromptTimeoutDetails()));
1456
+ }
1457
+ }, timeoutMs);
1458
+ }
1459
+
1460
+ startPromptActivityWatchdog(reject, timeoutMs) {
1461
+ const now = Date.now();
1462
+ this.promptActivity = {
1463
+ timer: null,
1464
+ reject,
1465
+ timeoutMs,
1466
+ startedAt: now,
1467
+ lastActivityAt: now,
1468
+ lastActivityKind: 'prompt_sent',
1469
+ pauseCount: 0
1470
+ };
1471
+ this.schedulePromptActivityTimer();
1472
+ }
1473
+
1474
+ stopPromptActivityWatchdog() {
1475
+ this.clearPromptActivityTimer();
1476
+ this.promptActivity.reject = null;
1477
+ this.promptActivity.pauseCount = 0;
1478
+ }
1479
+
1480
+ resolvePromptTurnCompletion(stopReason = 'end_turn') {
1481
+ if (!this.isPromptInFlight || !this.pendingPromptTurnCompletion) {
1482
+ return false;
1483
+ }
1484
+
1485
+ if (this.isLoadingSessionReplay || this.isReplayingPromptHistory) {
1486
+ return false;
1487
+ }
1488
+
1489
+ const resolve = this.pendingPromptTurnCompletion;
1490
+ this.pendingPromptTurnCompletion = null;
1491
+ resolve({ stopReason });
1492
+ return true;
1493
+ }
1494
+
1495
+ notePromptActivity(kind) {
1496
+ if (!this.isPromptInFlight) {
1497
+ return;
1498
+ }
1499
+ this.promptActivity.lastActivityAt = Date.now();
1500
+ this.promptActivity.lastActivityKind = kind || 'activity';
1501
+ this.schedulePromptActivityTimer();
1502
+ }
1503
+
1504
+ pausePromptActivityWatchdog(kind = 'paused') {
1505
+ if (!this.isPromptInFlight) {
1506
+ return;
1507
+ }
1508
+ this.promptActivity.pauseCount += 1;
1509
+ this.promptActivity.lastActivityAt = Date.now();
1510
+ this.promptActivity.lastActivityKind = kind;
1511
+ this.clearPromptActivityTimer();
1512
+ }
1513
+
1514
+ resumePromptActivityWatchdog(kind = 'resumed') {
1515
+ if (!this.isPromptInFlight) {
1516
+ return;
1517
+ }
1518
+ this.promptActivity.pauseCount = Math.max(0, this.promptActivity.pauseCount - 1);
1519
+ this.promptActivity.lastActivityAt = Date.now();
1520
+ this.promptActivity.lastActivityKind = kind;
1521
+ this.schedulePromptActivityTimer();
1522
+ }
1523
+
1129
1524
  async runExclusive(operation) {
1130
1525
  const previousTurn = this.turnQueue.catch(() => {});
1131
1526
  let releaseTurn;
@@ -1160,6 +1555,39 @@ export class AcpClient {
1160
1555
  );
1161
1556
  }
1162
1557
 
1558
+ normalizeAcpMessageId(messageId, prefix = 'assistant_text') {
1559
+ const normalizedMessageId = typeof messageId === 'string' && messageId.trim()
1560
+ ? messageId.trim()
1561
+ : null;
1562
+ if (!normalizedMessageId) {
1563
+ return this.createAssistantMessageId(prefix);
1564
+ }
1565
+
1566
+ const turnId = this.ensureTurnId();
1567
+ return createEventMessageId(
1568
+ prefix,
1569
+ this.agentKey,
1570
+ this.sessionId,
1571
+ `${turnId}:segment-${this.streamState.assistantSegmentIndex}:${normalizedMessageId}`
1572
+ );
1573
+ }
1574
+
1575
+ prepareAssistantMessageBoundary(messageId, timestamp, sourceType) {
1576
+ const nextMessageId = this.normalizeAcpMessageId(messageId);
1577
+ if (
1578
+ this.streamState.assistantMessageId &&
1579
+ this.streamState.assistantMessageId !== nextMessageId
1580
+ ) {
1581
+ this.closeAssistantMessageBoundary(timestamp, sourceType);
1582
+ }
1583
+
1584
+ if (!this.streamState.assistantMessageId) {
1585
+ this.streamState.assistantMessageId = nextMessageId;
1586
+ }
1587
+
1588
+ return this.streamState.assistantMessageId;
1589
+ }
1590
+
1163
1591
  closeAssistantMessageBoundary(timestamp = nowIso(), sourceType = 'acp-message-boundary') {
1164
1592
  const hadAssistantBoundary = Boolean(
1165
1593
  this.streamState.assistantMessageId || this.streamState.assistantTextStarted
@@ -1197,6 +1625,8 @@ export class AcpClient {
1197
1625
  );
1198
1626
  this.streamState.reasoningMessageId = null;
1199
1627
  }
1628
+
1629
+ this.streamState.textStreamsClosedForTurn = true;
1200
1630
  }
1201
1631
 
1202
1632
  resetTurnState({ nextTurnId = randomUUID() } = {}) {
@@ -1204,6 +1634,72 @@ export class AcpClient {
1204
1634
  this.streamState.toolCalls.clear();
1205
1635
  this.streamState.turnId = nextTurnId;
1206
1636
  this.streamState.assistantSegmentIndex = 0;
1637
+ this.streamState.textStreamsClosedForTurn = false;
1638
+ this.isReplayingPromptHistory = false;
1639
+ this.hasObservedActivePromptEcho = false;
1640
+ }
1641
+
1642
+ classifyPromptUserChunk(text) {
1643
+ if (!this.isPromptInFlight) {
1644
+ return {
1645
+ isActivePromptEcho: false,
1646
+ isPromptHistoryReplay: false
1647
+ };
1648
+ }
1649
+
1650
+ const activePromptText = normalizePromptEchoText(this.activePromptText);
1651
+ const chunkText = normalizePromptEchoText(text);
1652
+ const isActivePromptEcho = Boolean(activePromptText && chunkText && activePromptText === chunkText);
1653
+
1654
+ if (isActivePromptEcho) {
1655
+ this.hasObservedActivePromptEcho = true;
1656
+ this.isReplayingPromptHistory = false;
1657
+ return {
1658
+ isActivePromptEcho: true,
1659
+ isPromptHistoryReplay: false
1660
+ };
1661
+ }
1662
+
1663
+ this.isReplayingPromptHistory = true;
1664
+ return {
1665
+ isActivePromptEcho: false,
1666
+ isPromptHistoryReplay: true
1667
+ };
1668
+ }
1669
+
1670
+ buildSessionUpdateEventOptions(sourceType, timestamp) {
1671
+ const replayingPromptHistory = this.isReplayingPromptHistory;
1672
+ const isTextStreamSource = sourceType === 'acp-agent-message' ||
1673
+ sourceType === 'acp-agent-message-boundary' ||
1674
+ sourceType === 'acp-agent-thought';
1675
+ if (this.isLoadingSessionReplay) {
1676
+ return {
1677
+ timestamp,
1678
+ sourceType: 'acp-session-loaded-replay',
1679
+ extensions: { runtimeReplay: true }
1680
+ };
1681
+ }
1682
+
1683
+ if (replayingPromptHistory) {
1684
+ return {
1685
+ timestamp,
1686
+ sourceType: 'acp-prompt-history-replay',
1687
+ extensions: { runtimeReplay: true }
1688
+ };
1689
+ }
1690
+
1691
+ if (isTextStreamSource && this.streamState.textStreamsClosedForTurn) {
1692
+ return {
1693
+ timestamp,
1694
+ sourceType: 'acp-post-turn-replay',
1695
+ extensions: { runtimeReplay: true }
1696
+ };
1697
+ }
1698
+
1699
+ return {
1700
+ timestamp,
1701
+ sourceType
1702
+ };
1207
1703
  }
1208
1704
 
1209
1705
  async handleRequestPermission(request) {
@@ -1235,6 +1731,7 @@ export class AcpClient {
1235
1731
  const timestamp = nowIso();
1236
1732
 
1237
1733
  this.pendingPermissionIds.add(requestId);
1734
+ this.pausePromptActivityWatchdog('approval_request');
1238
1735
 
1239
1736
  this.emitConversationEvent(
1240
1737
  CONVERSATION_EVENT_KINDS.APPROVAL_REQUEST,
@@ -1294,39 +1791,154 @@ export class AcpClient {
1294
1791
  }
1295
1792
  );
1296
1793
 
1794
+ this.resumePromptActivityWatchdog('approval_resolved');
1795
+
1297
1796
  return response;
1298
1797
  }
1299
1798
 
1300
- async handleSessionUpdate(notification) {
1301
- if (this.suppressSessionUpdates) {
1302
- return;
1303
- }
1304
-
1305
- const update = notification?.update;
1799
+ async handleCreateElicitation(request) {
1800
+ const requestId = `acp-elicitation:${randomUUID()}`;
1306
1801
  const timestamp = nowIso();
1307
- if (!update || typeof update !== 'object') {
1308
- return;
1309
- }
1802
+ const mode = String(request?.mode || '').trim().toLowerCase() || 'form';
1310
1803
 
1311
- if (update.sessionUpdate === 'agent_message_chunk') {
1804
+ this.pendingElicitationIds.add(requestId);
1805
+ this.pausePromptActivityWatchdog('elicitation_request');
1806
+
1807
+ this.emitConversationEvent(
1808
+ CONVERSATION_EVENT_KINDS.ELICITATION_REQUEST,
1809
+ {
1810
+ requestId,
1811
+ mode,
1812
+ message: String(request?.message || ''),
1813
+ requestedSchema: request?.requestedSchema && typeof request.requestedSchema === 'object'
1814
+ ? cloneJsonValue(request.requestedSchema)
1815
+ : null,
1816
+ url: typeof request?.url === 'string' ? request.url : null,
1817
+ elicitationId: typeof request?.elicitationId === 'string' ? request.elicitationId : null,
1818
+ scope: {
1819
+ sessionId: request?.sessionId || this.sessionId || null,
1820
+ toolCallId: request?.toolCallId || null,
1821
+ requestId: request?.requestId || null
1822
+ },
1823
+ sessionId: request?.sessionId || this.sessionId || null
1824
+ },
1825
+ {
1826
+ timestamp,
1827
+ extensions: {
1828
+ requestId
1829
+ },
1830
+ sourceType: 'acp-elicitation-request'
1831
+ }
1832
+ );
1833
+
1834
+ const response = await new Promise((resolve) => {
1835
+ pendingElicitationRequests.set(requestId, {
1836
+ request,
1837
+ sessionId: this.sessionId,
1838
+ provider: this.agentKey,
1839
+ resolve
1840
+ });
1841
+ });
1842
+
1843
+ this.pendingElicitationIds.delete(requestId);
1844
+ pendingElicitationRequests.delete(requestId);
1845
+
1846
+ this.emitConversationEvent(
1847
+ CONVERSATION_EVENT_KINDS.ELICITATION_RESOLVED,
1848
+ {
1849
+ requestId,
1850
+ action: response.action,
1851
+ content: response.action === 'accept' ? cloneJsonValue(response.content || {}) : null,
1852
+ message: response.action === 'accept'
1853
+ ? 'User submitted input'
1854
+ : response.action === 'decline'
1855
+ ? 'User declined input'
1856
+ : 'User cancelled input'
1857
+ },
1858
+ {
1859
+ timestamp: nowIso(),
1860
+ extensions: {
1861
+ requestId
1862
+ },
1863
+ sourceType: 'acp-elicitation-resolution'
1864
+ }
1865
+ );
1866
+
1867
+ this.resumePromptActivityWatchdog('elicitation_resolved');
1868
+
1869
+ return response;
1870
+ }
1871
+
1872
+ async handleSessionUpdate(notification) {
1873
+ if (this.suppressSessionUpdates) {
1874
+ return;
1875
+ }
1876
+
1877
+ const update = notification?.update;
1878
+ const timestamp = nowIso();
1879
+ if (!update || typeof update !== 'object') {
1880
+ return;
1881
+ }
1882
+ const updateKind = String(update.sessionUpdate || '').trim() || 'unknown';
1883
+ this.notePromptActivity(`session_update:${updateKind}`);
1884
+
1885
+ if (update.sessionUpdate === 'user_message_chunk') {
1312
1886
  if (update.content?.type === 'text' && typeof update.content.text === 'string') {
1313
- const cleanText = stripAssistantProtocolTags(update.content.text);
1314
- if (!cleanText) {
1887
+ const text = update.content.text;
1888
+ if (!text) {
1315
1889
  return;
1316
1890
  }
1891
+ const promptChunk = this.classifyPromptUserChunk(text);
1892
+ const replayExtensions = (
1893
+ this.isLoadingSessionReplay ||
1894
+ promptChunk.isPromptHistoryReplay
1895
+ )
1896
+ ? { runtimeReplay: true }
1897
+ : {};
1898
+ const sourceType = this.isLoadingSessionReplay
1899
+ ? 'acp-session-loaded-replay'
1900
+ : promptChunk.isPromptHistoryReplay
1901
+ ? 'acp-prompt-history-replay'
1902
+ : 'acp-user-message-chunk';
1903
+
1904
+ this.emitPayload({
1905
+ type: 'conversation-event',
1906
+ event: createUserMessageConversationEvent({
1907
+ provider: this.agentKey,
1908
+ sessionId: notification?.sessionId || this.sessionId,
1909
+ timestamp,
1910
+ text,
1911
+ contentBlocks: [],
1912
+ rawRef: {
1913
+ runtime: 'acp',
1914
+ sourceType
1915
+ },
1916
+ clientRequestId: null,
1917
+ extensions: replayExtensions
1918
+ })
1919
+ });
1920
+ }
1921
+ return;
1922
+ }
1317
1923
 
1318
- const messageId = this.streamState.assistantMessageId
1319
- || this.createAssistantMessageId('assistant_text');
1320
-
1321
- if (!this.streamState.assistantMessageId) {
1322
- this.streamState.assistantMessageId = messageId;
1924
+ if (update.sessionUpdate === 'agent_message_chunk') {
1925
+ if (update.content?.type === 'text' && typeof update.content.text === 'string') {
1926
+ const cleanText = stripAssistantProtocolTags(update.content.text, { trim: false });
1927
+ if (!cleanText) {
1928
+ return;
1323
1929
  }
1324
1930
 
1931
+ const messageId = this.prepareAssistantMessageBoundary(
1932
+ update.messageId,
1933
+ timestamp,
1934
+ 'acp-agent-message-boundary'
1935
+ );
1936
+
1325
1937
  if (!this.streamState.assistantTextStarted) {
1326
1938
  this.emitConversationEvent(
1327
1939
  CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START,
1328
1940
  { messageId },
1329
- { timestamp, sourceType: 'acp-agent-message' }
1941
+ this.buildSessionUpdateEventOptions('acp-agent-message', timestamp)
1330
1942
  );
1331
1943
  this.streamState.assistantTextStarted = true;
1332
1944
  }
@@ -1337,22 +1949,18 @@ export class AcpClient {
1337
1949
  messageId,
1338
1950
  text: cleanText
1339
1951
  },
1340
- {
1341
- timestamp,
1342
- sourceType: 'acp-agent-message'
1343
- }
1952
+ this.buildSessionUpdateEventOptions('acp-agent-message', timestamp)
1344
1953
  );
1345
1954
  return;
1346
1955
  }
1347
1956
 
1348
1957
  const contentBlock = normalizeAssistantContentBlock(update.content);
1349
1958
  if (contentBlock) {
1350
- const messageId = this.streamState.assistantMessageId
1351
- || this.createAssistantMessageId('assistant_content');
1352
-
1353
- if (!this.streamState.assistantMessageId) {
1354
- this.streamState.assistantMessageId = messageId;
1355
- }
1959
+ const messageId = this.prepareAssistantMessageBoundary(
1960
+ update.messageId,
1961
+ timestamp,
1962
+ 'acp-agent-message-boundary'
1963
+ );
1356
1964
 
1357
1965
  this.emitConversationEvent(
1358
1966
  CONVERSATION_EVENT_KINDS.ASSISTANT_CONTENT_BLOCK,
@@ -1360,10 +1968,7 @@ export class AcpClient {
1360
1968
  messageId,
1361
1969
  contentBlock
1362
1970
  },
1363
- {
1364
- timestamp,
1365
- sourceType: 'acp-agent-message'
1366
- }
1971
+ this.buildSessionUpdateEventOptions('acp-agent-message', timestamp)
1367
1972
  );
1368
1973
  }
1369
1974
  return;
@@ -1378,7 +1983,7 @@ export class AcpClient {
1378
1983
  this.emitConversationEvent(
1379
1984
  CONVERSATION_EVENT_KINDS.REASONING_START,
1380
1985
  { messageId },
1381
- { timestamp, sourceType: 'acp-agent-thought' }
1986
+ this.buildSessionUpdateEventOptions('acp-agent-thought', timestamp)
1382
1987
  );
1383
1988
  this.streamState.reasoningMessageId = messageId;
1384
1989
  }
@@ -1389,10 +1994,7 @@ export class AcpClient {
1389
1994
  messageId,
1390
1995
  text: update.content.text
1391
1996
  },
1392
- {
1393
- timestamp,
1394
- sourceType: 'acp-agent-thought'
1395
- }
1997
+ this.buildSessionUpdateEventOptions('acp-agent-thought', timestamp)
1396
1998
  );
1397
1999
  }
1398
2000
  return;
@@ -1400,6 +2002,7 @@ export class AcpClient {
1400
2002
 
1401
2003
  if (update.sessionUpdate === 'end_turn') {
1402
2004
  this.flushOpenTextStreams(timestamp);
2005
+ this.resolvePromptTurnCompletion('end_turn');
1403
2006
  return;
1404
2007
  }
1405
2008
 
@@ -1447,10 +2050,11 @@ export class AcpClient {
1447
2050
  }
1448
2051
 
1449
2052
  if (update.sessionUpdate === 'config_option_update') {
2053
+ const normalizedConfigOptions = this.noteConfigOptions(update.configOptions || []);
1450
2054
  this.emitConversationEvent(
1451
2055
  CONVERSATION_EVENT_KINDS.CONFIG_OPTION_UPDATE,
1452
2056
  {
1453
- configOptions: cloneJsonValue(update.configOptions || [])
2057
+ configOptions: cloneJsonValue(normalizedConfigOptions)
1454
2058
  },
1455
2059
  {
1456
2060
  timestamp,
@@ -1557,15 +2161,20 @@ export class AcpClient {
1557
2161
  });
1558
2162
 
1559
2163
  if (resultSignature !== existing.resultSignature) {
2164
+ const resultPayload = {
2165
+ ...toolPayload,
2166
+ content: resultText || '',
2167
+ isError: update.status === 'failed',
2168
+ contentBlocks: Array.isArray(update.content) ? cloneJsonValue(update.content) : []
2169
+ };
2170
+
2171
+ if (update.rawOutput !== undefined) {
2172
+ resultPayload.rawOutput = cloneJsonValue(update.rawOutput);
2173
+ }
2174
+
1560
2175
  this.emitConversationEvent(
1561
2176
  CONVERSATION_EVENT_KINDS.TOOL_RESULT,
1562
- {
1563
- ...toolPayload,
1564
- content: resultText || '',
1565
- isError: update.status === 'failed',
1566
- rawOutput: cloneJsonValue(update.rawOutput),
1567
- contentBlocks: Array.isArray(update.content) ? cloneJsonValue(update.content) : []
1568
- },
2177
+ resultPayload,
1569
2178
  {
1570
2179
  timestamp,
1571
2180
  sourceType: 'acp-tool-call'
@@ -1588,14 +2197,11 @@ export class AcpClient {
1588
2197
  }
1589
2198
 
1590
2199
  const launchCommand = await resolveLaunchCommand(this.agentKey, this.agentCommandOverrides);
1591
- const { command, args } = splitCommandLine(launchCommand);
1592
2200
  const spawnEnvironment = await prepareSpawnEnvironment(this.agentKey);
1593
2201
  this.spawnCleanup = spawnEnvironment.cleanup;
1594
-
1595
- this.childProcess = spawnCommand(command, args, {
1596
- cwd: this.projectPath,
1597
- stdio: ['pipe', 'pipe', 'pipe'],
1598
- windowsHide: true,
2202
+ this.childProcess = await this.startChildProcess({
2203
+ commandLine: launchCommand,
2204
+ projectPath: this.projectPath,
1599
2205
  env: spawnEnvironment.env
1600
2206
  });
1601
2207
 
@@ -1613,10 +2219,12 @@ export class AcpClient {
1613
2219
  this.agentKey
1614
2220
  );
1615
2221
 
1616
- this.connection = new ClientSideConnection(() => ({
2222
+ this.connection = this.createConnection(() => ({
1617
2223
  requestPermission: (params) => this.handleRequestPermission(params),
2224
+ unstable_createElicitation: (params) => this.handleCreateElicitation(params),
1618
2225
  sessionUpdate: (params) => this.handleSessionUpdate(params),
1619
2226
  readTextFile: async (params) => {
2227
+ this.notePromptActivity('host_tool:readTextFile');
1620
2228
  const resolvedPath = path.isAbsolute(params.path)
1621
2229
  ? params.path
1622
2230
  : path.resolve(this.projectPath, params.path);
@@ -1626,6 +2234,7 @@ export class AcpClient {
1626
2234
  };
1627
2235
  },
1628
2236
  writeTextFile: async (params) => {
2237
+ this.notePromptActivity('host_tool:writeTextFile');
1629
2238
  const resolvedPath = path.isAbsolute(params.path)
1630
2239
  ? params.path
1631
2240
  : path.resolve(this.projectPath, params.path);
@@ -1634,6 +2243,7 @@ export class AcpClient {
1634
2243
  return {};
1635
2244
  },
1636
2245
  createTerminal: async (params) => {
2246
+ this.notePromptActivity('host_tool:createTerminal');
1637
2247
  const terminalId = `terminal:${randomUUID()}`;
1638
2248
  const terminal = new LocalTerminalProcess({
1639
2249
  sessionId: params.sessionId,
@@ -1647,12 +2257,15 @@ export class AcpClient {
1647
2257
  return { terminalId };
1648
2258
  },
1649
2259
  terminalOutput: async (params) => {
2260
+ this.notePromptActivity('host_tool:terminalOutput');
1650
2261
  return this.terminalProcesses.get(params.terminalId)?.currentOutput() || { output: '' };
1651
2262
  },
1652
2263
  waitForTerminalExit: async (params) => {
2264
+ this.notePromptActivity('host_tool:waitForTerminalExit');
1653
2265
  return this.terminalProcesses.get(params.terminalId)?.waitForExit() || {};
1654
2266
  },
1655
2267
  killTerminal: async (params) => {
2268
+ this.notePromptActivity('host_tool:killTerminal');
1656
2269
  const terminal = this.terminalProcesses.get(params.terminalId);
1657
2270
  if (terminal) {
1658
2271
  await terminal.kill();
@@ -1660,6 +2273,7 @@ export class AcpClient {
1660
2273
  return {};
1661
2274
  },
1662
2275
  releaseTerminal: async (params) => {
2276
+ this.notePromptActivity('host_tool:releaseTerminal');
1663
2277
  const terminal = this.terminalProcesses.get(params.terminalId);
1664
2278
  if (terminal) {
1665
2279
  await terminal.release();
@@ -1669,37 +2283,75 @@ export class AcpClient {
1669
2283
  }
1670
2284
  }), stream);
1671
2285
 
1672
- this.initializeResult = await this.connection.initialize({
1673
- protocolVersion: PROTOCOL_VERSION,
1674
- clientCapabilities: {
1675
- fs: {
1676
- readTextFile: true,
1677
- writeTextFile: true
2286
+ try {
2287
+ this.initializeResult = await withAcpOperationTimeout(
2288
+ this.connection.initialize({
2289
+ protocolVersion: PROTOCOL_VERSION,
2290
+ clientCapabilities: {
2291
+ fs: {
2292
+ readTextFile: true,
2293
+ writeTextFile: true
2294
+ },
2295
+ elicitation: {
2296
+ form: {}
2297
+ },
2298
+ terminal: true
2299
+ },
2300
+ clientInfo: {
2301
+ name: '@axhub/genie',
2302
+ title: 'Axhub Genie',
2303
+ version: process.env.APP_VERSION || 'unknown'
2304
+ }
2305
+ }),
2306
+ {
2307
+ agentKey: this.agentKey,
2308
+ operation: 'initialize',
2309
+ timeoutMs: this.startTimeoutMs,
2310
+ code: START_TIMEOUT_ERROR_CODE
1678
2311
  },
1679
- terminal: true
1680
- },
1681
- clientInfo: {
1682
- name: '@axhub/genie',
1683
- title: 'Axhub Genie',
1684
- version: process.env.APP_VERSION || 'unknown'
1685
- }
1686
- });
2312
+ );
2313
+ } catch (error) {
2314
+ await this.dispose().catch(() => {});
2315
+ throw error;
2316
+ }
1687
2317
 
1688
2318
  return this.initializeResult;
1689
2319
  }
1690
2320
 
1691
2321
  async createSession() {
1692
- const response = await this.connection.newSession({
1693
- cwd: this.projectPath,
1694
- mcpServers: []
1695
- });
2322
+ const response = await withAcpOperationTimeout(
2323
+ this.connection.newSession({
2324
+ cwd: this.projectPath,
2325
+ mcpServers: []
2326
+ }),
2327
+ {
2328
+ agentKey: this.agentKey,
2329
+ operation: 'session/new',
2330
+ timeoutMs: this.sessionTimeoutMs,
2331
+ code: SESSION_TIMEOUT_ERROR_CODE
2332
+ }
2333
+ );
1696
2334
 
1697
2335
  this.sessionId = response.sessionId;
1698
2336
  this.attachWriter(this.writer);
1699
- const normalizedModes = normalizeSessionModeState(response?.modes);
2337
+ const normalizedModes = normalizeAcpModeState(response?.modes);
2338
+ const normalizedConfigOptions = this.noteConfigOptions(response?.configOptions || []);
2339
+ const normalizedTokenUsage = normalizeAcpTokenUsage(response?.tokenUsage || null);
2340
+ const normalizedAvailableCommands = normalizeAcpAvailableCommands(response?.availableCommands || []);
1700
2341
  this.emitPayload({
1701
2342
  type: 'session-created',
1702
- modes: normalizedModes
2343
+ modes: normalizedModes,
2344
+ configOptions: normalizedConfigOptions,
2345
+ tokenUsage: normalizedTokenUsage,
2346
+ availableCommands: normalizedAvailableCommands,
2347
+ capabilitySnapshot: buildAcpCapabilities({
2348
+ configOptions: normalizedConfigOptions,
2349
+ modeState: normalizedModes,
2350
+ tokenUsage: normalizedTokenUsage,
2351
+ availableCommands: normalizedAvailableCommands,
2352
+ provider: this.agentKey,
2353
+ source: 'session-created'
2354
+ })
1703
2355
  });
1704
2356
 
1705
2357
  if (normalizedModes) {
@@ -1713,10 +2365,176 @@ export class AcpClient {
1713
2365
  );
1714
2366
  }
1715
2367
 
2368
+ if (normalizedConfigOptions.length > 0) {
2369
+ this.emitConversationEvent(
2370
+ CONVERSATION_EVENT_KINDS.CONFIG_OPTION_UPDATE,
2371
+ { configOptions: normalizedConfigOptions },
2372
+ {
2373
+ timestamp: nowIso(),
2374
+ sourceType: 'acp-session-created-config-options'
2375
+ }
2376
+ );
2377
+ }
2378
+
2379
+ if (normalizedAvailableCommands.length > 0) {
2380
+ this.emitConversationEvent(
2381
+ CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
2382
+ { availableCommands: normalizedAvailableCommands },
2383
+ {
2384
+ timestamp: nowIso(),
2385
+ sourceType: 'acp-session-created-available-commands'
2386
+ }
2387
+ );
2388
+ }
2389
+
2390
+ if (normalizedTokenUsage) {
2391
+ this.emitConversationEvent(
2392
+ CONVERSATION_EVENT_KINDS.USAGE_UPDATE,
2393
+ normalizedTokenUsage,
2394
+ {
2395
+ timestamp: nowIso(),
2396
+ sourceType: 'acp-session-created-usage'
2397
+ }
2398
+ );
2399
+ }
2400
+
2401
+ if (this.model) {
2402
+ await this.setModel(this.model);
2403
+ }
2404
+
2405
+ if (this.modeId) {
2406
+ await this.setMode(this.modeId);
2407
+ }
2408
+
2409
+ if (this.thoughtLevel) {
2410
+ await this.setConfigOption(this.resolveConfigOptionId('thought_level'), this.thoughtLevel);
2411
+ }
2412
+
2413
+ return response;
2414
+ }
2415
+
2416
+ canResumeSession() {
2417
+ const resumeCapability = this.initializeResult?.agentCapabilities?.sessionCapabilities?.resume;
2418
+ return resumeCapability != null && resumeCapability !== false;
2419
+ }
2420
+
2421
+ canListSessions() {
2422
+ const listCapability = this.initializeResult?.agentCapabilities?.sessionCapabilities?.list;
2423
+ return listCapability != null && listCapability !== false;
2424
+ }
2425
+
2426
+ canLoadSession() {
2427
+ return this.initializeResult?.agentCapabilities?.loadSession === true;
2428
+ }
2429
+
2430
+ canCloseSession() {
2431
+ const closeCapability = this.initializeResult?.agentCapabilities?.sessionCapabilities?.close;
2432
+ return closeCapability != null && closeCapability !== false;
2433
+ }
2434
+
2435
+ emitRestoredSessionState(response, sourceLabel) {
2436
+ const normalizedModes = normalizeAcpModeState(response?.modes);
2437
+ const normalizedConfigOptions = this.noteConfigOptions(response?.configOptions || []);
2438
+ const normalizedTokenUsage = normalizeAcpTokenUsage(response?.tokenUsage || null);
2439
+ const normalizedAvailableCommands = normalizeAcpAvailableCommands(response?.availableCommands || []);
2440
+
2441
+ if (normalizedModes) {
2442
+ this.emitConversationEvent(
2443
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
2444
+ normalizedModes,
2445
+ {
2446
+ timestamp: nowIso(),
2447
+ sourceType: `acp-session-${sourceLabel}-modes`
2448
+ }
2449
+ );
2450
+ }
2451
+
2452
+ if (normalizedConfigOptions.length > 0) {
2453
+ this.emitConversationEvent(
2454
+ CONVERSATION_EVENT_KINDS.CONFIG_OPTION_UPDATE,
2455
+ { configOptions: normalizedConfigOptions },
2456
+ {
2457
+ timestamp: nowIso(),
2458
+ sourceType: `acp-session-${sourceLabel}-config-options`
2459
+ }
2460
+ );
2461
+ }
2462
+
2463
+ if (normalizedAvailableCommands.length > 0) {
2464
+ this.emitConversationEvent(
2465
+ CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
2466
+ { availableCommands: normalizedAvailableCommands },
2467
+ {
2468
+ timestamp: nowIso(),
2469
+ sourceType: `acp-session-${sourceLabel}-available-commands`
2470
+ }
2471
+ );
2472
+ }
2473
+
2474
+ if (normalizedTokenUsage) {
2475
+ this.emitConversationEvent(
2476
+ CONVERSATION_EVENT_KINDS.USAGE_UPDATE,
2477
+ normalizedTokenUsage,
2478
+ {
2479
+ timestamp: nowIso(),
2480
+ sourceType: `acp-session-${sourceLabel}-usage`
2481
+ }
2482
+ );
2483
+ }
2484
+ }
2485
+
2486
+ emitLoadedSessionIdleState() {
2487
+ if (this.pendingPermissionIds.size > 0 || this.pendingElicitationIds.size > 0) {
2488
+ return;
2489
+ }
2490
+
2491
+ this.flushOpenTextStreams(nowIso());
2492
+ this.emitConversationEvent(
2493
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
2494
+ { state: 'idle' },
2495
+ {
2496
+ timestamp: nowIso(),
2497
+ sourceType: 'acp-session-loaded-idle'
2498
+ }
2499
+ );
2500
+ }
2501
+
2502
+ async applyPostSessionRestoreConfiguration() {
1716
2503
  if (this.model) {
1717
2504
  await this.setModel(this.model);
1718
2505
  }
1719
2506
 
2507
+ if (this.modeId) {
2508
+ await this.setMode(this.modeId);
2509
+ }
2510
+
2511
+ if (this.thoughtLevel) {
2512
+ await this.setConfigOption(this.resolveConfigOptionId('thought_level'), this.thoughtLevel);
2513
+ }
2514
+ }
2515
+
2516
+ async resumeSession(sessionId) {
2517
+ this.sessionId = String(sessionId || '').trim();
2518
+ this.attachWriter(this.writer);
2519
+
2520
+ if (typeof this.connection?.resumeSession !== 'function') {
2521
+ throw new Error('ACP connection does not support session/resume');
2522
+ }
2523
+
2524
+ const response = await this.connection.resumeSession({
2525
+ cwd: this.projectPath,
2526
+ mcpServers: [],
2527
+ sessionId: this.sessionId
2528
+ });
2529
+
2530
+ if (typeof response?.sessionId === 'string' && response.sessionId.trim()) {
2531
+ this.sessionId = response.sessionId.trim();
2532
+ this.attachWriter(this.writer);
2533
+ }
2534
+
2535
+ this.emitRestoredSessionState(response, 'resumed');
2536
+ await this.applyPostSessionRestoreConfiguration();
2537
+
1720
2538
  return response;
1721
2539
  }
1722
2540
 
@@ -1724,6 +2542,7 @@ export class AcpClient {
1724
2542
  this.sessionId = String(sessionId || '').trim();
1725
2543
  this.attachWriter(this.writer);
1726
2544
  this.suppressSessionUpdates = Boolean(suppressReplayUpdates);
2545
+ this.isLoadingSessionReplay = true;
1727
2546
  try {
1728
2547
  const response = await this.connection.loadSession({
1729
2548
  cwd: this.projectPath,
@@ -1731,28 +2550,35 @@ export class AcpClient {
1731
2550
  sessionId: this.sessionId
1732
2551
  });
1733
2552
 
1734
- const normalizedModes = normalizeSessionModeState(response?.modes);
1735
- if (normalizedModes) {
1736
- this.emitConversationEvent(
1737
- CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1738
- normalizedModes,
1739
- {
1740
- timestamp: nowIso(),
1741
- sourceType: 'acp-session-loaded-modes'
1742
- }
1743
- );
1744
- }
1745
-
1746
- if (this.model) {
1747
- await this.setModel(this.model);
1748
- }
2553
+ this.flushOpenTextStreams(nowIso());
2554
+ this.isLoadingSessionReplay = false;
2555
+ this.emitRestoredSessionState(response, 'loaded');
2556
+ this.emitLoadedSessionIdleState();
2557
+ await this.applyPostSessionRestoreConfiguration();
1749
2558
 
1750
2559
  return response;
1751
2560
  } finally {
1752
2561
  this.suppressSessionUpdates = false;
2562
+ this.isLoadingSessionReplay = false;
1753
2563
  }
1754
2564
  }
1755
2565
 
2566
+ async listSessions({ cursor = null, projectPath = this.projectPath } = {}) {
2567
+ if (typeof this.connection?.listSessions !== 'function') {
2568
+ return { sessions: [], nextCursor: null };
2569
+ }
2570
+
2571
+ const response = await this.connection.listSessions({
2572
+ cwd: projectPath || this.projectPath,
2573
+ cursor
2574
+ });
2575
+
2576
+ return {
2577
+ sessions: Array.isArray(response?.sessions) ? response.sessions : [],
2578
+ nextCursor: response?.nextCursor || null
2579
+ };
2580
+ }
2581
+
1756
2582
  async setModel(modelId) {
1757
2583
  if (!modelId || !this.sessionId || !this.connection) {
1758
2584
  return;
@@ -1794,6 +2620,25 @@ export class AcpClient {
1794
2620
  }
1795
2621
  }
1796
2622
 
2623
+ async setConfigOption(configId, value) {
2624
+ const normalizedConfigId = String(configId || '').trim();
2625
+ if (!normalizedConfigId || value == null || !this.sessionId || !this.connection) {
2626
+ return false;
2627
+ }
2628
+
2629
+ if (typeof this.connection.setSessionConfigOption !== 'function') {
2630
+ return false;
2631
+ }
2632
+
2633
+ await this.connection.setSessionConfigOption({
2634
+ sessionId: this.sessionId,
2635
+ configId: normalizedConfigId,
2636
+ value
2637
+ });
2638
+
2639
+ return true;
2640
+ }
2641
+
1797
2642
  async setMode(modeId) {
1798
2643
  const normalizedModeId = String(modeId || '').trim();
1799
2644
  if (!normalizedModeId || !this.sessionId || !this.connection?.setSessionMode) {
@@ -1807,6 +2652,35 @@ export class AcpClient {
1807
2652
  return true;
1808
2653
  }
1809
2654
 
2655
+ async requestPromptCancellation() {
2656
+ const normalizedSessionId = String(this.sessionId || '').trim();
2657
+ if (!normalizedSessionId || typeof this.connection?.cancel !== 'function') {
2658
+ return false;
2659
+ }
2660
+
2661
+ for (const requestId of this.pendingPermissionIds) {
2662
+ AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
2663
+ }
2664
+
2665
+ for (const requestId of this.pendingElicitationIds) {
2666
+ AcpClient.resolveElicitationRequest(requestId, { action: 'cancel' });
2667
+ }
2668
+
2669
+ let timeoutId = null;
2670
+ try {
2671
+ return await Promise.race([
2672
+ this.connection.cancel({ sessionId: normalizedSessionId }).then(() => true, () => false),
2673
+ new Promise((resolve) => {
2674
+ timeoutId = setTimeout(() => resolve(false), DEFAULT_CLOSE_GRACE_MS);
2675
+ })
2676
+ ]);
2677
+ } finally {
2678
+ if (timeoutId) {
2679
+ clearTimeout(timeoutId);
2680
+ }
2681
+ }
2682
+ }
2683
+
1810
2684
  async sendPrompt(promptText, attachments = {}) {
1811
2685
  this.resetTurnState();
1812
2686
  this.hasReportedPromptFatalError = false;
@@ -1815,6 +2689,10 @@ export class AcpClient {
1815
2689
  const clientRequestId = typeof attachments?.clientRequestId === 'string' && attachments.clientRequestId.trim()
1816
2690
  ? attachments.clientRequestId.trim()
1817
2691
  : null;
2692
+ this.activePromptClientRequestId = clientRequestId;
2693
+ this.activePromptText = promptPayload.text;
2694
+ this.isReplayingPromptHistory = false;
2695
+ this.hasObservedActivePromptEcho = false;
1818
2696
 
1819
2697
  this.emitConversationEvent(
1820
2698
  CONVERSATION_EVENT_KINDS.USER_MESSAGE,
@@ -1854,6 +2732,28 @@ export class AcpClient {
1854
2732
  };
1855
2733
  this.pendingPromptDiagnostic = diagnosticGuard;
1856
2734
  });
2735
+ let turnCompletionResolve = null;
2736
+ const turnCompletionPromise = new Promise((resolve) => {
2737
+ turnCompletionResolve = resolve;
2738
+ this.pendingPromptTurnCompletion = resolve;
2739
+ });
2740
+ let timeoutGuard = null;
2741
+ const promptTimeoutMs = this.promptTimeoutMs;
2742
+ const timeoutPromise = promptTimeoutMs > 0
2743
+ ? new Promise((_, reject) => {
2744
+ timeoutGuard = {
2745
+ handled: false,
2746
+ reject
2747
+ };
2748
+ this.startPromptActivityWatchdog((error) => {
2749
+ if (timeoutGuard.handled) {
2750
+ return;
2751
+ }
2752
+ timeoutGuard.handled = true;
2753
+ reject(error);
2754
+ }, promptTimeoutMs);
2755
+ })
2756
+ : null;
1857
2757
 
1858
2758
  const promptPromise = this.connection.prompt({
1859
2759
  sessionId: this.sessionId,
@@ -1862,7 +2762,10 @@ export class AcpClient {
1862
2762
  promptPromise.catch(() => {});
1863
2763
 
1864
2764
  try {
1865
- const response = await Promise.race([promptPromise, diagnosticPromise]);
2765
+ const racedPromises = timeoutPromise
2766
+ ? [promptPromise, diagnosticPromise, turnCompletionPromise, timeoutPromise]
2767
+ : [promptPromise, diagnosticPromise, turnCompletionPromise];
2768
+ const response = await Promise.race(racedPromises);
1866
2769
  const finishedAt = nowIso();
1867
2770
  this.flushOpenTextStreams(finishedAt);
1868
2771
  this.emitConversationEvent(
@@ -1884,17 +2787,28 @@ export class AcpClient {
1884
2787
 
1885
2788
  return response;
1886
2789
  } catch (error) {
2790
+ if (error?.code === PROMPT_TIMEOUT_ERROR_CODE) {
2791
+ await this.requestPromptCancellation();
2792
+ }
1887
2793
  this.reportPromptFailure(error);
1888
2794
  throw error;
1889
2795
  } finally {
2796
+ this.stopPromptActivityWatchdog();
1890
2797
  this.isPromptInFlight = false;
2798
+ this.activePromptClientRequestId = null;
2799
+ this.activePromptText = null;
2800
+ this.isReplayingPromptHistory = false;
2801
+ this.hasObservedActivePromptEcho = false;
1891
2802
  if (this.pendingPromptDiagnostic === diagnosticGuard) {
1892
2803
  this.pendingPromptDiagnostic = null;
1893
2804
  }
2805
+ if (this.pendingPromptTurnCompletion === turnCompletionResolve) {
2806
+ this.pendingPromptTurnCompletion = null;
2807
+ }
1894
2808
  }
1895
2809
  }
1896
2810
 
1897
- async cancel(sessionId = this.sessionId) {
2811
+ async cancel(sessionId = this.sessionId, { disposeOnTimeout = false, cancelGraceMs = DEFAULT_CANCEL_GRACE_MS } = {}) {
1898
2812
  const normalizedSessionId = String(sessionId || this.sessionId || '').trim();
1899
2813
  if (!normalizedSessionId) {
1900
2814
  return false;
@@ -1904,25 +2818,66 @@ export class AcpClient {
1904
2818
  AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
1905
2819
  }
1906
2820
 
2821
+ for (const requestId of this.pendingElicitationIds) {
2822
+ AcpClient.resolveElicitationRequest(requestId, { action: 'cancel' });
2823
+ }
2824
+
2825
+ let timeoutId = null;
1907
2826
  try {
1908
- await this.connection.cancel({
1909
- sessionId: normalizedSessionId
1910
- });
2827
+ const cancelled = await Promise.race([
2828
+ this.connection.cancel({
2829
+ sessionId: normalizedSessionId
2830
+ }).then(() => true, () => false),
2831
+ new Promise((resolve) => {
2832
+ timeoutId = setTimeout(() => resolve(false), normalizePromptTimeoutMs(cancelGraceMs, DEFAULT_CANCEL_GRACE_MS));
2833
+ })
2834
+ ]);
2835
+ if (!cancelled && disposeOnTimeout) {
2836
+ await this.dispose().catch(() => {});
2837
+ }
1911
2838
  this.emitPayload({
1912
2839
  type: 'session-aborted',
1913
- success: true
2840
+ success: cancelled
1914
2841
  });
1915
- return true;
2842
+ return cancelled;
1916
2843
  } catch {
2844
+ if (disposeOnTimeout) {
2845
+ await this.dispose().catch(() => {});
2846
+ }
1917
2847
  return false;
2848
+ } finally {
2849
+ if (timeoutId) {
2850
+ clearTimeout(timeoutId);
2851
+ }
1918
2852
  }
1919
2853
  }
1920
2854
 
1921
- async close() {
2855
+ async closeSession(sessionId = this.sessionId) {
2856
+ const normalizedSessionId = String(sessionId || this.sessionId || '').trim();
2857
+ if (!normalizedSessionId || !this.canCloseSession() || typeof this.connection?.closeSession !== 'function') {
2858
+ return false;
2859
+ }
2860
+
2861
+ try {
2862
+ await this.connection.closeSession({
2863
+ sessionId: normalizedSessionId
2864
+ });
2865
+ return true;
2866
+ } catch (error) {
2867
+ console.warn(`[ACP:${this.agentKey}] Failed to close session ${normalizedSessionId}: ${error.message}`);
2868
+ return false;
2869
+ }
2870
+ }
2871
+
2872
+ async dispose() {
1922
2873
  for (const requestId of this.pendingPermissionIds) {
1923
2874
  AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
1924
2875
  }
1925
2876
 
2877
+ for (const requestId of this.pendingElicitationIds) {
2878
+ AcpClient.resolveElicitationRequest(requestId, { action: 'cancel' });
2879
+ }
2880
+
1926
2881
  for (const [terminalId, terminal] of this.terminalProcesses.entries()) {
1927
2882
  try {
1928
2883
  await terminal.release();
@@ -1935,27 +2890,13 @@ export class AcpClient {
1935
2890
  this.childProcess.stdin.end();
1936
2891
  }
1937
2892
 
1938
- const closed = await new Promise((resolve) => {
1939
- let settled = false;
1940
- const finalize = (value) => {
1941
- if (settled) {
1942
- return;
1943
- }
1944
- settled = true;
1945
- resolve(value);
1946
- };
1947
-
1948
- const timer = setTimeout(() => finalize(false), DEFAULT_CLOSE_GRACE_MS);
1949
- this.childProcess.once('close', () => {
1950
- clearTimeout(timer);
1951
- finalize(true);
1952
- });
1953
- });
1954
-
2893
+ let closed = await waitForChildProcessClose(this.childProcess);
1955
2894
  if (!closed) {
1956
- try {
1957
- this.childProcess.kill('SIGTERM');
1958
- } catch {}
2895
+ terminateChildProcessTree(this.childProcess, 'SIGTERM');
2896
+ closed = await waitForChildProcessClose(this.childProcess);
2897
+ }
2898
+ if (!closed) {
2899
+ terminateChildProcessTree(this.childProcess, 'SIGKILL');
1959
2900
  }
1960
2901
  }
1961
2902
 
@@ -1971,6 +2912,11 @@ export class AcpClient {
1971
2912
  this.initializeResult = null;
1972
2913
  }
1973
2914
 
2915
+ async close() {
2916
+ await this.closeSession().catch(() => false);
2917
+ await this.dispose();
2918
+ }
2919
+
1974
2920
  static resolvePermissionRequest(requestId, decision = {}) {
1975
2921
  const pending = pendingPermissionRequests.get(requestId);
1976
2922
  if (!pending) {
@@ -1980,6 +2926,16 @@ export class AcpClient {
1980
2926
  pending.resolve(choosePermissionResponse(pending.request, decision));
1981
2927
  return true;
1982
2928
  }
2929
+
2930
+ static resolveElicitationRequest(requestId, decision = {}) {
2931
+ const pending = pendingElicitationRequests.get(requestId);
2932
+ if (!pending) {
2933
+ return false;
2934
+ }
2935
+
2936
+ pending.resolve(normalizeElicitationResponse(decision));
2937
+ return true;
2938
+ }
1983
2939
  }
1984
2940
 
1985
2941
  export { normalizeTerminalCommandForSpawn };