@axhub/genie 0.2.6 → 0.2.8

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 (102) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-CTKZtqB1.js +460 -0
  3. package/dist/assets/{ReviewApp-BEicSBzW.js → ReviewApp-DM6BNAzR.js} +1 -1
  4. package/dist/assets/{_basePickBy-DkiHsp3X.js → _basePickBy-CqJbRZ9y.js} +1 -1
  5. package/dist/assets/{_baseUniq-7ElXb2sX.js → _baseUniq-BS8YH8jO.js} +1 -1
  6. package/dist/assets/{arc-CEsS3MdK.js → arc-BBmKEN-S.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BubZ7T3U.js → architectureDiagram-2XIMDMQ5-N5lcb82R.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Cza6M6Ht.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-jhjtOQ12.js → c4Diagram-IC4MRINW-BTKlkXI9.js} +1 -1
  10. package/dist/assets/channel-1oJBvF-0.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB--HkodwbY.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-CyBuez4e.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-CuzG4iAl.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-BNi8S861.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-D817O-GT.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-DyujyOvx.js → chunk-NQ4KR5QH-CM50ygWP.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-VMEn-zxh.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-CQHHFLvx.js → chunk-WL4C6EOR-Cm9nQrsr.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
  21. package/dist/assets/clone-CinxIlEu.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-qykDd54p.js → cose-bilkent-S5V4N54A-Ccp_p0JZ.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-Bqp7DjEa.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BKtx468K.js → diagram-E7M64L7V-CeNVmFUp.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2--fHfW6V2.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-D1kQI5RB.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-DT9YzdNw.js → erDiagram-INFDFZHY-B8R5vwhd.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DWeNr4yg.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK--IgwcUhI.js → ganttDiagram-A5KZAMGK-DOu3hSNa.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-B5a8UWjN.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
  31. package/dist/assets/{graph-Cw1rYoD9.js → graph-D11wiwHo.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-BCxJHuqY.js → highlighted-body-TPN3WLV5-Babpthg-.js} +1 -1
  33. package/dist/assets/index-DFxzgWoO.js +2 -0
  34. package/dist/assets/index-YCFGDVKw.css +1 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-D2u70rhN.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-Cl8yrezU.js → ishikawaDiagram-PHBUUO56-BEquZd3E.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-ddP0AMU9.js → journeyDiagram-4ABVD52K-BfemGz7f.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-DbVt0v29.js → kanban-definition-K7BYSVSG-CWja3mln.js} +1 -1
  39. package/dist/assets/{layout-W_tRx4UV.js → layout-BLUNf-PJ.js} +1 -1
  40. package/dist/assets/{linear-CcMb2ay-.js → linear-DukIV_Xv.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-BBJqt8pT.js → mermaid-O7DHMXV3-SgtM28qI.js} +265 -215
  42. package/dist/assets/{mindmap-definition-YRQLILUH-BGhZa7Na.js → mindmap-definition-YRQLILUH-4UjqXITU.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CDyJaACv.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-BSYuqf0Q.js → quadrantDiagram-337W2JSQ-D60m8V8r.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-Cfi9YX9H.js → requirementDiagram-Z7DCOOCP-zqh9jBVf.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-Di1ShaMF.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-CYTTG38e.js → sequenceDiagram-2WXFIKYE-7BReFd0L.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-CVZYMqyW.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-B1sdb5mK.js → timeline-definition-YZTLITO2-CTVllFgr.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CGG4gx3C.js → treemap-KZPCXAKY-BtyxboJZ.js} +1 -1
  52. package/dist/assets/{vennDiagram-LZ73GAT5-Dds37L2k.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +1 -1
  53. package/dist/assets/{xychartDiagram-JWTSCODW-C8QKSyRR.js → xychartDiagram-JWTSCODW-eRk-39YO.js} +1 -1
  54. package/dist/index.html +2 -2
  55. package/package.json +35 -33
  56. package/server/_legacy-providers/README.md +30 -0
  57. package/server/_legacy-providers/claude-sdk.js +956 -0
  58. package/server/_legacy-providers/gemini-cli.js +368 -0
  59. package/server/_legacy-providers/openai-codex.js +705 -0
  60. package/server/_legacy-providers/opencode-cli.js +674 -0
  61. package/server/acp-runtime/client.js +1872 -0
  62. package/server/acp-runtime/index.js +408 -0
  63. package/server/acp-runtime/registry.js +45 -0
  64. package/server/acp-runtime/session-store.js +254 -0
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +22 -80
  66. package/server/claude-sdk.js +24 -946
  67. package/server/cli.js +140 -2
  68. package/server/external-agent/service.js +52 -63
  69. package/server/gemini-cli.js +21 -360
  70. package/server/index.js +133 -58
  71. package/server/openai-codex.js +19 -695
  72. package/server/opencode-cli.js +68 -640
  73. package/server/projects.js +128 -85
  74. package/server/routes/agent.js +2 -0
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/git.js +3 -20
  79. package/server/routes/projects.js +45 -24
  80. package/server/routes/session-core.js +44 -10
  81. package/server/session-core/abortSession.js +2 -18
  82. package/server/session-core/eventStore.js +5 -1
  83. package/server/session-core/providerAdapters.js +98 -10
  84. package/server/session-core/providerDiscovery.js +8 -3
  85. package/server/session-core/runtimeState.js +16 -17
  86. package/server/session-core/runtimeWriter.js +19 -12
  87. package/server/utils/ccConnectManager.js +390 -0
  88. package/server/utils/ccConnectState.js +575 -0
  89. package/server/utils/resolveCommandPath.js +71 -0
  90. package/server/utils/workspaceRoots.js +154 -0
  91. package/shared/conversationEvents.js +347 -10
  92. package/dist/assets/App-CYTE30Cf.js +0 -484
  93. package/dist/assets/channel-RmqTALN0.js +0 -1
  94. package/dist/assets/classDiagram-VBA2DB6C-wvVV1ggz.js +0 -1
  95. package/dist/assets/classDiagram-v2-RAHNMMFH-wvVV1ggz.js +0 -1
  96. package/dist/assets/clone-oT5aWXpf.js +0 -1
  97. package/dist/assets/index-CBuAXA5S.js +0 -2
  98. package/dist/assets/index-CyLWKyxy.css +0 -1
  99. package/dist/assets/stateDiagram-v2-FVOUBMTO-Bbl0b4-i.js +0 -1
  100. package/server/cli.test.js +0 -76
  101. package/server/external-agent/service.test.js +0 -53
  102. package/server/external-agent/ws.test.js +0 -289
@@ -0,0 +1,1872 @@
1
+ import fs from 'fs/promises';
2
+ import { randomUUID } from 'crypto';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { Readable, Writable } from 'stream';
6
+
7
+ import {
8
+ ClientSideConnection,
9
+ PROTOCOL_VERSION,
10
+ ndJsonStream
11
+ } from '@agentclientprotocol/sdk';
12
+
13
+ import {
14
+ CONVERSATION_EVENT_KINDS,
15
+ createConversationEvent
16
+ } from '../../shared/conversationEvents.js';
17
+ import { parseDataUrl } from '../utils/agentImages.js';
18
+ import { spawnCommand } from '../utils/spawnCommand.js';
19
+ import { resolveCommandPath } from '../utils/resolveCommandPath.js';
20
+ import { resolveAgentCommand } from './registry.js';
21
+
22
+ const DEFAULT_TERMINAL_OUTPUT_LIMIT = 256 * 1024;
23
+ const DEFAULT_CLOSE_GRACE_MS = 1000;
24
+ const DEFAULT_TERMINAL_MAX_LIFETIME_MS = parseInt(process.env.ACP_TERMINAL_MAX_LIFETIME_MS, 10) || (5 * 60 * 1000);
25
+ const FILTERED_CHILD_PROCESS_ENV_KEYS = new Set(['NODE_OPTIONS']);
26
+
27
+ const pendingPermissionRequests = new Map();
28
+
29
+ function getDisabledClaudeAcpMcpServers() {
30
+ const configured = typeof process.env.CLAUDE_SDK_DISABLED_MCP_SERVERS === 'string'
31
+ ? process.env.CLAUDE_SDK_DISABLED_MCP_SERVERS
32
+ : 'pencil';
33
+
34
+ return new Set(
35
+ configured
36
+ .split(',')
37
+ .map((value) => value.trim())
38
+ .filter(Boolean)
39
+ );
40
+ }
41
+
42
+ function cloneJsonValue(value) {
43
+ return JSON.parse(JSON.stringify(value));
44
+ }
45
+
46
+ function filterClaudeMcpServers(config, disabledServers) {
47
+ if (!config || typeof config !== 'object' || Array.isArray(config) || disabledServers.size === 0) {
48
+ return { changed: false, config };
49
+ }
50
+
51
+ const nextConfig = cloneJsonValue(config);
52
+ let changed = false;
53
+
54
+ if (nextConfig.mcpServers && typeof nextConfig.mcpServers === 'object' && !Array.isArray(nextConfig.mcpServers)) {
55
+ for (const serverName of disabledServers) {
56
+ if (Object.prototype.hasOwnProperty.call(nextConfig.mcpServers, serverName)) {
57
+ delete nextConfig.mcpServers[serverName];
58
+ changed = true;
59
+ }
60
+ }
61
+ }
62
+
63
+ if (nextConfig.projects && typeof nextConfig.projects === 'object' && !Array.isArray(nextConfig.projects)) {
64
+ for (const projectState of Object.values(nextConfig.projects)) {
65
+ if (!projectState || typeof projectState !== 'object' || Array.isArray(projectState)) {
66
+ continue;
67
+ }
68
+
69
+ if (projectState.mcpServers && typeof projectState.mcpServers === 'object' && !Array.isArray(projectState.mcpServers)) {
70
+ for (const serverName of disabledServers) {
71
+ if (Object.prototype.hasOwnProperty.call(projectState.mcpServers, serverName)) {
72
+ delete projectState.mcpServers[serverName];
73
+ changed = true;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ return {
81
+ changed,
82
+ config: nextConfig
83
+ };
84
+ }
85
+
86
+ async function prepareClaudeSpawnEnvironment() {
87
+ const disabledServers = getDisabledClaudeAcpMcpServers();
88
+ if (disabledServers.size === 0) {
89
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
90
+ }
91
+
92
+ const homeDir = os.homedir();
93
+ const claudeConfigPath = path.join(homeDir, '.claude.json');
94
+ let rawConfig;
95
+
96
+ try {
97
+ rawConfig = JSON.parse(await fs.readFile(claudeConfigPath, 'utf8'));
98
+ } catch {
99
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
100
+ }
101
+
102
+ const { changed, config } = filterClaudeMcpServers(rawConfig, disabledServers);
103
+ if (!changed) {
104
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
105
+ }
106
+
107
+ const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'axhub-genie-claude-home-'));
108
+ const sourceClaudeDir = path.join(homeDir, '.claude');
109
+ const targetClaudeDir = path.join(tempHome, '.claude');
110
+
111
+ try {
112
+ await fs.symlink(sourceClaudeDir, targetClaudeDir, process.platform === 'win32' ? 'junction' : 'dir');
113
+ } catch {
114
+ // Claude can still use the temporary config even if the data directory symlink is unavailable.
115
+ }
116
+
117
+ await fs.writeFile(
118
+ path.join(tempHome, '.claude.json'),
119
+ `${JSON.stringify(config, null, 2)}\n`,
120
+ 'utf8'
121
+ );
122
+
123
+ return {
124
+ env: sanitizeProcessEnv(process.env, {
125
+ HOME: tempHome,
126
+ USERPROFILE: tempHome
127
+ }),
128
+ cleanup: async () => {
129
+ await fs.rm(tempHome, { recursive: true, force: true });
130
+ }
131
+ };
132
+ }
133
+
134
+ async function prepareSpawnEnvironment(agentKey) {
135
+ if (String(agentKey || '').trim().toLowerCase() === 'claude') {
136
+ return prepareClaudeSpawnEnvironment();
137
+ }
138
+
139
+ return { env: sanitizeProcessEnv(process.env), cleanup: null };
140
+ }
141
+
142
+ function nowIso() {
143
+ return new Date().toISOString();
144
+ }
145
+
146
+ function normalizeText(value) {
147
+ return typeof value === 'string' ? value : String(value ?? '');
148
+ }
149
+
150
+ function normalizeComparableValue(value) {
151
+ try {
152
+ return JSON.stringify(value ?? null);
153
+ } catch {
154
+ return String(value ?? '');
155
+ }
156
+ }
157
+
158
+ function sanitizeProcessEnv(baseEnv = process.env, overrides = {}) {
159
+ const sanitized = {};
160
+
161
+ Object.entries(baseEnv || {}).forEach(([key, value]) => {
162
+ if (!key || value == null || FILTERED_CHILD_PROCESS_ENV_KEYS.has(key)) {
163
+ return;
164
+ }
165
+ sanitized[key] = String(value);
166
+ });
167
+
168
+ Object.entries(overrides || {}).forEach(([key, value]) => {
169
+ if (!key || value == null) {
170
+ return;
171
+ }
172
+ sanitized[key] = String(value);
173
+ });
174
+
175
+ return sanitized;
176
+ }
177
+
178
+ function normalizeTerminalCommandForSpawn(command, args = []) {
179
+ const normalizedArgs = Array.isArray(args) ? [...args] : [];
180
+ const normalizedCommand = typeof command === 'string' ? command : String(command || '');
181
+ const commandName = path.basename(normalizedCommand).toLowerCase();
182
+
183
+ if (
184
+ commandName === 'zsh'
185
+ && normalizedArgs[0] === '-lc'
186
+ && typeof normalizedArgs[1] === 'string'
187
+ && !normalizedArgs[1].includes('setopt no_nomatch')
188
+ ) {
189
+ normalizedArgs[1] = `setopt no_nomatch; ${normalizedArgs[1]}`;
190
+ }
191
+
192
+ return {
193
+ command: normalizedCommand,
194
+ args: normalizedArgs
195
+ };
196
+ }
197
+
198
+ function detectFatalAgentDiagnostic(agentKey, text) {
199
+ const normalizedAgent = String(agentKey || '').trim().toLowerCase();
200
+ const normalizedText = String(text || '').trim();
201
+
202
+ if (!normalizedText) {
203
+ return null;
204
+ }
205
+
206
+ if (normalizedAgent === 'gemini') {
207
+ const lowerText = normalizedText.toLowerCase();
208
+ const isRateLimited = (
209
+ lowerText.includes('status 429') ||
210
+ lowerText.includes('ratelimitexceeded') ||
211
+ lowerText.includes('resource_exhausted') ||
212
+ lowerText.includes('resource has been exhausted')
213
+ );
214
+
215
+ if (isRateLimited) {
216
+ return new Error('Gemini request failed because the logged-in account is currently rate limited (429 RESOURCE_EXHAUSTED). Please wait and try again later.');
217
+ }
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ function createEventMessageId(prefix, provider, sessionId, turnId = null) {
224
+ return `${prefix}:${provider}:${sessionId || 'pending'}:${turnId || randomUUID()}`;
225
+ }
226
+
227
+ function pickPermissionOption(options = [], kinds = []) {
228
+ for (const kind of kinds) {
229
+ const match = options.find((option) => option?.kind === kind);
230
+ if (match) {
231
+ return match;
232
+ }
233
+ }
234
+ return null;
235
+ }
236
+
237
+ function createSelectedPermissionResponse(optionId) {
238
+ return {
239
+ outcome: {
240
+ outcome: 'selected',
241
+ optionId
242
+ }
243
+ };
244
+ }
245
+
246
+ function createCancelledPermissionResponse() {
247
+ return {
248
+ outcome: {
249
+ outcome: 'cancelled'
250
+ }
251
+ };
252
+ }
253
+
254
+ function inferToolKind(toolCall = {}) {
255
+ if (toolCall.kind) {
256
+ return toolCall.kind;
257
+ }
258
+
259
+ const title = String(toolCall.title || '').trim().toLowerCase();
260
+ if (!title) {
261
+ return 'other';
262
+ }
263
+
264
+ if (title.includes('read') || title.includes('cat')) return 'read';
265
+ if (title.includes('search') || title.includes('find') || title.includes('grep')) return 'search';
266
+ if (title.includes('write') || title.includes('edit') || title.includes('patch')) return 'edit';
267
+ if (title.includes('delete') || title.includes('remove')) return 'delete';
268
+ if (title.includes('move') || title.includes('rename')) return 'move';
269
+ if (title.includes('run') || title.includes('bash') || title.includes('shell') || title.includes('exec')) return 'execute';
270
+ if (title.includes('fetch') || title.includes('http') || title.includes('url')) return 'fetch';
271
+ if (title.includes('think')) return 'think';
272
+
273
+ return 'other';
274
+ }
275
+
276
+ function classifyPermissionStatus(response, request) {
277
+ if (!response || response.outcome?.outcome !== 'selected') {
278
+ return 'cancelled';
279
+ }
280
+
281
+ const selected = request?.options?.find((option) => option.optionId === response.outcome.optionId);
282
+ if (!selected) {
283
+ return 'cancelled';
284
+ }
285
+
286
+ if (selected.kind === 'allow_once' || selected.kind === 'allow_always') {
287
+ return 'approved';
288
+ }
289
+
290
+ return 'denied';
291
+ }
292
+
293
+ function choosePermissionResponse(request, decision = {}) {
294
+ const options = Array.isArray(request?.options) ? request.options : [];
295
+ if (options.length === 0) {
296
+ return createCancelledPermissionResponse();
297
+ }
298
+
299
+ const explicitOutcome = decision?.outcome;
300
+ const explicitOptionId = explicitOutcome?.outcome === 'selected'
301
+ ? explicitOutcome.optionId
302
+ : decision?.optionId;
303
+
304
+ if (explicitOutcome?.outcome === 'cancelled') {
305
+ return createCancelledPermissionResponse();
306
+ }
307
+
308
+ if (explicitOptionId) {
309
+ return options.some((option) => option?.optionId === explicitOptionId)
310
+ ? createSelectedPermissionResponse(explicitOptionId)
311
+ : createCancelledPermissionResponse();
312
+ }
313
+
314
+ if (decision?.cancelled) {
315
+ return createCancelledPermissionResponse();
316
+ }
317
+
318
+ if (decision?.allow) {
319
+ const match = pickPermissionOption(
320
+ options,
321
+ decision?.rememberEntry ? ['allow_always', 'allow_once'] : ['allow_once', 'allow_always']
322
+ );
323
+ return match ? createSelectedPermissionResponse(match.optionId) : createCancelledPermissionResponse();
324
+ }
325
+
326
+ const rejectMatch = pickPermissionOption(
327
+ options,
328
+ decision?.rememberEntry ? ['reject_always', 'reject_once'] : ['reject_once', 'reject_always']
329
+ );
330
+
331
+ return rejectMatch ? createSelectedPermissionResponse(rejectMatch.optionId) : createCancelledPermissionResponse();
332
+ }
333
+
334
+ function readLines(content, line = null, limit = null) {
335
+ if (line == null && limit == null) {
336
+ return content;
337
+ }
338
+
339
+ const lines = String(content || '').split(/\r?\n/);
340
+ const startIndex = Math.max(0, Number(line || 1) - 1);
341
+ const endIndex = limit == null ? lines.length : startIndex + Math.max(0, Number(limit) || 0);
342
+ return lines.slice(startIndex, endIndex).join('\n');
343
+ }
344
+
345
+ function toContentBlockText(content) {
346
+ if (!content || typeof content !== 'object') {
347
+ return '';
348
+ }
349
+
350
+ if (content.type === 'text') {
351
+ return String(content.text || '');
352
+ }
353
+
354
+ if (content.type === 'image') {
355
+ return `[image:${content.mimeType || 'unknown'}]`;
356
+ }
357
+
358
+ if (content.type === 'resource_link') {
359
+ return String(content.uri || content.name || '');
360
+ }
361
+
362
+ if (content.type === 'resource') {
363
+ const resource = content.resource || {};
364
+ if (typeof resource.text === 'string') {
365
+ return resource.text;
366
+ }
367
+ }
368
+
369
+ if (content.type === 'audio') {
370
+ return `[audio:${content.mimeType || 'unknown'}]`;
371
+ }
372
+
373
+ try {
374
+ return JSON.stringify(content);
375
+ } catch {
376
+ return String(content);
377
+ }
378
+ }
379
+
380
+ function toToolContentText(toolCall = {}) {
381
+ if (toolCall.rawOutput != null) {
382
+ return typeof toolCall.rawOutput === 'string'
383
+ ? toolCall.rawOutput
384
+ : normalizeComparableValue(toolCall.rawOutput);
385
+ }
386
+
387
+ if (!Array.isArray(toolCall.content) || toolCall.content.length === 0) {
388
+ return '';
389
+ }
390
+
391
+ return toolCall.content
392
+ .map((item) => {
393
+ if (!item || typeof item !== 'object') {
394
+ return '';
395
+ }
396
+
397
+ if (item.type === 'content') {
398
+ return toContentBlockText(item.content);
399
+ }
400
+
401
+ if (item.type === 'diff') {
402
+ return `Diff ${item.path || ''}\n${item.newText || ''}`.trim();
403
+ }
404
+
405
+ if (item.type === 'terminal') {
406
+ return `Terminal ${item.terminalId || ''}`.trim();
407
+ }
408
+
409
+ return normalizeComparableValue(item);
410
+ })
411
+ .filter(Boolean)
412
+ .join('\n\n');
413
+ }
414
+
415
+ function createPermissionMessage(toolCall = {}) {
416
+ const title = String(toolCall.title || 'tool execution').trim();
417
+ return `Approval required for ${title}`;
418
+ }
419
+
420
+ function buildToolCallConversationPayload(update = {}, toolName = 'Tool') {
421
+ const payload = {
422
+ toolCallId: update.toolCallId,
423
+ toolName
424
+ };
425
+
426
+ if (update.title != null) {
427
+ payload.title = String(update.title);
428
+ }
429
+
430
+ if (update.kind != null) {
431
+ payload.kind = String(update.kind);
432
+ }
433
+
434
+ if (update.status != null) {
435
+ payload.status = String(update.status);
436
+ }
437
+
438
+ if (update.rawInput !== undefined) {
439
+ payload.rawInput = cloneJsonValue(update.rawInput);
440
+ }
441
+
442
+ if (update.rawOutput !== undefined) {
443
+ payload.rawOutput = cloneJsonValue(update.rawOutput);
444
+ }
445
+
446
+ if (Array.isArray(update.content)) {
447
+ payload.contentBlocks = cloneJsonValue(update.content);
448
+ }
449
+
450
+ if (Array.isArray(update.locations)) {
451
+ payload.locations = cloneJsonValue(update.locations);
452
+ }
453
+
454
+ return payload;
455
+ }
456
+
457
+ function normalizeSessionModeState(modes) {
458
+ if (!modes || typeof modes !== 'object') {
459
+ return null;
460
+ }
461
+
462
+ const availableModes = Array.isArray(modes.availableModes)
463
+ ? modes.availableModes
464
+ .map((mode) => {
465
+ if (!mode || typeof mode !== 'object') {
466
+ return null;
467
+ }
468
+
469
+ const id = String(mode.id || '').trim();
470
+ if (!id) {
471
+ return null;
472
+ }
473
+
474
+ return {
475
+ id,
476
+ name: String(mode.name || id),
477
+ description: mode.description == null ? null : String(mode.description)
478
+ };
479
+ })
480
+ .filter(Boolean)
481
+ : [];
482
+
483
+ const currentModeId = String(modes.currentModeId || '').trim();
484
+
485
+ if (!currentModeId && availableModes.length === 0) {
486
+ return null;
487
+ }
488
+
489
+ return {
490
+ availableModes,
491
+ currentModeId: currentModeId || null
492
+ };
493
+ }
494
+
495
+ function normalizeAvailableCommandsUpdate(update = {}) {
496
+ const availableCommands = Array.isArray(update.availableCommands)
497
+ ? update.availableCommands
498
+ .map((command) => {
499
+ if (!command || typeof command !== 'object') {
500
+ return null;
501
+ }
502
+
503
+ const name = String(command.name || '').trim();
504
+ if (!name) {
505
+ return null;
506
+ }
507
+
508
+ return {
509
+ name,
510
+ description: command.description == null ? '' : String(command.description),
511
+ input: command.input ? cloneJsonValue(command.input) : null
512
+ };
513
+ })
514
+ .filter(Boolean)
515
+ : [];
516
+
517
+ return {
518
+ availableCommands
519
+ };
520
+ }
521
+
522
+ function normalizePromptResourceLinks(resourceLinks = []) {
523
+ return Array.isArray(resourceLinks)
524
+ ? resourceLinks
525
+ .map((entry) => {
526
+ if (typeof entry === 'string') {
527
+ const uri = entry.trim();
528
+ if (!uri) return null;
529
+ return {
530
+ type: 'resource_link',
531
+ uri,
532
+ name: path.basename(uri) || uri
533
+ };
534
+ }
535
+
536
+ if (!entry || typeof entry !== 'object') {
537
+ return null;
538
+ }
539
+
540
+ const uri = String(entry.uri || entry.path || entry.url || '').trim();
541
+ if (!uri) {
542
+ return null;
543
+ }
544
+
545
+ return {
546
+ type: 'resource_link',
547
+ uri,
548
+ name: entry.name == null ? (path.basename(uri) || uri) : String(entry.name)
549
+ };
550
+ })
551
+ .filter(Boolean)
552
+ : [];
553
+ }
554
+
555
+ function buildConversationContentBlocks(images = [], resourceLinks = []) {
556
+ const blocks = [];
557
+
558
+ for (const image of images || []) {
559
+ if (!image?.data) {
560
+ continue;
561
+ }
562
+
563
+ blocks.push({
564
+ type: 'image',
565
+ data: image.data,
566
+ mimeType: image.mimeType || 'application/octet-stream',
567
+ name: image.name || null
568
+ });
569
+ }
570
+
571
+ blocks.push(...normalizePromptResourceLinks(resourceLinks));
572
+ return blocks;
573
+ }
574
+
575
+ function normalizeAssistantContentBlock(content = {}) {
576
+ if (!content || typeof content !== 'object' || !content.type) {
577
+ return null;
578
+ }
579
+
580
+ if (content.type === 'text') {
581
+ return null;
582
+ }
583
+
584
+ if (content.type === 'resource' && content.resource && typeof content.resource === 'object') {
585
+ return {
586
+ type: 'resource',
587
+ resource: cloneJsonValue(content.resource)
588
+ };
589
+ }
590
+
591
+ return cloneJsonValue(content);
592
+ }
593
+
594
+ export function splitCommandLine(value) {
595
+ const source = String(value || '').trim();
596
+ if (!source) {
597
+ throw new Error('ACP agent command cannot be empty');
598
+ }
599
+
600
+ const parts = [];
601
+ let current = '';
602
+ let quote = null;
603
+ let escaping = false;
604
+
605
+ for (const character of source) {
606
+ if (escaping) {
607
+ current += character;
608
+ escaping = false;
609
+ continue;
610
+ }
611
+
612
+ if (character === '\\' && quote !== '\'') {
613
+ escaping = true;
614
+ continue;
615
+ }
616
+
617
+ if (quote) {
618
+ if (character === quote) {
619
+ quote = null;
620
+ } else {
621
+ current += character;
622
+ }
623
+ continue;
624
+ }
625
+
626
+ if (character === '\'' || character === '"') {
627
+ quote = character;
628
+ continue;
629
+ }
630
+
631
+ if (/\s/.test(character)) {
632
+ if (current) {
633
+ parts.push(current);
634
+ current = '';
635
+ }
636
+ continue;
637
+ }
638
+
639
+ current += character;
640
+ }
641
+
642
+ if (current) {
643
+ parts.push(current);
644
+ }
645
+
646
+ if (parts.length === 0) {
647
+ throw new Error('ACP agent command cannot be empty');
648
+ }
649
+
650
+ return {
651
+ command: parts[0],
652
+ args: parts.slice(1)
653
+ };
654
+ }
655
+
656
+ function compareVersionParts(left, right) {
657
+ for (let index = 0; index < 3; index += 1) {
658
+ const leftValue = Number(left[index] || 0);
659
+ const rightValue = Number(right[index] || 0);
660
+ if (leftValue > rightValue) return 1;
661
+ if (leftValue < rightValue) return -1;
662
+ }
663
+ return 0;
664
+ }
665
+
666
+ async function resolveGeminiCommand(commandLine) {
667
+ const parsed = splitCommandLine(commandLine);
668
+
669
+ if (parsed.command !== 'gemini') {
670
+ return commandLine;
671
+ }
672
+
673
+ try {
674
+ const versionOutput = await new Promise((resolve, reject) => {
675
+ let stdout = '';
676
+ const childProcess = spawnCommand(parsed.command, ['--version'], {
677
+ stdio: ['ignore', 'pipe', 'ignore']
678
+ });
679
+
680
+ childProcess.stdout.on('data', (chunk) => {
681
+ stdout += chunk.toString();
682
+ });
683
+
684
+ childProcess.on('close', () => resolve(stdout.trim()));
685
+ childProcess.on('error', reject);
686
+ });
687
+
688
+ const match = String(versionOutput || '').match(/(\d+)\.(\d+)\.(\d+)/);
689
+ if (!match) {
690
+ return commandLine;
691
+ }
692
+
693
+ const versionParts = [Number(match[1]), Number(match[2]), Number(match[3])];
694
+ if (compareVersionParts(versionParts, [0, 33, 0]) >= 0) {
695
+ return commandLine;
696
+ }
697
+
698
+ const nextArgs = parsed.args.map((argument) => (
699
+ argument === '--acp' ? '--experimental-acp' : argument
700
+ ));
701
+ return [parsed.command, ...nextArgs].join(' ');
702
+ } catch {
703
+ return commandLine;
704
+ }
705
+ }
706
+
707
+ async function resolveNpxFallbackCommand(commandLine, agentKey) {
708
+ const parsed = splitCommandLine(commandLine);
709
+ if (parsed.command !== 'npx') {
710
+ return commandLine;
711
+ }
712
+
713
+ const npxCheck = await resolveCommandPath('npx');
714
+ if (npxCheck.found) {
715
+ return commandLine;
716
+ }
717
+
718
+ const npmCheck = await resolveCommandPath('npm');
719
+ if (npmCheck.found) {
720
+ return ['npm', 'exec', '--yes', '--', ...parsed.args].join(' ');
721
+ }
722
+
723
+ throw new Error(
724
+ `Unable to launch ${agentKey} ACP adapter because neither npx nor npm is available in PATH.`
725
+ );
726
+ }
727
+
728
+ export async function resolveLaunchCommand(agentKey, overrides = {}) {
729
+ const commandLine = resolveAgentCommand(agentKey, overrides);
730
+ if (!commandLine) {
731
+ throw new Error(`Unsupported ACP agent: ${agentKey}`);
732
+ }
733
+
734
+ const normalizedAgentKey = String(agentKey || '').trim().toLowerCase();
735
+ const resolvedCommandLine = await resolveNpxFallbackCommand(commandLine, normalizedAgentKey || 'unknown');
736
+
737
+ if (normalizedAgentKey === 'gemini') {
738
+ return resolveGeminiCommand(resolvedCommandLine);
739
+ }
740
+
741
+ return resolvedCommandLine;
742
+ }
743
+
744
+ function buildPromptBlocks(promptText, attachments = {}) {
745
+ const normalizedAttachments = Array.isArray(attachments)
746
+ ? { images: attachments }
747
+ : (attachments && typeof attachments === 'object' ? attachments : {});
748
+ const blocks = [];
749
+ const normalizedPromptText = String(promptText || '').trim() || 'Continue.';
750
+ blocks.push({
751
+ type: 'text',
752
+ text: normalizedPromptText
753
+ });
754
+
755
+ for (const image of normalizedAttachments.images || []) {
756
+ const parsed = parseDataUrl(image?.data);
757
+ if (!parsed) {
758
+ continue;
759
+ }
760
+
761
+ blocks.push({
762
+ type: 'image',
763
+ data: parsed.base64Data,
764
+ mimeType: parsed.mimeType
765
+ });
766
+ }
767
+
768
+ for (const resourceLink of normalizePromptResourceLinks(normalizedAttachments.resourceLinks || [])) {
769
+ blocks.push(resourceLink);
770
+ }
771
+
772
+ return {
773
+ blocks,
774
+ text: normalizedPromptText,
775
+ contentBlocks: buildConversationContentBlocks(
776
+ normalizedAttachments.images || [],
777
+ normalizedAttachments.resourceLinks || []
778
+ )
779
+ };
780
+ }
781
+
782
+ class LocalTerminalProcess {
783
+ constructor({
784
+ sessionId,
785
+ command,
786
+ args = [],
787
+ cwd = null,
788
+ env = [],
789
+ outputByteLimit = DEFAULT_TERMINAL_OUTPUT_LIMIT,
790
+ maxLifetimeMs = DEFAULT_TERMINAL_MAX_LIFETIME_MS
791
+ }) {
792
+ const normalizedSpawn = normalizeTerminalCommandForSpawn(command, args);
793
+
794
+ this.sessionId = sessionId;
795
+ this.command = normalizedSpawn.command;
796
+ this.args = normalizedSpawn.args;
797
+ this.cwd = cwd || process.cwd();
798
+ this.outputByteLimit = Number(outputByteLimit) > 0 ? Number(outputByteLimit) : DEFAULT_TERMINAL_OUTPUT_LIMIT;
799
+ this.maxLifetimeMs = Number(maxLifetimeMs) > 0 ? Number(maxLifetimeMs) : DEFAULT_TERMINAL_MAX_LIFETIME_MS;
800
+ this.output = '';
801
+ this.exitStatus = null;
802
+ this.waiters = [];
803
+ this.maxLifetimeTimer = null;
804
+
805
+ const envEntries = Array.isArray(env)
806
+ ? env.reduce((accumulator, entry) => {
807
+ if (entry?.name) {
808
+ accumulator[entry.name] = String(entry.value ?? '');
809
+ }
810
+ return accumulator;
811
+ }, {})
812
+ : {};
813
+
814
+ this.child = spawnCommand(this.command, this.args, {
815
+ cwd: this.cwd,
816
+ env: sanitizeProcessEnv(process.env, envEntries),
817
+ stdio: ['ignore', 'pipe', 'pipe']
818
+ });
819
+
820
+ this.maxLifetimeTimer = setTimeout(() => {
821
+ void this.kill();
822
+ }, this.maxLifetimeMs);
823
+ this.maxLifetimeTimer.unref?.();
824
+
825
+ const appendOutput = (chunk) => {
826
+ this.output += chunk.toString();
827
+ if (Buffer.byteLength(this.output, 'utf8') > this.outputByteLimit) {
828
+ const sliced = Buffer.from(this.output, 'utf8').subarray(-this.outputByteLimit);
829
+ this.output = sliced.toString('utf8');
830
+ }
831
+ };
832
+
833
+ this.child.stdout.on('data', appendOutput);
834
+ this.child.stderr.on('data', appendOutput);
835
+ this.child.on('close', (exitCode, signal) => {
836
+ if (this.maxLifetimeTimer) {
837
+ clearTimeout(this.maxLifetimeTimer);
838
+ this.maxLifetimeTimer = null;
839
+ }
840
+ this.exitStatus = {
841
+ exitCode,
842
+ signal
843
+ };
844
+ this.waiters.splice(0).forEach((resolve) => resolve(this.exitStatus));
845
+ });
846
+ }
847
+
848
+ currentOutput() {
849
+ return {
850
+ output: this.output,
851
+ exitStatus: this.exitStatus || undefined
852
+ };
853
+ }
854
+
855
+ async waitForExit() {
856
+ if (this.exitStatus) {
857
+ return this.exitStatus;
858
+ }
859
+
860
+ return new Promise((resolve) => {
861
+ this.waiters.push(resolve);
862
+ });
863
+ }
864
+
865
+ async kill() {
866
+ if (!this.exitStatus) {
867
+ try {
868
+ this.child.kill('SIGTERM');
869
+ } catch {}
870
+ }
871
+
872
+ return {};
873
+ }
874
+
875
+ async release() {
876
+ await this.kill();
877
+ return {};
878
+ }
879
+ }
880
+
881
+ export class AcpClient {
882
+ constructor({
883
+ agentKey,
884
+ writer,
885
+ projectPath,
886
+ agentCommandOverrides = {},
887
+ model = null,
888
+ permissionMode = 'default'
889
+ }) {
890
+ this.agentKey = String(agentKey || '').trim().toLowerCase();
891
+ this.writer = writer;
892
+ this.projectPath = path.resolve(projectPath || process.cwd());
893
+ this.agentCommandOverrides = agentCommandOverrides || {};
894
+ this.model = model || null;
895
+ this.permissionMode = permissionMode || 'default';
896
+
897
+ this.childProcess = null;
898
+ this.connection = null;
899
+ this.sessionId = null;
900
+ this.initializeResult = null;
901
+ this.suppressSessionUpdates = false;
902
+ this.pendingPermissionIds = new Set();
903
+ this.terminalProcesses = new Map();
904
+ this.spawnCleanup = null;
905
+ this.turnQueue = Promise.resolve();
906
+ this.isPromptInFlight = false;
907
+ this.pendingPromptDiagnostic = null;
908
+ this.hasReportedPromptFatalError = false;
909
+ this.agentDiagnosticBuffer = '';
910
+ this.streamState = {
911
+ turnId: null,
912
+ assistantSegmentIndex: 0,
913
+ assistantMessageId: null,
914
+ assistantTextStarted: false,
915
+ reasoningMessageId: null,
916
+ toolCalls: new Map()
917
+ };
918
+ }
919
+
920
+ emitPayload(payload) {
921
+ if (!this.writer || typeof this.writer.send !== 'function') {
922
+ return;
923
+ }
924
+
925
+ this.writer.send({
926
+ provider: this.agentKey,
927
+ sessionId: this.sessionId,
928
+ runtime: 'acp',
929
+ timestamp: nowIso(),
930
+ ...payload
931
+ });
932
+ }
933
+
934
+ attachWriter(writer) {
935
+ this.writer = writer || null;
936
+ if (this.sessionId) {
937
+ this.writer?.setSessionId?.(this.sessionId);
938
+ }
939
+ }
940
+
941
+ async configureForTurn({ writer, permissionMode = null, model = null } = {}) {
942
+ if (writer !== undefined) {
943
+ this.attachWriter(writer);
944
+ }
945
+
946
+ if (permissionMode) {
947
+ this.permissionMode = permissionMode;
948
+ }
949
+
950
+ if (model) {
951
+ const shouldUpdateModel = model !== this.model;
952
+ this.model = model;
953
+ if (shouldUpdateModel && this.connection && this.sessionId) {
954
+ await this.setModel(model);
955
+ }
956
+ }
957
+ }
958
+
959
+ emitConversationEvent(kind, payload = {}, options = {}) {
960
+ const event = createConversationEvent({
961
+ kind,
962
+ provider: this.agentKey,
963
+ sessionId: this.sessionId,
964
+ timestamp: options.timestamp || nowIso(),
965
+ payload,
966
+ extensions: {
967
+ runtimeSource: 'acp',
968
+ ...(options.extensions || {})
969
+ },
970
+ rawRef: {
971
+ runtime: 'acp',
972
+ sourceType: options.sourceType || 'acp-session-update'
973
+ }
974
+ });
975
+
976
+ this.emitPayload({
977
+ type: 'conversation-event',
978
+ event
979
+ });
980
+ }
981
+
982
+ emitSystemNotice(message, options = {}) {
983
+ if (!message) {
984
+ return;
985
+ }
986
+
987
+ this.emitConversationEvent(
988
+ CONVERSATION_EVENT_KINDS.SYSTEM_NOTICE,
989
+ { message },
990
+ {
991
+ sourceType: 'acp-system-notice',
992
+ ...options
993
+ }
994
+ );
995
+ }
996
+
997
+ reportPromptFailure(error, { emitRawError = false } = {}) {
998
+ if (this.hasReportedPromptFatalError) {
999
+ return;
1000
+ }
1001
+
1002
+ this.hasReportedPromptFatalError = true;
1003
+
1004
+ const failedAt = nowIso();
1005
+ this.flushOpenTextStreams(failedAt);
1006
+ this.emitConversationEvent(
1007
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
1008
+ {
1009
+ state: 'errored'
1010
+ },
1011
+ {
1012
+ timestamp: failedAt,
1013
+ sourceType: 'acp-prompt-error'
1014
+ }
1015
+ );
1016
+ this.emitPayload({
1017
+ type: 'session-status',
1018
+ isProcessing: false
1019
+ });
1020
+
1021
+ if (emitRawError) {
1022
+ this.emitPayload({
1023
+ type: 'error',
1024
+ error: error?.message || 'Agent prompt failed.'
1025
+ });
1026
+ }
1027
+ }
1028
+
1029
+ handleAgentDiagnostic(text) {
1030
+ const diagnosticText = String(text || '').trim();
1031
+ if (!diagnosticText) {
1032
+ return;
1033
+ }
1034
+
1035
+ const nextBuffer = this.agentDiagnosticBuffer
1036
+ ? `${this.agentDiagnosticBuffer}\n${diagnosticText}`
1037
+ : diagnosticText;
1038
+ this.agentDiagnosticBuffer = nextBuffer.length > 8192
1039
+ ? nextBuffer.slice(-8192)
1040
+ : nextBuffer;
1041
+
1042
+ const fatalError = detectFatalAgentDiagnostic(this.agentKey, this.agentDiagnosticBuffer);
1043
+ if (!fatalError || !this.isPromptInFlight) {
1044
+ return;
1045
+ }
1046
+
1047
+ const pendingDiagnostic = this.pendingPromptDiagnostic;
1048
+ if (pendingDiagnostic?.handled) {
1049
+ return;
1050
+ }
1051
+
1052
+ if (pendingDiagnostic) {
1053
+ pendingDiagnostic.handled = true;
1054
+ this.reportPromptFailure(fatalError, {
1055
+ emitRawError: true
1056
+ });
1057
+ pendingDiagnostic.reject(fatalError);
1058
+ }
1059
+
1060
+ if (this.childProcess && !this.childProcess.killed) {
1061
+ try {
1062
+ this.childProcess.kill('SIGTERM');
1063
+ } catch {}
1064
+ }
1065
+ }
1066
+
1067
+ async runExclusive(operation) {
1068
+ const previousTurn = this.turnQueue.catch(() => {});
1069
+ let releaseTurn;
1070
+
1071
+ this.turnQueue = new Promise((resolve) => {
1072
+ releaseTurn = resolve;
1073
+ });
1074
+
1075
+ await previousTurn;
1076
+
1077
+ try {
1078
+ return await operation();
1079
+ } finally {
1080
+ releaseTurn?.();
1081
+ }
1082
+ }
1083
+
1084
+ ensureTurnId() {
1085
+ if (!this.streamState.turnId) {
1086
+ this.streamState.turnId = randomUUID();
1087
+ }
1088
+ return this.streamState.turnId;
1089
+ }
1090
+
1091
+ createAssistantMessageId(prefix = 'assistant_text') {
1092
+ const turnId = this.ensureTurnId();
1093
+ return createEventMessageId(
1094
+ prefix,
1095
+ this.agentKey,
1096
+ this.sessionId,
1097
+ `${turnId}:segment-${this.streamState.assistantSegmentIndex}`
1098
+ );
1099
+ }
1100
+
1101
+ closeAssistantMessageBoundary(timestamp = nowIso(), sourceType = 'acp-message-boundary') {
1102
+ const hadAssistantBoundary = Boolean(
1103
+ this.streamState.assistantMessageId || this.streamState.assistantTextStarted
1104
+ );
1105
+
1106
+ if (this.streamState.assistantMessageId && this.streamState.assistantTextStarted) {
1107
+ this.emitConversationEvent(
1108
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_END,
1109
+ { messageId: this.streamState.assistantMessageId },
1110
+ {
1111
+ timestamp,
1112
+ sourceType
1113
+ }
1114
+ );
1115
+ }
1116
+
1117
+ this.streamState.assistantMessageId = null;
1118
+ this.streamState.assistantTextStarted = false;
1119
+ if (hadAssistantBoundary) {
1120
+ this.streamState.assistantSegmentIndex += 1;
1121
+ }
1122
+ }
1123
+
1124
+ flushOpenTextStreams(timestamp = nowIso()) {
1125
+ this.closeAssistantMessageBoundary(timestamp, 'acp-turn-complete');
1126
+
1127
+ if (this.streamState.reasoningMessageId) {
1128
+ this.emitConversationEvent(
1129
+ CONVERSATION_EVENT_KINDS.REASONING_END,
1130
+ { messageId: this.streamState.reasoningMessageId },
1131
+ {
1132
+ timestamp,
1133
+ sourceType: 'acp-turn-complete'
1134
+ }
1135
+ );
1136
+ this.streamState.reasoningMessageId = null;
1137
+ }
1138
+ }
1139
+
1140
+ resetTurnState({ nextTurnId = randomUUID() } = {}) {
1141
+ this.flushOpenTextStreams(nowIso());
1142
+ this.streamState.toolCalls.clear();
1143
+ this.streamState.turnId = nextTurnId;
1144
+ this.streamState.assistantSegmentIndex = 0;
1145
+ }
1146
+
1147
+ async handleRequestPermission(request) {
1148
+ const toolKind = inferToolKind(request?.toolCall);
1149
+ const allowAllOption = pickPermissionOption(request?.options, ['allow_once', 'allow_always']);
1150
+ const rejectOption = pickPermissionOption(request?.options, ['reject_once', 'reject_always']);
1151
+
1152
+ if (this.permissionMode === 'bypassPermissions' && allowAllOption) {
1153
+ return createSelectedPermissionResponse(allowAllOption.optionId);
1154
+ }
1155
+
1156
+ if (this.permissionMode === 'acceptEdits' && ['read', 'search', 'edit', 'move', 'think', 'switch_mode'].includes(toolKind)) {
1157
+ return allowAllOption
1158
+ ? createSelectedPermissionResponse(allowAllOption.optionId)
1159
+ : createCancelledPermissionResponse();
1160
+ }
1161
+
1162
+ if (this.permissionMode === 'plan') {
1163
+ if (['read', 'search', 'fetch', 'think', 'switch_mode'].includes(toolKind) && allowAllOption) {
1164
+ return createSelectedPermissionResponse(allowAllOption.optionId);
1165
+ }
1166
+ if (rejectOption) {
1167
+ return createSelectedPermissionResponse(rejectOption.optionId);
1168
+ }
1169
+ return createCancelledPermissionResponse();
1170
+ }
1171
+
1172
+ const requestId = `acp-permission:${randomUUID()}`;
1173
+ const timestamp = nowIso();
1174
+
1175
+ this.pendingPermissionIds.add(requestId);
1176
+
1177
+ this.emitConversationEvent(
1178
+ CONVERSATION_EVENT_KINDS.APPROVAL_REQUEST,
1179
+ {
1180
+ requestId,
1181
+ toolName: request?.toolCall?.title || 'tool execution',
1182
+ input: request?.toolCall?.rawInput ?? null,
1183
+ options: cloneJsonValue(request?.options || []),
1184
+ toolCall: cloneJsonValue(request?.toolCall || null),
1185
+ context: {
1186
+ kind: toolKind,
1187
+ options: request?.options || []
1188
+ },
1189
+ sessionId: this.sessionId,
1190
+ message: createPermissionMessage(request?.toolCall)
1191
+ },
1192
+ {
1193
+ timestamp,
1194
+ extensions: {
1195
+ requestId
1196
+ },
1197
+ sourceType: 'acp-permission-request'
1198
+ }
1199
+ );
1200
+
1201
+ const response = await new Promise((resolve) => {
1202
+ pendingPermissionRequests.set(requestId, {
1203
+ request,
1204
+ sessionId: this.sessionId,
1205
+ provider: this.agentKey,
1206
+ resolve
1207
+ });
1208
+ });
1209
+
1210
+ this.pendingPermissionIds.delete(requestId);
1211
+ pendingPermissionRequests.delete(requestId);
1212
+
1213
+ const status = classifyPermissionStatus(response, request);
1214
+ this.emitConversationEvent(
1215
+ CONVERSATION_EVENT_KINDS.APPROVAL_RESOLVED,
1216
+ {
1217
+ requestId,
1218
+ status,
1219
+ toolName: request?.toolCall?.title || null,
1220
+ message: status === 'approved'
1221
+ ? `Approved ${request?.toolCall?.title || 'tool execution'}`
1222
+ : status === 'denied'
1223
+ ? `Denied ${request?.toolCall?.title || 'tool execution'}`
1224
+ : `Cancelled ${request?.toolCall?.title || 'tool execution'}`
1225
+ },
1226
+ {
1227
+ timestamp: nowIso(),
1228
+ extensions: {
1229
+ requestId
1230
+ },
1231
+ sourceType: 'acp-permission-resolution'
1232
+ }
1233
+ );
1234
+
1235
+ return response;
1236
+ }
1237
+
1238
+ async handleSessionUpdate(notification) {
1239
+ if (this.suppressSessionUpdates) {
1240
+ return;
1241
+ }
1242
+
1243
+ const update = notification?.update;
1244
+ const timestamp = nowIso();
1245
+ if (!update || typeof update !== 'object') {
1246
+ return;
1247
+ }
1248
+
1249
+ if (update.sessionUpdate === 'agent_message_chunk') {
1250
+ if (update.content?.type === 'text' && typeof update.content.text === 'string') {
1251
+ const messageId = this.streamState.assistantMessageId
1252
+ || this.createAssistantMessageId('assistant_text');
1253
+
1254
+ if (!this.streamState.assistantMessageId) {
1255
+ this.streamState.assistantMessageId = messageId;
1256
+ }
1257
+
1258
+ if (!this.streamState.assistantTextStarted) {
1259
+ this.emitConversationEvent(
1260
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START,
1261
+ { messageId },
1262
+ { timestamp, sourceType: 'acp-agent-message' }
1263
+ );
1264
+ this.streamState.assistantTextStarted = true;
1265
+ }
1266
+
1267
+ this.emitConversationEvent(
1268
+ CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA,
1269
+ {
1270
+ messageId,
1271
+ text: update.content.text
1272
+ },
1273
+ {
1274
+ timestamp,
1275
+ sourceType: 'acp-agent-message'
1276
+ }
1277
+ );
1278
+ return;
1279
+ }
1280
+
1281
+ const contentBlock = normalizeAssistantContentBlock(update.content);
1282
+ if (contentBlock) {
1283
+ const messageId = this.streamState.assistantMessageId
1284
+ || this.createAssistantMessageId('assistant_content');
1285
+
1286
+ if (!this.streamState.assistantMessageId) {
1287
+ this.streamState.assistantMessageId = messageId;
1288
+ }
1289
+
1290
+ this.emitConversationEvent(
1291
+ CONVERSATION_EVENT_KINDS.ASSISTANT_CONTENT_BLOCK,
1292
+ {
1293
+ messageId,
1294
+ contentBlock
1295
+ },
1296
+ {
1297
+ timestamp,
1298
+ sourceType: 'acp-agent-message'
1299
+ }
1300
+ );
1301
+ }
1302
+ return;
1303
+ }
1304
+
1305
+ if (update.sessionUpdate === 'agent_thought_chunk') {
1306
+ if (update.content?.type === 'text' && typeof update.content.text === 'string') {
1307
+ const messageId = this.streamState.reasoningMessageId
1308
+ || createEventMessageId('reasoning', this.agentKey, this.sessionId, this.ensureTurnId());
1309
+
1310
+ if (!this.streamState.reasoningMessageId) {
1311
+ this.emitConversationEvent(
1312
+ CONVERSATION_EVENT_KINDS.REASONING_START,
1313
+ { messageId },
1314
+ { timestamp, sourceType: 'acp-agent-thought' }
1315
+ );
1316
+ this.streamState.reasoningMessageId = messageId;
1317
+ }
1318
+
1319
+ this.emitConversationEvent(
1320
+ CONVERSATION_EVENT_KINDS.REASONING_DELTA,
1321
+ {
1322
+ messageId,
1323
+ text: update.content.text
1324
+ },
1325
+ {
1326
+ timestamp,
1327
+ sourceType: 'acp-agent-thought'
1328
+ }
1329
+ );
1330
+ }
1331
+ return;
1332
+ }
1333
+
1334
+ if (update.sessionUpdate === 'end_turn') {
1335
+ this.flushOpenTextStreams(timestamp);
1336
+ return;
1337
+ }
1338
+
1339
+ if (update.sessionUpdate === 'plan') {
1340
+ this.emitConversationEvent(
1341
+ CONVERSATION_EVENT_KINDS.PLAN_UPDATE,
1342
+ {
1343
+ entries: cloneJsonValue(update.entries || []),
1344
+ message: update.message || 'Implementation plan updated'
1345
+ },
1346
+ {
1347
+ timestamp,
1348
+ sourceType: 'acp-plan'
1349
+ }
1350
+ );
1351
+ return;
1352
+ }
1353
+
1354
+ if (update.sessionUpdate === 'current_mode_update') {
1355
+ const payload = {
1356
+ currentModeId: String(update.currentModeId || '').trim() || null
1357
+ };
1358
+
1359
+ this.emitConversationEvent(
1360
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1361
+ payload,
1362
+ {
1363
+ timestamp,
1364
+ sourceType: 'acp-mode-update'
1365
+ }
1366
+ );
1367
+ return;
1368
+ }
1369
+
1370
+ if (update.sessionUpdate === 'available_commands_update') {
1371
+ this.emitConversationEvent(
1372
+ CONVERSATION_EVENT_KINDS.AVAILABLE_COMMANDS_UPDATE,
1373
+ normalizeAvailableCommandsUpdate(update),
1374
+ {
1375
+ timestamp,
1376
+ sourceType: 'acp-available-commands-update'
1377
+ }
1378
+ );
1379
+ return;
1380
+ }
1381
+
1382
+ if (update.sessionUpdate === 'tool_call' || update.sessionUpdate === 'tool_call_update') {
1383
+ const existing = this.streamState.toolCalls.get(update.toolCallId) || {
1384
+ snapshotSignature: null,
1385
+ resultSignature: null,
1386
+ ended: false,
1387
+ started: false,
1388
+ toolName: null
1389
+ };
1390
+ const toolName = update.title || existing.toolName || 'Tool';
1391
+ const toolPayload = buildToolCallConversationPayload(update, toolName);
1392
+
1393
+ if (!existing.started) {
1394
+ // Tool calls should visually split assistant prose into before/after segments.
1395
+ // Closing the current assistant message here lets post-tool text start a fresh block.
1396
+ this.closeAssistantMessageBoundary(timestamp, 'acp-tool-boundary');
1397
+ this.emitConversationEvent(
1398
+ CONVERSATION_EVENT_KINDS.TOOL_CALL_START,
1399
+ toolPayload,
1400
+ {
1401
+ timestamp,
1402
+ sourceType: 'acp-tool-call'
1403
+ }
1404
+ );
1405
+ existing.started = true;
1406
+ }
1407
+
1408
+ const snapshotPayload = update.rawInput === undefined
1409
+ ? toolPayload
1410
+ : {
1411
+ ...toolPayload,
1412
+ input: cloneJsonValue(update.rawInput)
1413
+ };
1414
+ const snapshotSignature = normalizeComparableValue(snapshotPayload);
1415
+ if (snapshotSignature !== existing.snapshotSignature) {
1416
+ this.emitConversationEvent(
1417
+ CONVERSATION_EVENT_KINDS.TOOL_CALL_INPUT,
1418
+ snapshotPayload,
1419
+ {
1420
+ timestamp,
1421
+ sourceType: 'acp-tool-call'
1422
+ }
1423
+ );
1424
+ existing.snapshotSignature = snapshotSignature;
1425
+ }
1426
+
1427
+ const isTerminalStatus = update.status === 'completed' || update.status === 'failed';
1428
+ if (isTerminalStatus && !existing.ended) {
1429
+ this.emitConversationEvent(
1430
+ CONVERSATION_EVENT_KINDS.TOOL_CALL_END,
1431
+ toolPayload,
1432
+ {
1433
+ timestamp,
1434
+ sourceType: 'acp-tool-call'
1435
+ }
1436
+ );
1437
+
1438
+ const resultText = toToolContentText(update);
1439
+ const resultSignature = normalizeComparableValue({
1440
+ text: resultText,
1441
+ rawOutput: update.rawOutput,
1442
+ status: update.status,
1443
+ content: update.content,
1444
+ locations: update.locations
1445
+ });
1446
+
1447
+ if (resultSignature !== existing.resultSignature) {
1448
+ this.emitConversationEvent(
1449
+ CONVERSATION_EVENT_KINDS.TOOL_RESULT,
1450
+ {
1451
+ ...toolPayload,
1452
+ content: resultText || '',
1453
+ isError: update.status === 'failed',
1454
+ rawOutput: cloneJsonValue(update.rawOutput),
1455
+ contentBlocks: Array.isArray(update.content) ? cloneJsonValue(update.content) : []
1456
+ },
1457
+ {
1458
+ timestamp,
1459
+ sourceType: 'acp-tool-call'
1460
+ }
1461
+ );
1462
+ existing.resultSignature = resultSignature;
1463
+ }
1464
+
1465
+ existing.ended = true;
1466
+ }
1467
+
1468
+ existing.toolName = toolName;
1469
+ this.streamState.toolCalls.set(update.toolCallId, existing);
1470
+ }
1471
+ }
1472
+
1473
+ async start() {
1474
+ if (this.connection) {
1475
+ return this.initializeResult;
1476
+ }
1477
+
1478
+ const launchCommand = await resolveLaunchCommand(this.agentKey, this.agentCommandOverrides);
1479
+ const { command, args } = splitCommandLine(launchCommand);
1480
+ const spawnEnvironment = await prepareSpawnEnvironment(this.agentKey);
1481
+ this.spawnCleanup = spawnEnvironment.cleanup;
1482
+
1483
+ this.childProcess = spawnCommand(command, args, {
1484
+ cwd: this.projectPath,
1485
+ stdio: ['pipe', 'pipe', 'pipe'],
1486
+ windowsHide: true,
1487
+ env: spawnEnvironment.env
1488
+ });
1489
+
1490
+ this.childProcess.stderr.on('data', (chunk) => {
1491
+ const text = String(chunk || '').trim();
1492
+ if (text) {
1493
+ console.warn(`[ACP:${this.agentKey}] ${text}`);
1494
+ this.handleAgentDiagnostic(text);
1495
+ }
1496
+ });
1497
+
1498
+ const stream = ndJsonStream(
1499
+ Writable.toWeb(this.childProcess.stdin),
1500
+ Readable.toWeb(this.childProcess.stdout)
1501
+ );
1502
+
1503
+ this.connection = new ClientSideConnection(() => ({
1504
+ requestPermission: (params) => this.handleRequestPermission(params),
1505
+ sessionUpdate: (params) => this.handleSessionUpdate(params),
1506
+ readTextFile: async (params) => {
1507
+ const resolvedPath = path.isAbsolute(params.path)
1508
+ ? params.path
1509
+ : path.resolve(this.projectPath, params.path);
1510
+ const content = await fs.readFile(resolvedPath, 'utf8');
1511
+ return {
1512
+ content: readLines(content, params.line, params.limit)
1513
+ };
1514
+ },
1515
+ writeTextFile: async (params) => {
1516
+ const resolvedPath = path.isAbsolute(params.path)
1517
+ ? params.path
1518
+ : path.resolve(this.projectPath, params.path);
1519
+ await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
1520
+ await fs.writeFile(resolvedPath, params.content, 'utf8');
1521
+ return {};
1522
+ },
1523
+ createTerminal: async (params) => {
1524
+ const terminalId = `terminal:${randomUUID()}`;
1525
+ const terminal = new LocalTerminalProcess({
1526
+ sessionId: params.sessionId,
1527
+ command: params.command,
1528
+ args: params.args,
1529
+ cwd: params.cwd || this.projectPath,
1530
+ env: params.env,
1531
+ outputByteLimit: params.outputByteLimit
1532
+ });
1533
+ this.terminalProcesses.set(terminalId, terminal);
1534
+ return { terminalId };
1535
+ },
1536
+ terminalOutput: async (params) => {
1537
+ return this.terminalProcesses.get(params.terminalId)?.currentOutput() || { output: '' };
1538
+ },
1539
+ waitForTerminalExit: async (params) => {
1540
+ return this.terminalProcesses.get(params.terminalId)?.waitForExit() || {};
1541
+ },
1542
+ killTerminal: async (params) => {
1543
+ const terminal = this.terminalProcesses.get(params.terminalId);
1544
+ if (terminal) {
1545
+ await terminal.kill();
1546
+ }
1547
+ return {};
1548
+ },
1549
+ releaseTerminal: async (params) => {
1550
+ const terminal = this.terminalProcesses.get(params.terminalId);
1551
+ if (terminal) {
1552
+ await terminal.release();
1553
+ this.terminalProcesses.delete(params.terminalId);
1554
+ }
1555
+ return {};
1556
+ }
1557
+ }), stream);
1558
+
1559
+ this.initializeResult = await this.connection.initialize({
1560
+ protocolVersion: PROTOCOL_VERSION,
1561
+ clientCapabilities: {
1562
+ fs: {
1563
+ readTextFile: true,
1564
+ writeTextFile: true
1565
+ },
1566
+ terminal: true
1567
+ },
1568
+ clientInfo: {
1569
+ name: '@axhub/genie',
1570
+ title: 'Axhub Genie',
1571
+ version: process.env.APP_VERSION || 'unknown'
1572
+ }
1573
+ });
1574
+
1575
+ return this.initializeResult;
1576
+ }
1577
+
1578
+ async createSession() {
1579
+ const response = await this.connection.newSession({
1580
+ cwd: this.projectPath,
1581
+ mcpServers: []
1582
+ });
1583
+
1584
+ this.sessionId = response.sessionId;
1585
+ this.attachWriter(this.writer);
1586
+ const normalizedModes = normalizeSessionModeState(response?.modes);
1587
+ this.emitPayload({
1588
+ type: 'session-created',
1589
+ modes: normalizedModes
1590
+ });
1591
+
1592
+ if (normalizedModes) {
1593
+ this.emitConversationEvent(
1594
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1595
+ normalizedModes,
1596
+ {
1597
+ timestamp: nowIso(),
1598
+ sourceType: 'acp-session-created-modes'
1599
+ }
1600
+ );
1601
+ }
1602
+
1603
+ if (this.model) {
1604
+ await this.setModel(this.model);
1605
+ }
1606
+
1607
+ return response;
1608
+ }
1609
+
1610
+ async loadSession(sessionId, { suppressReplayUpdates = true } = {}) {
1611
+ this.sessionId = String(sessionId || '').trim();
1612
+ this.attachWriter(this.writer);
1613
+ this.suppressSessionUpdates = Boolean(suppressReplayUpdates);
1614
+ try {
1615
+ const response = await this.connection.loadSession({
1616
+ cwd: this.projectPath,
1617
+ mcpServers: [],
1618
+ sessionId: this.sessionId
1619
+ });
1620
+
1621
+ const normalizedModes = normalizeSessionModeState(response?.modes);
1622
+ if (normalizedModes) {
1623
+ this.emitConversationEvent(
1624
+ CONVERSATION_EVENT_KINDS.MODE_UPDATE,
1625
+ normalizedModes,
1626
+ {
1627
+ timestamp: nowIso(),
1628
+ sourceType: 'acp-session-loaded-modes'
1629
+ }
1630
+ );
1631
+ }
1632
+
1633
+ if (this.model) {
1634
+ await this.setModel(this.model);
1635
+ }
1636
+
1637
+ return response;
1638
+ } finally {
1639
+ this.suppressSessionUpdates = false;
1640
+ }
1641
+ }
1642
+
1643
+ async setModel(modelId) {
1644
+ if (!modelId || !this.sessionId || !this.connection) {
1645
+ return;
1646
+ }
1647
+
1648
+ let lastError = null;
1649
+
1650
+ try {
1651
+ if (typeof this.connection.setSessionConfigOption === 'function') {
1652
+ try {
1653
+ await this.connection.setSessionConfigOption({
1654
+ sessionId: this.sessionId,
1655
+ configId: 'model',
1656
+ value: modelId
1657
+ });
1658
+ return;
1659
+ } catch (error) {
1660
+ lastError = error;
1661
+ }
1662
+ }
1663
+
1664
+ if (typeof this.connection.unstable_setSessionModel === 'function') {
1665
+ try {
1666
+ await this.connection.unstable_setSessionModel({
1667
+ sessionId: this.sessionId,
1668
+ modelId
1669
+ });
1670
+ return;
1671
+ } catch (error) {
1672
+ lastError = error;
1673
+ }
1674
+ }
1675
+ } catch (error) {
1676
+ lastError = error;
1677
+ }
1678
+
1679
+ if (lastError) {
1680
+ console.warn(`[ACP:${this.agentKey}] Failed to set model "${modelId}": ${lastError.message}`);
1681
+ }
1682
+ }
1683
+
1684
+ async setMode(modeId) {
1685
+ const normalizedModeId = String(modeId || '').trim();
1686
+ if (!normalizedModeId || !this.sessionId || !this.connection?.setSessionMode) {
1687
+ return false;
1688
+ }
1689
+
1690
+ await this.connection.setSessionMode({
1691
+ sessionId: this.sessionId,
1692
+ modeId: normalizedModeId
1693
+ });
1694
+ return true;
1695
+ }
1696
+
1697
+ async sendPrompt(promptText, attachments = {}) {
1698
+ this.resetTurnState();
1699
+ this.hasReportedPromptFatalError = false;
1700
+ this.agentDiagnosticBuffer = '';
1701
+ const promptPayload = buildPromptBlocks(promptText, attachments);
1702
+ const clientRequestId = typeof attachments?.clientRequestId === 'string' && attachments.clientRequestId.trim()
1703
+ ? attachments.clientRequestId.trim()
1704
+ : null;
1705
+
1706
+ this.emitConversationEvent(
1707
+ CONVERSATION_EVENT_KINDS.USER_MESSAGE,
1708
+ {
1709
+ text: promptPayload.text,
1710
+ contentBlocks: promptPayload.contentBlocks
1711
+ },
1712
+ {
1713
+ timestamp: nowIso(),
1714
+ sourceType: 'acp-user-prompt',
1715
+ extensions: clientRequestId ? { clientRequestId } : {}
1716
+ }
1717
+ );
1718
+
1719
+ this.emitConversationEvent(
1720
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
1721
+ {
1722
+ state: 'queued'
1723
+ },
1724
+ {
1725
+ timestamp: nowIso(),
1726
+ sourceType: 'acp-prompt-start'
1727
+ }
1728
+ );
1729
+
1730
+ this.emitPayload({
1731
+ type: 'session-status',
1732
+ isProcessing: true
1733
+ });
1734
+
1735
+ this.isPromptInFlight = true;
1736
+ let diagnosticGuard = null;
1737
+ const diagnosticPromise = new Promise((_, reject) => {
1738
+ diagnosticGuard = {
1739
+ handled: false,
1740
+ reject
1741
+ };
1742
+ this.pendingPromptDiagnostic = diagnosticGuard;
1743
+ });
1744
+
1745
+ const promptPromise = this.connection.prompt({
1746
+ sessionId: this.sessionId,
1747
+ prompt: promptPayload.blocks
1748
+ });
1749
+ promptPromise.catch(() => {});
1750
+
1751
+ try {
1752
+ const response = await Promise.race([promptPromise, diagnosticPromise]);
1753
+ const finishedAt = nowIso();
1754
+ this.flushOpenTextStreams(finishedAt);
1755
+ this.emitConversationEvent(
1756
+ CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED,
1757
+ {
1758
+ state: response?.stopReason === 'cancelled' ? 'aborted' : 'completed',
1759
+ stopReason: response?.stopReason || null
1760
+ },
1761
+ {
1762
+ timestamp: finishedAt,
1763
+ sourceType: 'acp-prompt-complete'
1764
+ }
1765
+ );
1766
+
1767
+ this.emitPayload({
1768
+ type: 'session-status',
1769
+ isProcessing: false
1770
+ });
1771
+
1772
+ return response;
1773
+ } catch (error) {
1774
+ this.reportPromptFailure(error);
1775
+ throw error;
1776
+ } finally {
1777
+ this.isPromptInFlight = false;
1778
+ if (this.pendingPromptDiagnostic === diagnosticGuard) {
1779
+ this.pendingPromptDiagnostic = null;
1780
+ }
1781
+ }
1782
+ }
1783
+
1784
+ async cancel(sessionId = this.sessionId) {
1785
+ const normalizedSessionId = String(sessionId || this.sessionId || '').trim();
1786
+ if (!normalizedSessionId) {
1787
+ return false;
1788
+ }
1789
+
1790
+ for (const requestId of this.pendingPermissionIds) {
1791
+ AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
1792
+ }
1793
+
1794
+ try {
1795
+ await this.connection.cancel({
1796
+ sessionId: normalizedSessionId
1797
+ });
1798
+ this.emitPayload({
1799
+ type: 'session-aborted',
1800
+ success: true
1801
+ });
1802
+ return true;
1803
+ } catch {
1804
+ return false;
1805
+ }
1806
+ }
1807
+
1808
+ async close() {
1809
+ for (const requestId of this.pendingPermissionIds) {
1810
+ AcpClient.resolvePermissionRequest(requestId, { cancelled: true });
1811
+ }
1812
+
1813
+ for (const [terminalId, terminal] of this.terminalProcesses.entries()) {
1814
+ try {
1815
+ await terminal.release();
1816
+ } catch {}
1817
+ this.terminalProcesses.delete(terminalId);
1818
+ }
1819
+
1820
+ if (this.childProcess && !this.childProcess.killed) {
1821
+ if (!this.childProcess.stdin.destroyed) {
1822
+ this.childProcess.stdin.end();
1823
+ }
1824
+
1825
+ const closed = await new Promise((resolve) => {
1826
+ let settled = false;
1827
+ const finalize = (value) => {
1828
+ if (settled) {
1829
+ return;
1830
+ }
1831
+ settled = true;
1832
+ resolve(value);
1833
+ };
1834
+
1835
+ const timer = setTimeout(() => finalize(false), DEFAULT_CLOSE_GRACE_MS);
1836
+ this.childProcess.once('close', () => {
1837
+ clearTimeout(timer);
1838
+ finalize(true);
1839
+ });
1840
+ });
1841
+
1842
+ if (!closed) {
1843
+ try {
1844
+ this.childProcess.kill('SIGTERM');
1845
+ } catch {}
1846
+ }
1847
+ }
1848
+
1849
+ if (this.spawnCleanup) {
1850
+ try {
1851
+ await this.spawnCleanup();
1852
+ } catch {}
1853
+ this.spawnCleanup = null;
1854
+ }
1855
+
1856
+ this.connection = null;
1857
+ this.childProcess = null;
1858
+ this.initializeResult = null;
1859
+ }
1860
+
1861
+ static resolvePermissionRequest(requestId, decision = {}) {
1862
+ const pending = pendingPermissionRequests.get(requestId);
1863
+ if (!pending) {
1864
+ return false;
1865
+ }
1866
+
1867
+ pending.resolve(choosePermissionResponse(pending.request, decision));
1868
+ return true;
1869
+ }
1870
+ }
1871
+
1872
+ export { normalizeTerminalCommandForSpawn };