happy-imou-cloud 2.1.49 → 2.1.51

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 (55) hide show
  1. package/dist/AcpBackend-CqO3D07V.mjs +2619 -0
  2. package/dist/AcpBackend-XPiTd6ph.cjs +2621 -0
  3. package/dist/{BaseReasoningProcessor-Dn9NcoHz.cjs → BaseReasoningProcessor-BD9tiwep.cjs} +1 -144
  4. package/dist/{BaseReasoningProcessor-CAVeOdyo.mjs → BaseReasoningProcessor-CjlayL2f.mjs} +2 -144
  5. package/dist/ConversationHistory-Bl2doTA-.cjs +780 -0
  6. package/dist/ConversationHistory-CI5bBfuA.mjs +771 -0
  7. package/dist/{ProviderSelectionHandler-BJJc7qOR.cjs → ProviderSelectionHandler-C7GE5QjX.cjs} +6 -6
  8. package/dist/{ProviderSelectionHandler-DIYidT13.mjs → ProviderSelectionHandler-uQ8jzdzr.mjs} +2 -2
  9. package/dist/RuntimeShell-BDt42io_.mjs +252 -0
  10. package/dist/RuntimeShell-D_Te12wq.cjs +258 -0
  11. package/dist/bootstrapManagedProviderSession-Bln-TwyB.cjs +147 -0
  12. package/dist/bootstrapManagedProviderSession-D2Z6YU3n.mjs +145 -0
  13. package/dist/claude-BKNT-2fG.cjs +1080 -0
  14. package/dist/claude-CnN5WCWj.mjs +1073 -0
  15. package/dist/codex-DLGP8WF6.mjs +577 -0
  16. package/dist/codex-Fv2eali8.cjs +582 -0
  17. package/dist/{command-VcH4hbhi.cjs → command-BWPlJyCN.cjs} +16 -8
  18. package/dist/{command-CzfRRhVe.mjs → command-CELwsYoG.mjs} +15 -7
  19. package/dist/config-CFL0Gkqt.cjs +184 -0
  20. package/dist/config-ChSPe7p9.mjs +174 -0
  21. package/dist/createDefaultRuntimeShell-BXu3vCvT.cjs +33 -0
  22. package/dist/createDefaultRuntimeShell-DOg6g3-G.mjs +31 -0
  23. package/dist/cursor-Blq1cHdr.cjs +91 -0
  24. package/dist/cursor-CwPNSy_A.mjs +88 -0
  25. package/dist/future-Dq4Ha1Dn.cjs +24 -0
  26. package/dist/future-xRdLl3vf.mjs +22 -0
  27. package/dist/{index-xa1kwZoj.cjs → index-B_JYgMUS.cjs} +189 -5352
  28. package/dist/{index-7Z93BoVn.mjs → index-CX-F_fuk.mjs} +177 -5331
  29. package/dist/index.cjs +2 -2
  30. package/dist/index.mjs +2 -2
  31. package/dist/installFatalProcessHandlers-0vaw9MAz.mjs +55 -0
  32. package/dist/installFatalProcessHandlers-CyURn5Bp.cjs +57 -0
  33. package/dist/launch-BoCCEd5p.mjs +63 -0
  34. package/dist/launch-wZA5BcvS.cjs +66 -0
  35. package/dist/lib.cjs +2 -3
  36. package/dist/lib.d.cts +20 -17
  37. package/dist/lib.d.mts +20 -17
  38. package/dist/lib.mjs +1 -2
  39. package/dist/resolveCommand-B3BGyBE2.mjs +189 -0
  40. package/dist/resolveCommand-DYMd9PNC.cjs +193 -0
  41. package/dist/{runClaude-zCwRhpOw.mjs → runClaude-Be0myF9k.mjs} +8 -5
  42. package/dist/{runClaude-BBGNmGj6.cjs → runClaude-DZJt5er7.cjs} +46 -43
  43. package/dist/{runCodex-BbgLVjb9.mjs → runCodex-BSnyN4m7.mjs} +226 -117
  44. package/dist/{runCodex-jUU6U2tZ.cjs → runCodex-DTCcGRue.cjs} +269 -160
  45. package/dist/runCursor-Bn1PuwJy.cjs +506 -0
  46. package/dist/runCursor-M6dQ6bGF.mjs +504 -0
  47. package/dist/{runGemini-DcwNsudA.mjs → runGemini-BNm4vYKA.mjs} +279 -5
  48. package/dist/{runGemini-C0NT8MHK.cjs → runGemini-Bn3lFhz6.cjs} +309 -35
  49. package/dist/{registerKillSessionHandler-DLDg2EES.mjs → sessionControl-1bT_7OI6.mjs} +1643 -2405
  50. package/dist/{registerKillSessionHandler-CfCya6si.cjs → sessionControl-flKnQrx0.cjs} +1647 -2417
  51. package/dist/{api-DnqaNvyV.mjs → types-B5vtxa38.mjs} +55 -5
  52. package/dist/{api-D7nAeZi7.cjs → types-CttABk32.cjs} +55 -4
  53. package/package.json +2 -2
  54. package/dist/types-CiliQpqS.mjs +0 -52
  55. package/dist/types-DVk3crez.cjs +0 -54
@@ -0,0 +1,2619 @@
1
+ import spawn from 'cross-spawn';
2
+ import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { l as logger, H as HeadTailPreviewBuffer, p as packageJson, e as delay } from './types-B5vtxa38.mjs';
5
+ import { i as isTerminalReferenceOnlyPayload, f as formatDisplayMessage } from './RuntimeShell-BDt42io_.mjs';
6
+ import { D as DefaultTransport, f as resolveAcpSessionPreferences, g as buildAcpSessionConfigPresetPlan, h as resolveAcpPostPromptNoUpdatesTimeoutMs, i as resolveAcpResponseWaitTimeoutMs } from './index-CX-F_fuk.mjs';
7
+ import psList from 'ps-list';
8
+
9
+ const DEFAULT_IDLE_TIMEOUT_MS = 500;
10
+ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 10 * 6e4;
11
+ const DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_HEAD_BYTES = 4096;
12
+ const DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_TAIL_BYTES = 12288;
13
+ function parseArgsFromContent(content) {
14
+ if (Array.isArray(content)) {
15
+ return { items: content };
16
+ }
17
+ if (content && typeof content === "object" && content !== null) {
18
+ return content;
19
+ }
20
+ return {};
21
+ }
22
+ function buildToolCallArgs(_toolKind, content, locations) {
23
+ const args = parseArgsFromContent(content);
24
+ if (Array.isArray(locations)) {
25
+ args.locations = locations;
26
+ }
27
+ return args;
28
+ }
29
+ const MAX_TOOL_OUTPUT_OVERLAP_CHARS = 4096;
30
+ const MAX_TRACKED_TOOL_OUTPUT_EXACT_CHARS = 16384;
31
+ const MAX_TRACKED_TOOL_OUTPUT_HEAD_CHARS = 2048;
32
+ const MAX_TRACKED_TOOL_OUTPUT_TAIL_CHARS = MAX_TOOL_OUTPUT_OVERLAP_CHARS;
33
+ const MAX_TRACKED_TOOL_OUTPUT_SUFFIX_CHARS = 65536;
34
+ function findTrailingNewlineTrimmedLength(text) {
35
+ let trimmedLength = text.length;
36
+ while (trimmedLength > 0) {
37
+ const charCode = text.charCodeAt(trimmedLength - 1);
38
+ if (charCode !== 10 && charCode !== 13) {
39
+ break;
40
+ }
41
+ trimmedLength--;
42
+ }
43
+ return trimmedLength;
44
+ }
45
+ function sliceSnapshotHead(text, length) {
46
+ return text.slice(0, Math.min(length, MAX_TRACKED_TOOL_OUTPUT_HEAD_CHARS));
47
+ }
48
+ function sliceSnapshotTail(text, length) {
49
+ const tailLength = Math.min(length, MAX_TRACKED_TOOL_OUTPUT_TAIL_CHARS);
50
+ return text.slice(Math.max(0, length - tailLength), length);
51
+ }
52
+ function sliceSnapshotSuffixWindow(text, length) {
53
+ const suffixLength = Math.min(length, MAX_TRACKED_TOOL_OUTPUT_SUFFIX_CHARS);
54
+ return text.slice(Math.max(0, length - suffixLength), length);
55
+ }
56
+ function buildToolOutputSnapshot(text) {
57
+ const length = text.length;
58
+ const trimmedLength = findTrailingNewlineTrimmedLength(text);
59
+ return {
60
+ length,
61
+ exact: length <= MAX_TRACKED_TOOL_OUTPUT_EXACT_CHARS ? text : void 0,
62
+ head: sliceSnapshotHead(text, length),
63
+ tail: sliceSnapshotTail(text, length),
64
+ suffixWindow: sliceSnapshotSuffixWindow(text, length),
65
+ trimmedLength,
66
+ trimmedExact: trimmedLength <= MAX_TRACKED_TOOL_OUTPUT_EXACT_CHARS ? text.slice(0, trimmedLength) : void 0,
67
+ trimmedHead: sliceSnapshotHead(text, trimmedLength),
68
+ trimmedTail: sliceSnapshotTail(text, trimmedLength),
69
+ trimmedSuffixWindow: sliceSnapshotSuffixWindow(text, trimmedLength)
70
+ };
71
+ }
72
+ function snapshotMatchesEdges(next, snapshotLength, snapshotHead, snapshotTail) {
73
+ if (next.length < snapshotLength) {
74
+ return false;
75
+ }
76
+ if (snapshotHead.length > 0 && !next.startsWith(snapshotHead)) {
77
+ return false;
78
+ }
79
+ if (snapshotTail.length === 0) {
80
+ return true;
81
+ }
82
+ const tailStart = snapshotLength - snapshotTail.length;
83
+ return next.slice(tailStart, tailStart + snapshotTail.length) === snapshotTail;
84
+ }
85
+ function snapshotMatchesText(snapshot, next, useTrimmedSnapshot = false) {
86
+ const snapshotLength = useTrimmedSnapshot ? snapshot.trimmedLength : snapshot.length;
87
+ if (next.length !== snapshotLength) {
88
+ return false;
89
+ }
90
+ const snapshotExact = useTrimmedSnapshot ? snapshot.trimmedExact : snapshot.exact;
91
+ if (snapshotExact !== void 0) {
92
+ return next === snapshotExact;
93
+ }
94
+ const snapshotHead = useTrimmedSnapshot ? snapshot.trimmedHead : snapshot.head;
95
+ const snapshotTail = useTrimmedSnapshot ? snapshot.trimmedTail : snapshot.tail;
96
+ return snapshotMatchesEdges(next, snapshotLength, snapshotHead, snapshotTail);
97
+ }
98
+ function snapshotIsPrefixOfText(snapshot, next, useTrimmedSnapshot = false) {
99
+ const snapshotLength = useTrimmedSnapshot ? snapshot.trimmedLength : snapshot.length;
100
+ if (next.length < snapshotLength) {
101
+ return false;
102
+ }
103
+ const snapshotExact = useTrimmedSnapshot ? snapshot.trimmedExact : snapshot.exact;
104
+ if (snapshotExact !== void 0) {
105
+ return next.startsWith(snapshotExact);
106
+ }
107
+ const snapshotHead = useTrimmedSnapshot ? snapshot.trimmedHead : snapshot.head;
108
+ const snapshotTail = useTrimmedSnapshot ? snapshot.trimmedTail : snapshot.tail;
109
+ return snapshotMatchesEdges(next, snapshotLength, snapshotHead, snapshotTail);
110
+ }
111
+ function snapshotEndsWithText(snapshot, next) {
112
+ if (next.length === 0) {
113
+ return true;
114
+ }
115
+ if (snapshot.exact !== void 0) {
116
+ return snapshot.exact.endsWith(next);
117
+ }
118
+ return next.length <= snapshot.suffixWindow.length && snapshot.suffixWindow.endsWith(next);
119
+ }
120
+ function findToolOutputOverlap(previous, next) {
121
+ const maxOverlap = Math.min(previous.length, next.length, MAX_TOOL_OUTPUT_OVERLAP_CHARS);
122
+ if (maxOverlap === 0) {
123
+ return 0;
124
+ }
125
+ const previousSuffix = previous.slice(-maxOverlap);
126
+ const nextPrefix = next.slice(0, maxOverlap);
127
+ for (let overlap = maxOverlap; overlap > 0; overlap--) {
128
+ if (previousSuffix.endsWith(nextPrefix.slice(0, overlap))) {
129
+ return overlap;
130
+ }
131
+ }
132
+ return 0;
133
+ }
134
+ function getToolOutputDelta(previous, next) {
135
+ if (!previous || previous.length === 0) {
136
+ return next;
137
+ }
138
+ if (snapshotMatchesText(previous, next) || snapshotEndsWithText(previous, next)) {
139
+ return "";
140
+ }
141
+ if (snapshotIsPrefixOfText(previous, next)) {
142
+ return next.slice(previous.length);
143
+ }
144
+ const overlapSource = previous.exact ?? previous.tail;
145
+ const overlap = findToolOutputOverlap(overlapSource, next);
146
+ return overlap > 0 ? next.slice(overlap) : next;
147
+ }
148
+ function shouldReplaceToolOutput(previous, next) {
149
+ if (!previous || previous.length === 0) {
150
+ return false;
151
+ }
152
+ if (snapshotIsPrefixOfText(previous, next)) {
153
+ return false;
154
+ }
155
+ if (previous.trimmedLength === previous.length) {
156
+ return false;
157
+ }
158
+ return snapshotIsPrefixOfText(previous, next, true);
159
+ }
160
+ function appendToolOutput(existing, next) {
161
+ const shouldReplace = shouldReplaceToolOutput(existing?.lastRawText, next);
162
+ const preview = shouldReplace || !existing?.preview ? new HeadTailPreviewBuffer(
163
+ DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_HEAD_BYTES,
164
+ DEFAULT_TOOL_CALL_OUTPUT_PREVIEW_TAIL_BYTES
165
+ ) : existing.preview;
166
+ let emittedChunk = null;
167
+ if (shouldReplace) {
168
+ preview.append(next);
169
+ } else {
170
+ const textToAppend = getToolOutputDelta(existing?.lastRawText, next);
171
+ if (textToAppend.length > 0) {
172
+ preview.append(textToAppend);
173
+ emittedChunk = textToAppend;
174
+ }
175
+ }
176
+ return {
177
+ accumulator: {
178
+ preview,
179
+ // ACP runtimes sometimes resend the entire cumulative stdout snapshot.
180
+ // Keep only a bounded summary for dedupe so long tool runs cannot retain
181
+ // every historical snapshot in memory.
182
+ lastRawText: buildToolOutputSnapshot(next),
183
+ updateCount: (existing?.updateCount ?? 0) + 1
184
+ },
185
+ emittedChunk
186
+ };
187
+ }
188
+ function renderToolOutput(accumulator) {
189
+ if (!accumulator) {
190
+ return void 0;
191
+ }
192
+ return accumulator.preview.render("tool output");
193
+ }
194
+ function extractTerminalOutputMeta(update) {
195
+ if (!isRecord(update._meta)) {
196
+ return null;
197
+ }
198
+ const terminalOutput = update._meta.terminal_output;
199
+ if (!isRecord(terminalOutput)) {
200
+ return null;
201
+ }
202
+ const data = typeof terminalOutput.data === "string" && terminalOutput.data.length > 0 ? terminalOutput.data : null;
203
+ if (!data) {
204
+ return null;
205
+ }
206
+ const terminalId = typeof terminalOutput.terminal_id === "string" && terminalOutput.terminal_id.length > 0 ? terminalOutput.terminal_id : null;
207
+ const toolCallId = typeof update.toolCallId === "string" && update.toolCallId.length > 0 ? update.toolCallId : terminalId;
208
+ return toolCallId ? { toolCallId, data } : null;
209
+ }
210
+ function isTerminalLikeToolKind(toolKind) {
211
+ if (typeof toolKind !== "string" || toolKind.length === 0) {
212
+ return false;
213
+ }
214
+ const normalized = toolKind.toLowerCase();
215
+ return normalized === "execute" || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command");
216
+ }
217
+ function formatToolCallTimeoutLimit(timeoutMs) {
218
+ if (timeoutMs < 1e3) {
219
+ return `${timeoutMs}ms`;
220
+ }
221
+ return `${(timeoutMs / 1e3).toFixed(Number.isInteger(timeoutMs / 1e3) ? 0 : 1)}s`;
222
+ }
223
+ function isRecord(value) {
224
+ return typeof value === "object" && value !== null && !Array.isArray(value);
225
+ }
226
+ function extractNestedToolContentText(value) {
227
+ if (isTerminalReferenceOnlyPayload(value)) {
228
+ return null;
229
+ }
230
+ if (typeof value === "string") {
231
+ return value.length > 0 ? value : null;
232
+ }
233
+ if (Array.isArray(value)) {
234
+ const parts = value.map((item) => extractNestedToolContentText(item)).filter((item) => Boolean(item));
235
+ return parts.length > 0 ? parts.join("") : null;
236
+ }
237
+ if (!isRecord(value)) {
238
+ return null;
239
+ }
240
+ if (typeof value.text === "string" && value.text.length > 0) {
241
+ return value.text;
242
+ }
243
+ if ("content" in value) {
244
+ return extractNestedToolContentText(value.content);
245
+ }
246
+ return null;
247
+ }
248
+ function hasMeaningfulContent(value) {
249
+ if (value === null || value === void 0) {
250
+ return false;
251
+ }
252
+ if (typeof value === "string") {
253
+ return value.length > 0;
254
+ }
255
+ if (Array.isArray(value)) {
256
+ return value.length > 0;
257
+ }
258
+ if (isRecord(value)) {
259
+ return Object.keys(value).length > 0;
260
+ }
261
+ return true;
262
+ }
263
+ function looksLikeToolMetadata(record) {
264
+ const metadataKeys = [
265
+ "command",
266
+ "cmd",
267
+ "script",
268
+ "argv",
269
+ "cwd",
270
+ "workingDirectory",
271
+ "description",
272
+ "title",
273
+ "parsed_cmd"
274
+ ];
275
+ if (metadataKeys.some((key) => key in record)) {
276
+ return true;
277
+ }
278
+ const nestedKeys = ["input", "toolCall", "arguments", "content"];
279
+ for (const key of nestedKeys) {
280
+ const nested = record[key];
281
+ if (isRecord(nested) && looksLikeToolMetadata(nested)) {
282
+ return true;
283
+ }
284
+ }
285
+ return false;
286
+ }
287
+ function extractToolOutputChunk(content) {
288
+ if (typeof content === "string") {
289
+ return content.length > 0 ? content : null;
290
+ }
291
+ if (Array.isArray(content)) {
292
+ const parts = content.map((item) => extractToolOutputChunk(item)).filter((item) => Boolean(item));
293
+ return parts.length > 0 ? parts.join("") : null;
294
+ }
295
+ if (!isRecord(content)) {
296
+ return null;
297
+ }
298
+ if (isTerminalReferenceOnlyPayload(content)) {
299
+ return null;
300
+ }
301
+ const outputKeys = ["stdout", "stderr", "output", "text", "message", "data", "error", "reason"];
302
+ const hasOutputKey = outputKeys.some((key) => key in content);
303
+ if (!hasOutputKey && looksLikeToolMetadata(content)) {
304
+ return null;
305
+ }
306
+ for (const key of outputKeys) {
307
+ if (!(key in content)) {
308
+ continue;
309
+ }
310
+ const value = content[key];
311
+ const nestedText2 = extractNestedToolContentText(value);
312
+ if (nestedText2 && nestedText2.length > 0) {
313
+ return nestedText2;
314
+ }
315
+ const formatted2 = typeof value === "string" ? value : formatDisplayMessage(value);
316
+ if (formatted2.length > 0) {
317
+ return formatted2;
318
+ }
319
+ }
320
+ const nestedText = extractNestedToolContentText(content);
321
+ if (nestedText && nestedText.length > 0) {
322
+ return nestedText;
323
+ }
324
+ const formatted = formatDisplayMessage(content);
325
+ return formatted.length > 0 ? formatted : null;
326
+ }
327
+ function hasVisibleToolCallProgress(content) {
328
+ return extractToolOutputChunk(content) !== null;
329
+ }
330
+ function mergeStreamedOutputWithResult(content, streamedOutput) {
331
+ if (!streamedOutput || streamedOutput.length === 0) {
332
+ return content;
333
+ }
334
+ if (!hasMeaningfulContent(content)) {
335
+ return streamedOutput;
336
+ }
337
+ if (isRecord(content)) {
338
+ const hasStructuredOutput = ["stdout", "stderr", "output", "text", "message", "data"].some((key) => key in content);
339
+ if (!hasStructuredOutput) {
340
+ return {
341
+ ...content,
342
+ stdout: streamedOutput
343
+ };
344
+ }
345
+ }
346
+ return content;
347
+ }
348
+ function buildCompletedToolResult(toolKind, content, streamedOutput) {
349
+ if (isTerminalLikeToolKind(toolKind) && isTerminalReferenceOnlyPayload(content)) {
350
+ return void 0;
351
+ }
352
+ return mergeStreamedOutputWithResult(content, streamedOutput);
353
+ }
354
+ function extractErrorDetail(content) {
355
+ if (!content) return void 0;
356
+ if (typeof content === "string") {
357
+ return content;
358
+ }
359
+ if (typeof content === "object" && content !== null && !Array.isArray(content)) {
360
+ const obj = content;
361
+ if (obj.error) {
362
+ const error = obj.error;
363
+ if (typeof error === "string") return error;
364
+ if (error && typeof error === "object" && "message" in error) {
365
+ const errObj = error;
366
+ if (typeof errObj.message === "string") return errObj.message;
367
+ }
368
+ return JSON.stringify(error);
369
+ }
370
+ if (typeof obj.message === "string") return obj.message;
371
+ const status = typeof obj.status === "string" ? obj.status : void 0;
372
+ const reason = typeof obj.reason === "string" ? obj.reason : void 0;
373
+ return status || reason || JSON.stringify(obj).substring(0, 500);
374
+ }
375
+ return void 0;
376
+ }
377
+ function formatDuration(startTime) {
378
+ if (!startTime) return "unknown";
379
+ const duration = Date.now() - startTime;
380
+ return `${(duration / 1e3).toFixed(2)}s`;
381
+ }
382
+ function formatDurationMinutes(startTime) {
383
+ if (!startTime) return "unknown";
384
+ const duration = Date.now() - startTime;
385
+ return (duration / 1e3 / 60).toFixed(2);
386
+ }
387
+ function handleAgentMessageChunk(update, ctx) {
388
+ const content = update.content;
389
+ if (!content || typeof content !== "object" || !("text" in content)) {
390
+ return { handled: false };
391
+ }
392
+ const text = content.text;
393
+ if (typeof text !== "string") {
394
+ return { handled: false };
395
+ }
396
+ const isThinking = /^\*\*[^*]+\*\*\n/.test(text);
397
+ if (isThinking) {
398
+ ctx.emit({
399
+ type: "event",
400
+ name: "thinking",
401
+ payload: { text }
402
+ });
403
+ } else {
404
+ logger.debug(`[AcpBackend] Received message chunk (length: ${text.length}): ${text.substring(0, 50)}...`);
405
+ ctx.emit({
406
+ type: "model-output",
407
+ textDelta: text
408
+ });
409
+ ctx.clearIdleTimeout();
410
+ const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
411
+ ctx.setIdleTimeout(() => {
412
+ if (ctx.activeToolCalls.size === 0) {
413
+ logger.debug("[AcpBackend] No more chunks received, emitting idle status");
414
+ ctx.emitIdleStatus();
415
+ } else {
416
+ logger.debug(`[AcpBackend] Delaying idle status - ${ctx.activeToolCalls.size} active tool calls`);
417
+ }
418
+ }, idleTimeoutMs);
419
+ }
420
+ return { handled: true };
421
+ }
422
+ function handleAgentThoughtChunk(update, ctx) {
423
+ const content = update.content;
424
+ if (!content || typeof content !== "object" || !("text" in content)) {
425
+ return { handled: false };
426
+ }
427
+ const text = content.text;
428
+ if (typeof text !== "string") {
429
+ return { handled: false };
430
+ }
431
+ if (ctx.activeToolCalls.size > 0) {
432
+ const activeToolCallsList = Array.from(ctx.activeToolCalls);
433
+ logger.debug(`[AcpBackend] \u{1F4AD} Thinking chunk received (${text.length} chars) during active tool calls: ${activeToolCallsList.join(", ")}`);
434
+ }
435
+ ctx.emit({
436
+ type: "event",
437
+ name: "thinking",
438
+ payload: { text }
439
+ });
440
+ return { handled: true };
441
+ }
442
+ function startToolCall(toolCallId, toolKind, update, ctx, source) {
443
+ const startTime = Date.now();
444
+ const toolKindStr = typeof toolKind === "string" ? toolKind : void 0;
445
+ const isInvestigation = ctx.transport.isInvestigationTool?.(toolCallId, toolKindStr) ?? false;
446
+ const extractedName = ctx.transport.extractToolNameFromId?.(toolCallId);
447
+ const realToolName = extractedName ?? (toolKindStr || "unknown");
448
+ ctx.toolCallIdToNameMap.set(toolCallId, realToolName);
449
+ ctx.activeToolCalls.add(toolCallId);
450
+ ctx.toolCallStartTimes.set(toolCallId, startTime);
451
+ logger.debug(`[AcpBackend] \u23F1\uFE0F Set startTime for ${toolCallId} at ${new Date(startTime).toISOString()} (from ${source})`);
452
+ logger.debug(`[AcpBackend] \u{1F527} Tool call START: ${toolCallId} (${toolKind} -> ${realToolName})${isInvestigation ? " [INVESTIGATION TOOL]" : ""}`);
453
+ if (isInvestigation) {
454
+ logger.debug(`[AcpBackend] \u{1F50D} Investigation tool detected - extended timeout (10min) will be used`);
455
+ }
456
+ const timeoutMs = ctx.transport.getToolCallTimeout?.(toolCallId, toolKindStr) ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
457
+ if (!ctx.toolCallTimeouts.has(toolCallId)) {
458
+ ctx.armToolCallTimeout({
459
+ toolCallId,
460
+ toolKind,
461
+ toolName: realToolName,
462
+ timeoutMs,
463
+ source
464
+ });
465
+ logger.debug(`[AcpBackend] \u23F1\uFE0F Set no-progress timeout for ${toolCallId}: ${(timeoutMs / 1e3).toFixed(0)}s${isInvestigation ? " (investigation tool)" : ""}`);
466
+ } else {
467
+ logger.debug(`[AcpBackend] Timeout already set for ${toolCallId}, skipping`);
468
+ }
469
+ ctx.clearIdleTimeout();
470
+ ctx.emit({ type: "status", status: "running" });
471
+ const args = buildToolCallArgs(toolKind, update.content, update.locations);
472
+ if (isInvestigation && args.objective) {
473
+ logger.debug(`[AcpBackend] \u{1F50D} Investigation tool objective: ${String(args.objective).substring(0, 100)}...`);
474
+ }
475
+ ctx.emit({
476
+ type: "tool-call",
477
+ toolName: toolKindStr || "unknown",
478
+ args,
479
+ callId: toolCallId
480
+ });
481
+ }
482
+ function completeToolCall(toolCallId, toolKind, content, ctx) {
483
+ const startTime = ctx.toolCallStartTimes.get(toolCallId);
484
+ const duration = formatDuration(startTime);
485
+ const toolKindStr = typeof toolKind === "string" && toolKind !== "unknown" ? toolKind : ctx.toolCallIdToNameMap.get(toolCallId) || "unknown";
486
+ ctx.activeToolCalls.delete(toolCallId);
487
+ ctx.toolCallStartTimes.delete(toolCallId);
488
+ ctx.toolCallIdToNameMap.delete(toolCallId);
489
+ ctx.clearToolCallTimeout(toolCallId);
490
+ const streamedOutput = renderToolOutput(ctx.toolCallOutputs.get(toolCallId));
491
+ ctx.toolCallOutputs.delete(toolCallId);
492
+ logger.debug(`[AcpBackend] \u2705 Tool call COMPLETED: ${toolCallId} (${toolKindStr}) - Duration: ${duration}. Active tool calls: ${ctx.activeToolCalls.size}`);
493
+ ctx.emit({
494
+ type: "tool-result",
495
+ toolName: toolKindStr,
496
+ result: buildCompletedToolResult(toolKindStr, content, streamedOutput),
497
+ callId: toolCallId
498
+ });
499
+ if (ctx.activeToolCalls.size === 0) {
500
+ ctx.clearIdleTimeout();
501
+ logger.debug("[AcpBackend] All tool calls completed, emitting idle status");
502
+ ctx.emitIdleStatus();
503
+ }
504
+ }
505
+ function failToolCall(toolCallId, status, toolKind, content, ctx) {
506
+ const startTime = ctx.toolCallStartTimes.get(toolCallId);
507
+ const duration = startTime ? Date.now() - startTime : null;
508
+ const toolKindStr = typeof toolKind === "string" && toolKind !== "unknown" ? toolKind : ctx.toolCallIdToNameMap.get(toolCallId) || "unknown";
509
+ const isInvestigation = ctx.transport.isInvestigationTool?.(toolCallId, toolKindStr) ?? false;
510
+ const hadTimeout = ctx.toolCallTimeouts.has(toolCallId);
511
+ if (isInvestigation) {
512
+ const durationStr2 = formatDuration(startTime);
513
+ const durationMinutes = formatDurationMinutes(startTime);
514
+ logger.debug(`[AcpBackend] \u{1F50D} Investigation tool ${status.toUpperCase()} after ${durationMinutes} minutes (${durationStr2})`);
515
+ if (duration) {
516
+ const threeMinutes = 3 * 60 * 1e3;
517
+ const tolerance = 5e3;
518
+ if (Math.abs(duration - threeMinutes) < tolerance) {
519
+ logger.debug(`[AcpBackend] \u{1F50D} \u26A0\uFE0F Investigation tool failed at ~3 minutes - likely Gemini CLI timeout, not our timeout`);
520
+ }
521
+ }
522
+ logger.debug(`[AcpBackend] \u{1F50D} Investigation tool FAILED - full content:`, JSON.stringify(content, null, 2));
523
+ logger.debug(`[AcpBackend] \u{1F50D} Investigation tool timeout status BEFORE cleanup: ${hadTimeout ? "timeout was set" : "no timeout was set"}`);
524
+ logger.debug(`[AcpBackend] \u{1F50D} Investigation tool startTime status BEFORE cleanup: ${startTime ? `set at ${new Date(startTime).toISOString()}` : "not set"}`);
525
+ }
526
+ ctx.activeToolCalls.delete(toolCallId);
527
+ ctx.toolCallStartTimes.delete(toolCallId);
528
+ ctx.toolCallIdToNameMap.delete(toolCallId);
529
+ const clearedTimeout = ctx.clearToolCallTimeout(toolCallId);
530
+ if (clearedTimeout) {
531
+ logger.debug(`[AcpBackend] Cleared timeout for ${toolCallId} (tool call ${status})`);
532
+ } else {
533
+ logger.debug(`[AcpBackend] No timeout found for ${toolCallId} (tool call ${status}) - timeout may not have been set`);
534
+ }
535
+ const durationStr = formatDuration(startTime);
536
+ logger.debug(`[AcpBackend] \u274C Tool call ${status.toUpperCase()}: ${toolCallId} (${toolKindStr}) - Duration: ${durationStr}. Active tool calls: ${ctx.activeToolCalls.size}`);
537
+ const streamedOutput = renderToolOutput(ctx.toolCallOutputs.get(toolCallId));
538
+ ctx.toolCallOutputs.delete(toolCallId);
539
+ const errorDetail = extractErrorDetail(content);
540
+ if (errorDetail) {
541
+ logger.debug(`[AcpBackend] \u274C Tool call error details: ${errorDetail.substring(0, 500)}`);
542
+ } else {
543
+ logger.debug(`[AcpBackend] \u274C Tool call ${status} but no error details in content`);
544
+ }
545
+ ctx.emit({
546
+ type: "tool-result",
547
+ toolName: toolKindStr,
548
+ result: streamedOutput ? {
549
+ stdout: streamedOutput,
550
+ error: errorDetail || `Tool call ${status}`,
551
+ status
552
+ } : errorDetail ? { error: errorDetail, status } : { error: `Tool call ${status}`, status },
553
+ callId: toolCallId
554
+ });
555
+ if (ctx.activeToolCalls.size === 0) {
556
+ ctx.clearIdleTimeout();
557
+ logger.debug("[AcpBackend] All tool calls completed/failed, emitting idle status");
558
+ ctx.emitIdleStatus();
559
+ }
560
+ }
561
+ function handleToolCallUpdate(update, ctx, options) {
562
+ const status = update.status;
563
+ const toolCallId = update.toolCallId;
564
+ if (!toolCallId) {
565
+ logger.debug("[AcpBackend] Tool call update without toolCallId:", update);
566
+ return { handled: false };
567
+ }
568
+ const toolKind = update.kind || ctx.toolCallIdToNameMap.get(toolCallId) || "unknown";
569
+ let toolCallCountSincePrompt = ctx.toolCallCountSincePrompt;
570
+ const hasSupplementalTerminalOutput = typeof options?.supplementalOutputChunk === "string" && options.supplementalOutputChunk.length > 0;
571
+ const outputChunk = extractToolOutputChunk(update.content) ?? options?.supplementalOutputChunk ?? null;
572
+ if (status === "in_progress" || status === "pending") {
573
+ if (!ctx.activeToolCalls.has(toolCallId)) {
574
+ toolCallCountSincePrompt++;
575
+ startToolCall(toolCallId, toolKind, update, ctx, "tool_call_update");
576
+ } else {
577
+ logger.debug(`[AcpBackend] Tool call ${toolCallId} already tracked, status: ${status}`);
578
+ }
579
+ }
580
+ if (outputChunk) {
581
+ const nextOutput = appendToolOutput(ctx.toolCallOutputs.get(toolCallId), outputChunk);
582
+ ctx.toolCallOutputs.set(toolCallId, nextOutput.accumulator);
583
+ if (ctx.activeToolCalls.has(toolCallId) && nextOutput.emittedChunk && (hasSupplementalTerminalOutput || isTerminalLikeToolKind(toolKind))) {
584
+ ctx.emit({
585
+ type: "terminal-output",
586
+ data: nextOutput.emittedChunk,
587
+ callId: toolCallId
588
+ });
589
+ }
590
+ }
591
+ if (status === "completed") {
592
+ completeToolCall(toolCallId, toolKind, update.content, ctx);
593
+ } else if (status === "failed" || status === "cancelled") {
594
+ failToolCall(toolCallId, status, toolKind, update.content, ctx);
595
+ }
596
+ return {
597
+ handled: true,
598
+ toolCallCountSincePrompt,
599
+ hadVisibleProgress: outputChunk !== null
600
+ };
601
+ }
602
+ function extractSupplementalToolProgress(update) {
603
+ const terminalOutput = extractTerminalOutputMeta(update);
604
+ if (!terminalOutput) {
605
+ return null;
606
+ }
607
+ return {
608
+ toolCallId: terminalOutput.toolCallId,
609
+ outputChunk: terminalOutput.data
610
+ };
611
+ }
612
+ function handleToolCall(update, ctx) {
613
+ const toolCallId = update.toolCallId;
614
+ const status = update.status;
615
+ logger.debug(`[AcpBackend] Received tool_call: toolCallId=${toolCallId}, status=${status}, kind=${update.kind}`);
616
+ const isInProgress = !status || status === "in_progress" || status === "pending";
617
+ if (!toolCallId || !isInProgress) {
618
+ logger.debug(`[AcpBackend] Tool call ${toolCallId} not in progress (status: ${status}), skipping`);
619
+ return { handled: false };
620
+ }
621
+ if (ctx.activeToolCalls.has(toolCallId)) {
622
+ logger.debug(`[AcpBackend] Tool call ${toolCallId} already in active set, skipping`);
623
+ return { handled: true };
624
+ }
625
+ startToolCall(toolCallId, update.kind, update, ctx, "tool_call");
626
+ return { handled: true };
627
+ }
628
+ function handleLegacyMessageChunk(update, ctx) {
629
+ if (!update.messageChunk) {
630
+ return { handled: false };
631
+ }
632
+ const chunk = update.messageChunk;
633
+ if (chunk.textDelta) {
634
+ ctx.emit({
635
+ type: "model-output",
636
+ textDelta: chunk.textDelta
637
+ });
638
+ return { handled: true };
639
+ }
640
+ return { handled: false };
641
+ }
642
+ function handlePlanUpdate(update, ctx) {
643
+ if (!update.plan) {
644
+ return { handled: false };
645
+ }
646
+ ctx.emit({
647
+ type: "event",
648
+ name: "plan",
649
+ payload: update.plan
650
+ });
651
+ return { handled: true };
652
+ }
653
+ function handleThinkingUpdate(update, ctx) {
654
+ if (!update.thinking) {
655
+ return { handled: false };
656
+ }
657
+ ctx.emit({
658
+ type: "event",
659
+ name: "thinking",
660
+ payload: update.thinking
661
+ });
662
+ return { handled: true };
663
+ }
664
+
665
+ function buildAcpSpawnSpec(params) {
666
+ return {
667
+ command: params.command,
668
+ args: (params.args ?? []).map((arg) => String(arg)),
669
+ options: {
670
+ cwd: params.cwd,
671
+ env: params.env,
672
+ stdio: ["pipe", "pipe", "pipe"],
673
+ windowsHide: process.platform === "win32"
674
+ }
675
+ };
676
+ }
677
+
678
+ const MAX_DROPPED_LINE_PREVIEW_CHARS = 4096;
679
+ const DEFAULT_ACP_MAX_MULTILINE_STDOUT_BYTES = 32 * 1024;
680
+ function formatDroppedLinePreview(line) {
681
+ if (line.length <= MAX_DROPPED_LINE_PREVIEW_CHARS) {
682
+ return line;
683
+ }
684
+ const headChars = 2048;
685
+ const tailChars = 1024;
686
+ const omittedChars = Math.max(0, line.length - headChars - tailChars);
687
+ return `${line.slice(0, headChars)}
688
+ ...[stdout line truncated ${omittedChars} chars]...
689
+ ${line.slice(-tailChars)}`;
690
+ }
691
+ function createAcpFilteredStdoutReadable(params) {
692
+ const maxMultilineBytes = typeof params.maxMultilineBytes === "number" && Number.isFinite(params.maxMultilineBytes) && params.maxMultilineBytes > 0 ? Math.trunc(params.maxMultilineBytes) : DEFAULT_ACP_MAX_MULTILINE_STDOUT_BYTES;
693
+ const encoder = new TextEncoder();
694
+ return new ReadableStream({
695
+ async start(controller) {
696
+ const reader = params.readable.getReader();
697
+ let buffer = Buffer.alloc(0);
698
+ let multiline = null;
699
+ let discardingOversizedLine = false;
700
+ let controllerErrored = false;
701
+ const drop = (reason, line) => {
702
+ params.onDroppedLine?.({ reason, line: formatDroppedLinePreview(line) });
703
+ };
704
+ const enqueueLine = (line) => {
705
+ if (!line.trim()) {
706
+ return;
707
+ }
708
+ const filtered = params.transport.filterStdoutLine?.(line);
709
+ if (filtered === void 0) {
710
+ controller.enqueue(encoder.encode(`${line}
711
+ `));
712
+ return;
713
+ }
714
+ if (filtered === null) {
715
+ drop("transport_filter_null", line);
716
+ return;
717
+ }
718
+ controller.enqueue(encoder.encode(`${filtered}
719
+ `));
720
+ };
721
+ const tryFlushMultiline = (candidate) => {
722
+ const trimmed = candidate.trim();
723
+ if (!trimmed) {
724
+ return false;
725
+ }
726
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
727
+ return false;
728
+ }
729
+ try {
730
+ const parsed = JSON.parse(trimmed);
731
+ if (typeof parsed !== "object" || parsed === null) {
732
+ return false;
733
+ }
734
+ enqueueLine(JSON.stringify(parsed));
735
+ return true;
736
+ } catch {
737
+ return false;
738
+ }
739
+ };
740
+ const consumeLine = (line, lineBytes) => {
741
+ if (multiline) {
742
+ const nextBuf = multiline.buf.length > 0 ? `${multiline.buf}
743
+ ${line}` : line;
744
+ const nextBytes = multiline.bytes + lineBytes + 1;
745
+ if (nextBytes > maxMultilineBytes) {
746
+ drop("multiline_overflow", multiline.buf);
747
+ multiline = null;
748
+ if (lineBytes > maxMultilineBytes) {
749
+ drop("multiline_overflow", line);
750
+ return;
751
+ }
752
+ enqueueLine(line);
753
+ return;
754
+ }
755
+ multiline = { buf: nextBuf, bytes: nextBytes };
756
+ if (tryFlushMultiline(multiline.buf)) {
757
+ multiline = null;
758
+ }
759
+ return;
760
+ }
761
+ if (lineBytes > maxMultilineBytes) {
762
+ drop("multiline_overflow", line);
763
+ return;
764
+ }
765
+ const trimmed = line.trim();
766
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
767
+ try {
768
+ const parsed = JSON.parse(trimmed);
769
+ if (typeof parsed === "object" && parsed !== null) {
770
+ enqueueLine(line);
771
+ return;
772
+ }
773
+ } catch {
774
+ multiline = { buf: line, bytes: lineBytes };
775
+ return;
776
+ }
777
+ }
778
+ enqueueLine(line);
779
+ };
780
+ try {
781
+ while (true) {
782
+ const { value, done } = await reader.read();
783
+ if (done) {
784
+ break;
785
+ }
786
+ if (!value) {
787
+ continue;
788
+ }
789
+ let chunkBuffer = Buffer.from(value);
790
+ if (discardingOversizedLine) {
791
+ const newlineIndex2 = chunkBuffer.indexOf(10);
792
+ if (newlineIndex2 === -1) {
793
+ continue;
794
+ }
795
+ chunkBuffer = chunkBuffer.subarray(newlineIndex2 + 1);
796
+ discardingOversizedLine = false;
797
+ }
798
+ if (chunkBuffer.length === 0) {
799
+ continue;
800
+ }
801
+ buffer = buffer.length === 0 ? chunkBuffer : Buffer.concat([buffer, chunkBuffer]);
802
+ let newlineIndex = buffer.indexOf(10);
803
+ while (newlineIndex !== -1) {
804
+ const rawLine = buffer.subarray(0, newlineIndex);
805
+ buffer = buffer.subarray(newlineIndex + 1);
806
+ const lineBuffer = rawLine.at(-1) === 13 ? rawLine.subarray(0, rawLine.length - 1) : rawLine;
807
+ const line = lineBuffer.toString("utf8");
808
+ consumeLine(line, lineBuffer.length);
809
+ newlineIndex = buffer.indexOf(10);
810
+ }
811
+ if (buffer.length > maxMultilineBytes) {
812
+ drop("multiline_overflow", buffer.toString("utf8"));
813
+ buffer = Buffer.alloc(0);
814
+ discardingOversizedLine = true;
815
+ }
816
+ }
817
+ if (buffer.length > 0) {
818
+ const trailingLine = buffer.toString("utf8");
819
+ const pendingMultiline = multiline;
820
+ if (pendingMultiline) {
821
+ const nextBuf = pendingMultiline.buf.length > 0 ? `${pendingMultiline.buf}
822
+ ${trailingLine}` : trailingLine;
823
+ if (!tryFlushMultiline(nextBuf)) {
824
+ drop("multiline_incomplete", nextBuf);
825
+ }
826
+ multiline = null;
827
+ } else if (trailingLine.trim()) {
828
+ drop("multiline_incomplete", trailingLine);
829
+ }
830
+ }
831
+ const remainingMultiline = multiline;
832
+ if (remainingMultiline) {
833
+ if (!tryFlushMultiline(remainingMultiline.buf)) {
834
+ drop("multiline_incomplete", remainingMultiline.buf);
835
+ }
836
+ multiline = null;
837
+ }
838
+ } catch (error) {
839
+ controllerErrored = true;
840
+ controller.error(error);
841
+ } finally {
842
+ reader.releaseLock();
843
+ try {
844
+ params.onDone?.();
845
+ } catch {
846
+ }
847
+ if (!controllerErrored) {
848
+ controller.close();
849
+ }
850
+ }
851
+ }
852
+ });
853
+ }
854
+
855
+ function isAlive(pid) {
856
+ try {
857
+ process.kill(pid, 0);
858
+ return true;
859
+ } catch {
860
+ return false;
861
+ }
862
+ }
863
+ async function resolveDescendantPids(rootPid) {
864
+ const processes = await psList();
865
+ const childrenByParent = /* @__PURE__ */ new Map();
866
+ for (const proc of processes) {
867
+ if (typeof proc.pid !== "number" || typeof proc.ppid !== "number") {
868
+ continue;
869
+ }
870
+ const children = childrenByParent.get(proc.ppid) ?? [];
871
+ children.push(proc.pid);
872
+ childrenByParent.set(proc.ppid, children);
873
+ }
874
+ const descendants = [];
875
+ const seen = /* @__PURE__ */ new Set();
876
+ const visit = (pid) => {
877
+ for (const childPid of childrenByParent.get(pid) ?? []) {
878
+ if (seen.has(childPid)) {
879
+ continue;
880
+ }
881
+ seen.add(childPid);
882
+ visit(childPid);
883
+ descendants.push(childPid);
884
+ }
885
+ };
886
+ visit(rootPid);
887
+ return descendants;
888
+ }
889
+ function bestEffortKillPid(pid, signal) {
890
+ try {
891
+ process.kill(pid, signal);
892
+ } catch {
893
+ }
894
+ }
895
+ async function waitForAllGone(pids, timeoutMs) {
896
+ const start = Date.now();
897
+ while (Date.now() - start < timeoutMs) {
898
+ if (pids.every((pid) => !isAlive(pid))) {
899
+ return;
900
+ }
901
+ await new Promise((resolve) => setTimeout(resolve, 25));
902
+ }
903
+ }
904
+ async function killProcessTree(proc, options) {
905
+ const pid = proc.pid;
906
+ if (!pid) {
907
+ return;
908
+ }
909
+ const graceMs = Math.max(1, options?.graceMs ?? 1e3);
910
+ const descendants = await resolveDescendantPids(pid).catch(() => []);
911
+ const allPids = [...descendants, pid];
912
+ for (const targetPid of allPids) {
913
+ bestEffortKillPid(targetPid, "SIGTERM");
914
+ }
915
+ await waitForAllGone(allPids, graceMs);
916
+ const remaining = allPids.filter((targetPid) => isAlive(targetPid));
917
+ if (remaining.length === 0) {
918
+ return;
919
+ }
920
+ for (const targetPid of remaining) {
921
+ bestEffortKillPid(targetPid, "SIGKILL");
922
+ }
923
+ await waitForAllGone(remaining, Math.min(250, graceMs));
924
+ }
925
+
926
+ const RETRY_CONFIG = {
927
+ /** Maximum number of retry attempts for init/newSession */
928
+ maxAttempts: 3,
929
+ /** Base delay between retries in ms */
930
+ baseDelayMs: 1e3,
931
+ /** Maximum delay between retries in ms */
932
+ maxDelayMs: 5e3
933
+ };
934
+ const DEFAULT_POST_PROMPT_NO_UPDATES_TIMEOUT_MS = 2 * 6e4;
935
+ const MAX_DROPPED_STDOUT_LINE_SAMPLES = 5;
936
+ const HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV = "HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES";
937
+ const HAPPIER_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV = "HAPPIER_ACP_MAX_MULTILINE_STDOUT_BYTES";
938
+ function readPositiveIntegerEnv(name) {
939
+ const raw = typeof process.env[name] === "string" ? process.env[name].trim() : "";
940
+ if (!raw) {
941
+ return null;
942
+ }
943
+ const value = Number(raw);
944
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
945
+ return null;
946
+ }
947
+ return value;
948
+ }
949
+ function resolvePostPromptNoUpdatesTimeoutMs(transport) {
950
+ const envValue = readPositiveIntegerEnv("HAPPY_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS") ?? readPositiveIntegerEnv("HAPPIER_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS");
951
+ if (envValue != null) {
952
+ return envValue;
953
+ }
954
+ const transportValue = transport.getPostPromptNoUpdatesTimeoutMs?.();
955
+ if (typeof transportValue === "number" && Number.isFinite(transportValue) && transportValue > 0) {
956
+ return Math.trunc(transportValue);
957
+ }
958
+ return DEFAULT_POST_PROMPT_NO_UPDATES_TIMEOUT_MS;
959
+ }
960
+ function resolveAcpMaxMultilineStdoutBytes() {
961
+ const envValue = readPositiveIntegerEnv(HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV) ?? readPositiveIntegerEnv(HAPPIER_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV);
962
+ return envValue ?? DEFAULT_ACP_MAX_MULTILINE_STDOUT_BYTES;
963
+ }
964
+ function formatAcpStdoutByteLimit(bytes) {
965
+ if (bytes >= 1024 * 1024) {
966
+ const value = bytes / (1024 * 1024);
967
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} MiB`;
968
+ }
969
+ if (bytes >= 1024) {
970
+ const value = bytes / 1024;
971
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} KiB`;
972
+ }
973
+ return `${bytes} bytes`;
974
+ }
975
+ function buildMultilineOverflowStatusDetail(maxBytes) {
976
+ const formattedLimit = formatAcpStdoutByteLimit(maxBytes);
977
+ return `Suppressed oversized agent output (>${formattedLimit} / ${maxBytes} bytes per ACP stdout record). Full output stays hidden by default. If you need larger payloads, set ${HAPPY_ACP_MAX_MULTILINE_STDOUT_BYTES_ENV}.`;
978
+ }
979
+ function getSessionUpdates(params) {
980
+ const notification = params;
981
+ const updates = Array.isArray(notification.updates) ? notification.updates.filter((update) => Boolean(update && typeof update === "object")) : [];
982
+ if (updates.length > 0) {
983
+ return updates;
984
+ }
985
+ if (notification.update && typeof notification.update === "object") {
986
+ return [notification.update];
987
+ }
988
+ return [];
989
+ }
990
+ function asNonNegativeFiniteNumber(value) {
991
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
992
+ }
993
+ function shouldRefreshProgressForToolCallUpdate(update, wasToolCallActive, isToolCallActive, hadVisibleProgress = false) {
994
+ if (!wasToolCallActive && isToolCallActive) {
995
+ return true;
996
+ }
997
+ if (update.status === "completed" || update.status === "failed" || update.status === "cancelled") {
998
+ return true;
999
+ }
1000
+ return hadVisibleProgress || hasVisibleToolCallProgress(update.content);
1001
+ }
1002
+ function extractUsageTokens(record) {
1003
+ const used = asNonNegativeFiniteNumber(record.used);
1004
+ const size = asNonNegativeFiniteNumber(record.size);
1005
+ if (used != null || size != null) {
1006
+ const tokens2 = {
1007
+ total: used ?? 0
1008
+ };
1009
+ if (used != null) tokens2.used = used;
1010
+ if (size != null) tokens2.size = size;
1011
+ return tokens2;
1012
+ }
1013
+ const input = asNonNegativeFiniteNumber(record.input_tokens) ?? asNonNegativeFiniteNumber(record.inputTokens) ?? asNonNegativeFiniteNumber(record.prompt_tokens) ?? asNonNegativeFiniteNumber(record.promptTokens);
1014
+ const output = asNonNegativeFiniteNumber(record.output_tokens) ?? asNonNegativeFiniteNumber(record.outputTokens) ?? asNonNegativeFiniteNumber(record.completion_tokens) ?? asNonNegativeFiniteNumber(record.completionTokens);
1015
+ const cacheRead = asNonNegativeFiniteNumber(record.cache_read_input_tokens) ?? asNonNegativeFiniteNumber(record.cacheReadInputTokens) ?? asNonNegativeFiniteNumber(record.cache_read_tokens) ?? asNonNegativeFiniteNumber(record.cacheReadTokens) ?? asNonNegativeFiniteNumber(record.cached_read_tokens) ?? asNonNegativeFiniteNumber(record.cachedReadTokens);
1016
+ const cacheCreation = asNonNegativeFiniteNumber(record.cache_creation_input_tokens) ?? asNonNegativeFiniteNumber(record.cacheCreationInputTokens) ?? asNonNegativeFiniteNumber(record.cache_creation_tokens) ?? asNonNegativeFiniteNumber(record.cacheCreationTokens) ?? asNonNegativeFiniteNumber(record.cached_write_tokens) ?? asNonNegativeFiniteNumber(record.cachedWriteTokens);
1017
+ const thought = asNonNegativeFiniteNumber(record.thought_tokens) ?? asNonNegativeFiniteNumber(record.thoughtTokens);
1018
+ const totalFromPayload = asNonNegativeFiniteNumber(record.total_tokens) ?? asNonNegativeFiniteNumber(record.totalTokens);
1019
+ const anyPresent = totalFromPayload != null || input != null || output != null || cacheRead != null || cacheCreation != null || thought != null;
1020
+ if (!anyPresent) {
1021
+ return null;
1022
+ }
1023
+ const total = totalFromPayload ?? (input ?? 0) + (output ?? 0) + (cacheRead ?? 0) + (cacheCreation ?? 0) + (thought ?? 0);
1024
+ const tokens = { total };
1025
+ if (input != null) tokens.input = input;
1026
+ if (output != null) tokens.output = output;
1027
+ if (cacheRead != null) tokens.cache_read = cacheRead;
1028
+ if (cacheCreation != null) tokens.cache_creation = cacheCreation;
1029
+ if (thought != null) tokens.thought = thought;
1030
+ return tokens;
1031
+ }
1032
+ function isApprovalOption(option) {
1033
+ if (option.optionId === "proceed_once" || option.optionId === "proceed_always" || option.optionId === "cancel") {
1034
+ return true;
1035
+ }
1036
+ const searchable = `${option.name ?? ""} ${option.label ?? ""}`.toLowerCase();
1037
+ return searchable.includes("once") || searchable.includes("always") || searchable.includes("cancel");
1038
+ }
1039
+ function isSelectionPermissionRequest(params) {
1040
+ if (Array.isArray(params.codex_command) && params.codex_command.length > 0) {
1041
+ return false;
1042
+ }
1043
+ if (params.requestedSchema?.properties?.optionId) {
1044
+ return true;
1045
+ }
1046
+ const options = Array.isArray(params.options) ? params.options : [];
1047
+ if (options.length === 0) {
1048
+ return false;
1049
+ }
1050
+ return !options.every((option) => isApprovalOption(option));
1051
+ }
1052
+ function appendDroppedStdoutLineSummary(summary, entry) {
1053
+ summary.count += 1;
1054
+ if (summary.samples.length < MAX_DROPPED_STDOUT_LINE_SAMPLES) {
1055
+ summary.samples.push(entry);
1056
+ }
1057
+ }
1058
+ function nodeToWebStreams(stdin, stdout) {
1059
+ const isIgnorableStdinPipeError = (error) => {
1060
+ const code = typeof error === "object" && error !== null ? error.code : void 0;
1061
+ return code === "ERR_STREAM_DESTROYED" || code === "EPIPE";
1062
+ };
1063
+ stdin.on("error", (err) => {
1064
+ if (isIgnorableStdinPipeError(err)) {
1065
+ logger.debug("[AcpBackend] Ignoring stdin pipe error:", err);
1066
+ return;
1067
+ }
1068
+ logger.debug("[AcpBackend] Stdin error:", err);
1069
+ });
1070
+ const writable = new WritableStream({
1071
+ write(chunk) {
1072
+ return new Promise((resolve, reject) => {
1073
+ if (stdin.destroyed) {
1074
+ resolve();
1075
+ return;
1076
+ }
1077
+ const ok = stdin.write(chunk, (err) => {
1078
+ if (err) {
1079
+ if (isIgnorableStdinPipeError(err)) {
1080
+ resolve();
1081
+ return;
1082
+ }
1083
+ logger.debug(`[AcpBackend] Error writing to stdin:`, err);
1084
+ reject(err);
1085
+ return;
1086
+ }
1087
+ resolve();
1088
+ });
1089
+ if (!ok) {
1090
+ stdin.once("drain", resolve);
1091
+ }
1092
+ });
1093
+ },
1094
+ close() {
1095
+ return new Promise((resolve) => {
1096
+ if (stdin.destroyed) {
1097
+ resolve();
1098
+ return;
1099
+ }
1100
+ let settled = false;
1101
+ const settle = () => {
1102
+ if (settled) {
1103
+ return;
1104
+ }
1105
+ settled = true;
1106
+ stdin.off("close", settle);
1107
+ stdin.off("finish", settle);
1108
+ stdin.off("error", handleError);
1109
+ resolve();
1110
+ };
1111
+ const handleError = (err) => {
1112
+ if (!isIgnorableStdinPipeError(err)) {
1113
+ logger.debug("[AcpBackend] Error ending stdin:", err);
1114
+ }
1115
+ settle();
1116
+ };
1117
+ stdin.once("close", settle);
1118
+ stdin.once("finish", settle);
1119
+ stdin.once("error", handleError);
1120
+ try {
1121
+ stdin.end(() => settle());
1122
+ } catch (error) {
1123
+ if (!isIgnorableStdinPipeError(error)) {
1124
+ logger.debug("[AcpBackend] Error closing stdin:", error);
1125
+ }
1126
+ settle();
1127
+ }
1128
+ });
1129
+ },
1130
+ abort(reason) {
1131
+ stdin.destroy(reason instanceof Error ? reason : new Error(String(reason)));
1132
+ }
1133
+ });
1134
+ const readable = new ReadableStream({
1135
+ start(controller) {
1136
+ stdout.on("data", (chunk) => {
1137
+ controller.enqueue(new Uint8Array(chunk));
1138
+ });
1139
+ stdout.on("end", () => {
1140
+ controller.close();
1141
+ });
1142
+ stdout.on("error", (err) => {
1143
+ logger.debug(`[AcpBackend] Stdout error:`, err);
1144
+ controller.error(err);
1145
+ });
1146
+ },
1147
+ cancel() {
1148
+ stdout.destroy();
1149
+ }
1150
+ });
1151
+ return { writable, readable };
1152
+ }
1153
+ async function withRetry(operation, options) {
1154
+ let lastError = null;
1155
+ for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
1156
+ try {
1157
+ return await operation();
1158
+ } catch (error) {
1159
+ lastError = normalizeAcpError(error);
1160
+ const retryable = options.shouldRetry?.(lastError) ?? true;
1161
+ if (!retryable || attempt >= options.maxAttempts) {
1162
+ throw lastError;
1163
+ }
1164
+ const delayMs = Math.min(
1165
+ options.baseDelayMs * Math.pow(2, attempt - 1),
1166
+ options.maxDelayMs
1167
+ );
1168
+ logger.debug(`[AcpBackend] ${options.operationName} failed (attempt ${attempt}/${options.maxAttempts}): ${lastError.message}. Retrying in ${delayMs}ms...`);
1169
+ options.onRetry?.(attempt, lastError);
1170
+ await delay(delayMs);
1171
+ }
1172
+ }
1173
+ throw lastError;
1174
+ }
1175
+ function formatAcpErrorMessage(error) {
1176
+ if (error instanceof Error && error.message && error.message !== "[object Object]") {
1177
+ return error.message;
1178
+ }
1179
+ if (typeof error === "object" && error !== null) {
1180
+ const record = error;
1181
+ const message = formatDisplayMessage(error).trim();
1182
+ const code = record.code;
1183
+ const status = record.status;
1184
+ const prefix = [
1185
+ code !== void 0 && code !== null ? `[code=${String(code)}]` : "",
1186
+ status !== void 0 && status !== null ? `[status=${String(status)}]` : ""
1187
+ ].filter(Boolean).join(" ");
1188
+ if (message.length > 0) {
1189
+ return prefix ? `${prefix} ${message}` : message;
1190
+ }
1191
+ }
1192
+ const fallback = formatDisplayMessage(error).trim();
1193
+ return fallback || "Unknown ACP error";
1194
+ }
1195
+ function normalizeAcpError(error) {
1196
+ if (error instanceof Error && error.message && error.message !== "[object Object]") {
1197
+ return error;
1198
+ }
1199
+ const normalized = new Error(formatAcpErrorMessage(error));
1200
+ normalized.name = error instanceof Error ? error.name : "Error";
1201
+ if (error && typeof error === "object") {
1202
+ const { message: _message, ...rest } = error;
1203
+ Object.assign(normalized, rest);
1204
+ }
1205
+ return normalized;
1206
+ }
1207
+ function getStatusErrorDetail(message) {
1208
+ if (message.type !== "status" || message.status !== "error") {
1209
+ return null;
1210
+ }
1211
+ const detail = typeof message.detail === "string" ? message.detail.trim() : "";
1212
+ return detail || "Unknown ACP transport error";
1213
+ }
1214
+ function looksLikeDroppedStdoutError(text) {
1215
+ const lower = text.toLowerCase();
1216
+ return lower.startsWith("error") || lower.includes("error:") || lower.includes("exception") || lower.includes("traceback") || lower.includes("invalid request") || lower.includes("invalid_request") || lower.includes("unauthorized") || lower.includes("forbidden") || lower.includes("permission denied") || /\b(4\d\d|5\d\d)\b/.test(lower) && (lower.includes("http") || lower.includes("status") || lower.includes("request"));
1217
+ }
1218
+ function createAcpAbortError(message) {
1219
+ const error = new Error(message);
1220
+ error.name = "AbortError";
1221
+ return error;
1222
+ }
1223
+ function enrichAcpError(error, stderrExcerpt) {
1224
+ const normalized = normalizeAcpError(error);
1225
+ if (!stderrExcerpt.trim()) {
1226
+ return normalized;
1227
+ }
1228
+ const existingStderr = normalized.stderr;
1229
+ if (typeof existingStderr !== "string" || existingStderr.trim().length === 0) {
1230
+ Object.assign(normalized, { stderr: stderrExcerpt });
1231
+ }
1232
+ return normalized;
1233
+ }
1234
+ class AcpProcessStartupError extends Error {
1235
+ constructor(message, exitCode, signal, stderr) {
1236
+ super(message);
1237
+ this.exitCode = exitCode;
1238
+ this.signal = signal;
1239
+ this.stderr = stderr;
1240
+ this.name = "AcpProcessStartupError";
1241
+ }
1242
+ }
1243
+ function createProcessStartupError(agentName, operationName, code, signal, stderrExcerpt) {
1244
+ const normalizedStderrExcerpt = stderrExcerpt?.trim() || void 0;
1245
+ if (code !== null) {
1246
+ return new AcpProcessStartupError(
1247
+ `${agentName} exited with code ${code} during ${operationName}`,
1248
+ code,
1249
+ signal,
1250
+ normalizedStderrExcerpt
1251
+ );
1252
+ }
1253
+ if (signal) {
1254
+ return new AcpProcessStartupError(
1255
+ `${agentName} exited due to signal ${signal} during ${operationName}`,
1256
+ code,
1257
+ signal,
1258
+ normalizedStderrExcerpt
1259
+ );
1260
+ }
1261
+ return new AcpProcessStartupError(
1262
+ `${agentName} exited unexpectedly during ${operationName}`,
1263
+ code,
1264
+ signal,
1265
+ normalizedStderrExcerpt
1266
+ );
1267
+ }
1268
+ function isAcpConnectionClosedError(error) {
1269
+ return error.message.trim() === "ACP connection closed";
1270
+ }
1271
+ function createCurrentProcessExitError(childProcess, options) {
1272
+ if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
1273
+ return createProcessStartupError(
1274
+ options.agentName,
1275
+ options.operationName,
1276
+ childProcess.exitCode,
1277
+ childProcess.signalCode ?? null,
1278
+ options.getStderrExcerpt?.()
1279
+ );
1280
+ }
1281
+ return null;
1282
+ }
1283
+ async function raceWithProcessExit(childProcess, operationFactory, options) {
1284
+ if (childProcess.exitCode !== null || childProcess.signalCode !== null) {
1285
+ throw createProcessStartupError(
1286
+ options.agentName,
1287
+ options.operationName,
1288
+ childProcess.exitCode,
1289
+ childProcess.signalCode ?? null,
1290
+ options.getStderrExcerpt?.()
1291
+ );
1292
+ }
1293
+ return await new Promise((resolve, reject) => {
1294
+ let settled = false;
1295
+ let closedConnectionFallbackTimeout = null;
1296
+ const cleanup = () => {
1297
+ if (closedConnectionFallbackTimeout) {
1298
+ clearTimeout(closedConnectionFallbackTimeout);
1299
+ closedConnectionFallbackTimeout = null;
1300
+ }
1301
+ childProcess.off("error", handleError);
1302
+ childProcess.off("exit", handleExit);
1303
+ };
1304
+ const settleResolve = (value) => {
1305
+ if (settled) {
1306
+ return;
1307
+ }
1308
+ settled = true;
1309
+ cleanup();
1310
+ resolve(value);
1311
+ };
1312
+ const rejectWithNormalizedError = (error) => {
1313
+ if (settled) {
1314
+ return;
1315
+ }
1316
+ settled = true;
1317
+ cleanup();
1318
+ reject(error);
1319
+ };
1320
+ const settleReject = (error) => {
1321
+ const normalizedError = normalizeAcpError(error);
1322
+ const currentProcessExitError = createCurrentProcessExitError(childProcess, options);
1323
+ if (currentProcessExitError && isAcpConnectionClosedError(normalizedError)) {
1324
+ rejectWithNormalizedError(currentProcessExitError);
1325
+ return;
1326
+ }
1327
+ if (!settled && !currentProcessExitError && !closedConnectionFallbackTimeout && isAcpConnectionClosedError(normalizedError)) {
1328
+ closedConnectionFallbackTimeout = setTimeout(() => {
1329
+ closedConnectionFallbackTimeout = null;
1330
+ rejectWithNormalizedError(normalizedError);
1331
+ }, 50);
1332
+ return;
1333
+ }
1334
+ rejectWithNormalizedError(normalizedError);
1335
+ };
1336
+ const handleError = (error) => {
1337
+ settleReject(error);
1338
+ };
1339
+ const handleExit = (code, signal) => {
1340
+ settleReject(
1341
+ createProcessStartupError(
1342
+ options.agentName,
1343
+ options.operationName,
1344
+ code,
1345
+ signal,
1346
+ options.getStderrExcerpt?.()
1347
+ )
1348
+ );
1349
+ };
1350
+ childProcess.once("error", handleError);
1351
+ childProcess.once("exit", handleExit);
1352
+ let operation;
1353
+ try {
1354
+ operation = operationFactory();
1355
+ } catch (error) {
1356
+ settleReject(error);
1357
+ return;
1358
+ }
1359
+ operation.then(settleResolve, settleReject);
1360
+ });
1361
+ }
1362
+ class AcpBackend {
1363
+ constructor(options) {
1364
+ this.options = options;
1365
+ this.transport = options.transportHandler ?? new DefaultTransport(options.agentName);
1366
+ this.sessionPreferences = resolveAcpSessionPreferences({
1367
+ agentName: options.agentName,
1368
+ env: options.env
1369
+ });
1370
+ this.acpMaxMultilineStdoutBytes = resolveAcpMaxMultilineStdoutBytes();
1371
+ }
1372
+ listeners = [];
1373
+ process = null;
1374
+ connection = null;
1375
+ acpSessionId = null;
1376
+ disposed = false;
1377
+ /** Track active tool calls to prevent duplicate events */
1378
+ activeToolCalls = /* @__PURE__ */ new Set();
1379
+ toolCallTimeouts = /* @__PURE__ */ new Map();
1380
+ toolCallTimeoutSpecs = /* @__PURE__ */ new Map();
1381
+ /** Track tool call start times for performance monitoring */
1382
+ toolCallStartTimes = /* @__PURE__ */ new Map();
1383
+ /** Track streamed tool output between ACP updates and final completion */
1384
+ toolCallOutputs = /* @__PURE__ */ new Map();
1385
+ /** Pending permission requests that need response */
1386
+ pendingPermissions = /* @__PURE__ */ new Map();
1387
+ /** Map from permission request ID to real tool call ID for tracking */
1388
+ permissionToToolCallMap = /* @__PURE__ */ new Map();
1389
+ /** Map from real tool call ID to tool name for auto-approval */
1390
+ toolCallIdToNameMap = /* @__PURE__ */ new Map();
1391
+ /** Track tool calls count since last prompt (to identify first tool call) */
1392
+ toolCallCountSincePrompt = 0;
1393
+ /** Timeout for emitting 'idle' status after last message chunk */
1394
+ idleTimeout = null;
1395
+ /** Promise resolver for waitForResponseComplete */
1396
+ idleResolver = null;
1397
+ /** Promise rejecter for waitForResponseComplete */
1398
+ idleRejecter = null;
1399
+ /** Completion signal captured before waitForResponseComplete is attached */
1400
+ responseCompletionOutcome = null;
1401
+ /** Whether the current prompt is still waiting for completion */
1402
+ waitingForResponse = false;
1403
+ /** First fatal prompt-level error observed for the current turn */
1404
+ responseCompletionError = null;
1405
+ /** Resettable no-progress timeout while waiting for a turn to finish */
1406
+ responseWaitTimeout = null;
1407
+ /** Current inactivity threshold used by waitForResponseComplete */
1408
+ responseWaitTimeoutMs = null;
1409
+ /** Timestamp of the last meaningful response progress for the current turn */
1410
+ responseLastProgressAt = null;
1411
+ /** Fallback completion when prompt returns but the agent emits no session updates */
1412
+ postPromptCompletionIdleTimeout = null;
1413
+ /** Whether at least one session/update arrived after the current prompt */
1414
+ sawSessionUpdateSincePrompt = false;
1415
+ /** Transport handler for agent-specific behavior */
1416
+ transport;
1417
+ sessionPreferences;
1418
+ sessionConfigOptions = null;
1419
+ agentCapabilities = null;
1420
+ /** Keep a short rolling stderr buffer so startup failures can surface the real cause. */
1421
+ recentStderrLines = [];
1422
+ acpMaxMultilineStdoutBytes;
1423
+ oversizedStdoutNoticeEmittedForCurrentTurn = false;
1424
+ resourceCleanupDone = false;
1425
+ getProviderSessionId() {
1426
+ return this.acpSessionId;
1427
+ }
1428
+ recordRecentStderr(text) {
1429
+ const normalized = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1430
+ if (normalized.length === 0) {
1431
+ return;
1432
+ }
1433
+ this.recentStderrLines.push(...normalized);
1434
+ if (this.recentStderrLines.length > 12) {
1435
+ this.recentStderrLines = this.recentStderrLines.slice(-12);
1436
+ }
1437
+ }
1438
+ getRecentStderrExcerpt() {
1439
+ return this.recentStderrLines.slice(-6).join("\n");
1440
+ }
1441
+ getRecentStderrSummaryLine() {
1442
+ for (const line of this.recentStderrLines.slice(-6).reverse()) {
1443
+ if (/^note:\s+run with\b/i.test(line)) {
1444
+ continue;
1445
+ }
1446
+ return line;
1447
+ }
1448
+ return this.recentStderrLines.at(-1);
1449
+ }
1450
+ async cleanupOwnedResources() {
1451
+ if (this.resourceCleanupDone) {
1452
+ return;
1453
+ }
1454
+ this.resourceCleanupDone = true;
1455
+ try {
1456
+ await this.options.resourceCleanup?.();
1457
+ } catch (error) {
1458
+ logger.debug("[AcpBackend] Failed to clean up backend-owned resources:", error);
1459
+ }
1460
+ }
1461
+ updateSessionConfigOptions(configOptions) {
1462
+ this.sessionConfigOptions = Array.isArray(configOptions) ? configOptions : null;
1463
+ }
1464
+ supportsLoadSession() {
1465
+ const capabilities = this.agentCapabilities;
1466
+ return capabilities?.loadSession === true;
1467
+ }
1468
+ supportsResumeSession() {
1469
+ const capabilities = this.agentCapabilities;
1470
+ return capabilities?.sessionCapabilities?.resume != null;
1471
+ }
1472
+ syncSessionConfigOptionsFromUpdate(update) {
1473
+ const configOptions = update.configOptions;
1474
+ if (!Array.isArray(configOptions)) {
1475
+ return false;
1476
+ }
1477
+ this.updateSessionConfigOptions(configOptions);
1478
+ return true;
1479
+ }
1480
+ async applySessionConfigPresets() {
1481
+ if (!this.connection || !this.acpSessionId) {
1482
+ return;
1483
+ }
1484
+ const presetPlan = buildAcpSessionConfigPresetPlan({
1485
+ sessionId: this.acpSessionId,
1486
+ configOptions: this.sessionConfigOptions,
1487
+ preferences: this.sessionPreferences
1488
+ });
1489
+ if (presetPlan.length === 0) {
1490
+ return;
1491
+ }
1492
+ logger.debug(
1493
+ `[AcpBackend] Applying ${presetPlan.length} ACP session config preset(s) for ${this.sessionPreferences.agentName}`
1494
+ );
1495
+ for (const preset of presetPlan) {
1496
+ try {
1497
+ logger.debug(
1498
+ `[AcpBackend] Setting ACP session config ${preset.optionId} from ${String(preset.currentValue)} to ${String(preset.targetValue)}`
1499
+ );
1500
+ const response = await this.connection.setSessionConfigOption(preset.request);
1501
+ this.updateSessionConfigOptions(response.configOptions);
1502
+ } catch (error) {
1503
+ logger.warn(
1504
+ `[AcpBackend] Failed to apply ACP session config preset ${preset.optionId}:`,
1505
+ error
1506
+ );
1507
+ }
1508
+ }
1509
+ }
1510
+ getPostPromptNoUpdatesTimeoutMs() {
1511
+ const explicitTimeout = readPositiveIntegerEnv("HAPPY_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS") ?? readPositiveIntegerEnv("HAPPIER_ACP_POST_PROMPT_NO_UPDATES_TIMEOUT_MS");
1512
+ if (explicitTimeout != null) {
1513
+ return explicitTimeout;
1514
+ }
1515
+ if (this.sessionPreferences.timeoutSource === "env") {
1516
+ return resolveAcpPostPromptNoUpdatesTimeoutMs(
1517
+ DEFAULT_POST_PROMPT_NO_UPDATES_TIMEOUT_MS,
1518
+ this.sessionPreferences.timeoutProfile
1519
+ );
1520
+ }
1521
+ return resolvePostPromptNoUpdatesTimeoutMs(this.transport);
1522
+ }
1523
+ getResponseWaitTimeoutMs(timeoutMs) {
1524
+ if (typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0) {
1525
+ return Math.trunc(timeoutMs);
1526
+ }
1527
+ return readPositiveIntegerEnv("HAPPY_ACP_RESPONSE_WAIT_TIMEOUT_MS") ?? readPositiveIntegerEnv("HAPPIER_ACP_RESPONSE_WAIT_TIMEOUT_MS") ?? resolveAcpResponseWaitTimeoutMs(this.sessionPreferences.timeoutProfile);
1528
+ }
1529
+ createPromptCompletionWatchdog(timeoutMs) {
1530
+ let timeoutHandle = null;
1531
+ let cancelled = false;
1532
+ const promise = new Promise((resolve, reject) => {
1533
+ const schedule = () => {
1534
+ if (cancelled) {
1535
+ return;
1536
+ }
1537
+ if (this.responseCompletionError) {
1538
+ reject(this.responseCompletionError);
1539
+ return;
1540
+ }
1541
+ if (!this.waitingForResponse) {
1542
+ resolve();
1543
+ return;
1544
+ }
1545
+ const lastProgressAt = this.responseLastProgressAt ?? Date.now();
1546
+ const elapsedMs = Math.max(0, Date.now() - lastProgressAt);
1547
+ const remainingMs = Math.max(1, timeoutMs - elapsedMs);
1548
+ timeoutHandle = setTimeout(() => {
1549
+ timeoutHandle = null;
1550
+ if (cancelled) {
1551
+ return;
1552
+ }
1553
+ if (this.responseCompletionError) {
1554
+ reject(this.responseCompletionError);
1555
+ return;
1556
+ }
1557
+ if (!this.waitingForResponse) {
1558
+ resolve();
1559
+ return;
1560
+ }
1561
+ const latestProgressAt = this.responseLastProgressAt ?? Date.now();
1562
+ const latestElapsedMs = Math.max(0, Date.now() - latestProgressAt);
1563
+ if (latestElapsedMs < timeoutMs) {
1564
+ schedule();
1565
+ return;
1566
+ }
1567
+ const timeoutLabel = formatToolCallTimeoutLimit(timeoutMs);
1568
+ const error = new Error(`Timeout waiting for Codex ACP prompt to complete after ${timeoutLabel} without response progress`);
1569
+ this.failPendingResponseWait(error);
1570
+ reject(error);
1571
+ }, remainingMs);
1572
+ };
1573
+ schedule();
1574
+ });
1575
+ return {
1576
+ promise,
1577
+ cancel: () => {
1578
+ cancelled = true;
1579
+ if (timeoutHandle) {
1580
+ clearTimeout(timeoutHandle);
1581
+ timeoutHandle = null;
1582
+ }
1583
+ }
1584
+ };
1585
+ }
1586
+ clearIdleTimeoutState() {
1587
+ if (this.idleTimeout) {
1588
+ clearTimeout(this.idleTimeout);
1589
+ this.idleTimeout = null;
1590
+ }
1591
+ }
1592
+ clearPostPromptCompletionIdleTimeout() {
1593
+ if (this.postPromptCompletionIdleTimeout) {
1594
+ clearTimeout(this.postPromptCompletionIdleTimeout);
1595
+ this.postPromptCompletionIdleTimeout = null;
1596
+ }
1597
+ }
1598
+ clearResponseWaitTimeout() {
1599
+ if (this.responseWaitTimeout) {
1600
+ clearTimeout(this.responseWaitTimeout);
1601
+ this.responseWaitTimeout = null;
1602
+ }
1603
+ }
1604
+ resetResponseWaitTracking() {
1605
+ this.clearResponseWaitTimeout();
1606
+ this.responseWaitTimeoutMs = null;
1607
+ this.responseLastProgressAt = null;
1608
+ }
1609
+ armResponseWaitTimeout(timeoutMs) {
1610
+ this.responseWaitTimeoutMs = timeoutMs;
1611
+ this.clearResponseWaitTimeout();
1612
+ const lastProgressAt = this.responseLastProgressAt ?? Date.now();
1613
+ const elapsedMs = Math.max(0, Date.now() - lastProgressAt);
1614
+ const remainingMs = Math.max(1, timeoutMs - elapsedMs);
1615
+ this.responseWaitTimeout = setTimeout(() => {
1616
+ this.responseWaitTimeout = null;
1617
+ this.responseWaitTimeoutMs = null;
1618
+ this.failPendingResponseWait(new Error("Timeout waiting for response to complete"));
1619
+ }, remainingMs);
1620
+ }
1621
+ scheduleToolCallTimeout(toolCallId) {
1622
+ const spec = this.toolCallTimeoutSpecs.get(toolCallId);
1623
+ if (!spec) {
1624
+ return;
1625
+ }
1626
+ const existing = this.toolCallTimeouts.get(toolCallId);
1627
+ if (existing) {
1628
+ clearTimeout(existing);
1629
+ }
1630
+ const timeout = setTimeout(() => {
1631
+ const duration = formatDuration(this.toolCallStartTimes.get(toolCallId));
1632
+ const timeoutLabel = formatToolCallTimeoutLimit(spec.timeoutMs);
1633
+ const timeoutDetail = `Tool call ${spec.toolName} timed out after ${timeoutLabel}`;
1634
+ logger.debug(`[AcpBackend] \u23F1\uFE0F Tool call TIMEOUT (from ${spec.source}): ${toolCallId} (${spec.toolKind}) after ${timeoutLabel} without progress - Duration: ${duration}, failing current turn`);
1635
+ this.activeToolCalls.delete(toolCallId);
1636
+ this.toolCallStartTimes.delete(toolCallId);
1637
+ this.clearToolCallTimeout(toolCallId);
1638
+ this.clearIdleTimeoutState();
1639
+ const streamedOutput = renderToolOutput(this.toolCallOutputs.get(toolCallId));
1640
+ this.toolCallOutputs.delete(toolCallId);
1641
+ this.emit({
1642
+ type: "tool-result",
1643
+ toolName: spec.toolName,
1644
+ result: streamedOutput ? {
1645
+ stdout: streamedOutput,
1646
+ error: timeoutDetail,
1647
+ status: "failed",
1648
+ timedOut: true
1649
+ } : {
1650
+ error: timeoutDetail,
1651
+ status: "failed",
1652
+ timedOut: true
1653
+ },
1654
+ callId: toolCallId
1655
+ });
1656
+ this.emit({
1657
+ type: "status",
1658
+ status: "error",
1659
+ detail: timeoutDetail
1660
+ });
1661
+ this.failPendingResponseWait(new Error(timeoutDetail));
1662
+ }, spec.timeoutMs);
1663
+ this.toolCallTimeouts.set(toolCallId, timeout);
1664
+ }
1665
+ armToolCallTimeout(spec) {
1666
+ this.toolCallTimeoutSpecs.set(spec.toolCallId, spec);
1667
+ this.scheduleToolCallTimeout(spec.toolCallId);
1668
+ }
1669
+ clearToolCallTimeout(toolCallId) {
1670
+ const timeout = this.toolCallTimeouts.get(toolCallId);
1671
+ const hadTimeout = Boolean(timeout);
1672
+ if (timeout) {
1673
+ clearTimeout(timeout);
1674
+ this.toolCallTimeouts.delete(toolCallId);
1675
+ }
1676
+ this.toolCallTimeoutSpecs.delete(toolCallId);
1677
+ return hadTimeout;
1678
+ }
1679
+ finalizeDanglingToolCallsAfterTaskComplete() {
1680
+ if (this.activeToolCalls.size === 0) {
1681
+ return;
1682
+ }
1683
+ const danglingToolCallIds = Array.from(this.activeToolCalls);
1684
+ logger.debug(
1685
+ `[AcpBackend] task_complete arrived with ${danglingToolCallIds.length} active tool call(s); synthesizing completion from buffered output`,
1686
+ danglingToolCallIds
1687
+ );
1688
+ for (const toolCallId of danglingToolCallIds) {
1689
+ const toolName = this.toolCallIdToNameMap.get(toolCallId) || "unknown";
1690
+ const streamedOutput = renderToolOutput(this.toolCallOutputs.get(toolCallId));
1691
+ this.activeToolCalls.delete(toolCallId);
1692
+ this.toolCallStartTimes.delete(toolCallId);
1693
+ this.toolCallIdToNameMap.delete(toolCallId);
1694
+ this.toolCallOutputs.delete(toolCallId);
1695
+ this.clearToolCallTimeout(toolCallId);
1696
+ this.emit({
1697
+ type: "tool-result",
1698
+ toolName,
1699
+ result: streamedOutput,
1700
+ callId: toolCallId
1701
+ });
1702
+ }
1703
+ }
1704
+ refreshActiveToolCallTimeouts() {
1705
+ for (const toolCallId of this.activeToolCalls) {
1706
+ if (this.toolCallTimeoutSpecs.has(toolCallId)) {
1707
+ this.scheduleToolCallTimeout(toolCallId);
1708
+ }
1709
+ }
1710
+ }
1711
+ markResponseProgress(opts = {}) {
1712
+ this.responseLastProgressAt = Date.now();
1713
+ if (opts.refreshToolTimeouts !== false) {
1714
+ this.refreshActiveToolCallTimeouts();
1715
+ }
1716
+ if (this.waitingForResponse && this.responseWaitTimeoutMs != null) {
1717
+ this.armResponseWaitTimeout(this.responseWaitTimeoutMs);
1718
+ }
1719
+ }
1720
+ clearToolCallTracking() {
1721
+ this.activeToolCalls.clear();
1722
+ for (const timeout of this.toolCallTimeouts.values()) {
1723
+ clearTimeout(timeout);
1724
+ }
1725
+ this.toolCallTimeouts.clear();
1726
+ this.toolCallTimeoutSpecs.clear();
1727
+ this.toolCallStartTimes.clear();
1728
+ this.toolCallIdToNameMap.clear();
1729
+ this.toolCallOutputs.clear();
1730
+ this.toolCallCountSincePrompt = 0;
1731
+ }
1732
+ resetResponseTrackingForNewPrompt() {
1733
+ this.responseCompletionOutcome = null;
1734
+ this.responseCompletionError = null;
1735
+ this.sawSessionUpdateSincePrompt = false;
1736
+ this.oversizedStdoutNoticeEmittedForCurrentTurn = false;
1737
+ this.resetResponseWaitTracking();
1738
+ this.clearIdleTimeoutState();
1739
+ this.clearPostPromptCompletionIdleTimeout();
1740
+ this.clearToolCallTracking();
1741
+ }
1742
+ failPendingResponseWait(error) {
1743
+ if (this.responseCompletionError) {
1744
+ return;
1745
+ }
1746
+ this.responseCompletionError = error;
1747
+ this.responseCompletionOutcome = null;
1748
+ this.waitingForResponse = false;
1749
+ this.resetResponseWaitTracking();
1750
+ this.clearPostPromptCompletionIdleTimeout();
1751
+ if (this.idleRejecter) {
1752
+ this.idleRejecter(error);
1753
+ }
1754
+ this.idleResolver = null;
1755
+ this.idleRejecter = null;
1756
+ }
1757
+ settleResponseWaiter(outcome) {
1758
+ const hasActiveWaiter = Boolean(this.idleResolver || this.idleRejecter);
1759
+ this.resetResponseWaitTracking();
1760
+ if (!this.waitingForResponse && !hasActiveWaiter) {
1761
+ return;
1762
+ }
1763
+ if (!hasActiveWaiter) {
1764
+ this.waitingForResponse = false;
1765
+ this.responseCompletionOutcome = outcome;
1766
+ return;
1767
+ }
1768
+ if (outcome.kind === "resolved") {
1769
+ this.idleResolver?.();
1770
+ return;
1771
+ }
1772
+ this.idleRejecter?.(outcome.error);
1773
+ }
1774
+ onMessage(handler) {
1775
+ this.listeners.push(handler);
1776
+ }
1777
+ offMessage(handler) {
1778
+ const index = this.listeners.indexOf(handler);
1779
+ if (index !== -1) {
1780
+ this.listeners.splice(index, 1);
1781
+ }
1782
+ }
1783
+ emit(msg) {
1784
+ if (this.disposed) return;
1785
+ for (const listener of this.listeners) {
1786
+ try {
1787
+ listener(msg);
1788
+ } catch (error) {
1789
+ logger.warn("[AcpBackend] Error in message handler:", error);
1790
+ }
1791
+ }
1792
+ }
1793
+ handleDroppedStdoutLine(entry) {
1794
+ if (entry.reason === "multiline_overflow") {
1795
+ if (!this.waitingForResponse || this.responseCompletionError || this.oversizedStdoutNoticeEmittedForCurrentTurn) {
1796
+ return;
1797
+ }
1798
+ this.oversizedStdoutNoticeEmittedForCurrentTurn = true;
1799
+ this.emit({
1800
+ type: "status",
1801
+ status: "running",
1802
+ detail: buildMultilineOverflowStatusDetail(this.acpMaxMultilineStdoutBytes)
1803
+ });
1804
+ return;
1805
+ }
1806
+ if (entry.reason !== "transport_filter_null" || !this.waitingForResponse || this.responseCompletionError) {
1807
+ return;
1808
+ }
1809
+ const raw = entry.line.trim();
1810
+ if (!raw) {
1811
+ return;
1812
+ }
1813
+ const context = {
1814
+ activeToolCalls: this.activeToolCalls,
1815
+ hasActiveInvestigation: this.transport.isInvestigationTool ? Array.from(this.activeToolCalls).some((id) => this.transport.isInvestigationTool(id)) : false
1816
+ };
1817
+ const transportResult = this.transport.handleStderr?.(entry.line, context);
1818
+ const transportMessage = transportResult?.message ?? null;
1819
+ if (transportMessage) {
1820
+ this.emit(transportMessage);
1821
+ const errorDetail = getStatusErrorDetail(transportMessage);
1822
+ if (errorDetail) {
1823
+ this.failPendingResponseWait(new Error(errorDetail));
1824
+ }
1825
+ return;
1826
+ }
1827
+ if (!looksLikeDroppedStdoutError(raw)) {
1828
+ return;
1829
+ }
1830
+ this.emit({
1831
+ type: "status",
1832
+ status: "error",
1833
+ detail: raw
1834
+ });
1835
+ this.failPendingResponseWait(new Error(raw));
1836
+ }
1837
+ async startSession(initialPrompt) {
1838
+ if (this.disposed) {
1839
+ throw new Error("Backend has been disposed");
1840
+ }
1841
+ const sessionId = randomUUID();
1842
+ this.emit({ type: "status", status: "starting" });
1843
+ try {
1844
+ logger.debug(`[AcpBackend] Starting session: ${sessionId}`);
1845
+ this.recentStderrLines = [];
1846
+ const spawnSpec = buildAcpSpawnSpec({
1847
+ command: this.options.command,
1848
+ args: this.options.args ?? [],
1849
+ cwd: this.options.cwd,
1850
+ env: { ...process.env, ...this.options.env }
1851
+ });
1852
+ this.process = spawn(spawnSpec.command, spawnSpec.args, spawnSpec.options);
1853
+ if (this.process.stderr) {
1854
+ }
1855
+ if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
1856
+ throw new Error("Failed to create stdio pipes");
1857
+ }
1858
+ this.process.stderr.on("data", (data) => {
1859
+ const text = data.toString();
1860
+ if (!text.trim()) return;
1861
+ this.recordRecentStderr(text);
1862
+ const hasActiveInvestigation = this.transport.isInvestigationTool ? Array.from(this.activeToolCalls).some((id) => this.transport.isInvestigationTool(id)) : false;
1863
+ const context = {
1864
+ activeToolCalls: this.activeToolCalls,
1865
+ hasActiveInvestigation
1866
+ };
1867
+ if (hasActiveInvestigation) {
1868
+ logger.debug(`[AcpBackend] \u{1F50D} Agent stderr (during investigation): ${text.trim()}`);
1869
+ } else {
1870
+ logger.debug(`[AcpBackend] Agent stderr: ${text.trim()}`);
1871
+ }
1872
+ if (this.transport.handleStderr) {
1873
+ const result = this.transport.handleStderr(text, context);
1874
+ if (result.message) {
1875
+ this.emit(result.message);
1876
+ const errorDetail = getStatusErrorDetail(result.message);
1877
+ if (errorDetail && this.waitingForResponse) {
1878
+ this.failPendingResponseWait(new Error(errorDetail));
1879
+ }
1880
+ }
1881
+ }
1882
+ });
1883
+ this.process.on("error", (err) => {
1884
+ logger.debug(`[AcpBackend] Process error:`, err);
1885
+ if (this.waitingForResponse) {
1886
+ this.failPendingResponseWait(err);
1887
+ }
1888
+ this.emit({ type: "status", status: "error", detail: err.message });
1889
+ });
1890
+ this.process.on("exit", (code, signal) => {
1891
+ logger.debug(
1892
+ `[AcpBackend] Process exited with code ${code}, signal ${signal}, disposed=${this.disposed}, waitingForResponse=${this.waitingForResponse}`
1893
+ );
1894
+ const recentStderrExcerpt = this.getRecentStderrExcerpt();
1895
+ const recentStderrSummaryLine = this.getRecentStderrSummaryLine();
1896
+ if (recentStderrExcerpt) {
1897
+ logger.debug(`[AcpBackend] Recent stderr before exit:
1898
+ ${recentStderrExcerpt}`);
1899
+ }
1900
+ if (this.disposed) {
1901
+ return;
1902
+ }
1903
+ if (this.waitingForResponse) {
1904
+ const detail = code !== null ? `ACP process exited with code ${code}${signal ? ` (${signal})` : ""}` : signal ? `ACP process exited with signal ${signal}` : "ACP process exited unexpectedly";
1905
+ this.failPendingResponseWait(new Error(detail));
1906
+ }
1907
+ if (code !== 0 && code !== null) {
1908
+ const detail = recentStderrSummaryLine ? `Exit code: ${code}
1909
+ Recent stderr: ${recentStderrSummaryLine}` : `Exit code: ${code}`;
1910
+ this.emit({ type: "status", status: "stopped", detail });
1911
+ return;
1912
+ }
1913
+ if (signal) {
1914
+ const detail = recentStderrSummaryLine ? `Signal: ${signal}
1915
+ Recent stderr: ${recentStderrSummaryLine}` : `Signal: ${signal}`;
1916
+ this.emit({ type: "status", status: "stopped", detail });
1917
+ }
1918
+ });
1919
+ const streams = nodeToWebStreams(
1920
+ this.process.stdin,
1921
+ this.process.stdout
1922
+ );
1923
+ const writable = streams.writable;
1924
+ const readable = streams.readable;
1925
+ const droppedStdoutSummary = {
1926
+ count: 0,
1927
+ samples: []
1928
+ };
1929
+ const filteredReadable = createAcpFilteredStdoutReadable({
1930
+ readable,
1931
+ transport: this.transport,
1932
+ onDroppedLine: (entry) => {
1933
+ appendDroppedStdoutLineSummary(droppedStdoutSummary, entry);
1934
+ this.handleDroppedStdoutLine(entry);
1935
+ },
1936
+ onDone: () => {
1937
+ if (droppedStdoutSummary.count > 0) {
1938
+ logger.debug(
1939
+ `[AcpBackend] Filtered out ${droppedStdoutSummary.count} stdout lines from ${this.transport.agentName}`,
1940
+ droppedStdoutSummary.samples
1941
+ );
1942
+ }
1943
+ },
1944
+ maxMultilineBytes: this.acpMaxMultilineStdoutBytes
1945
+ });
1946
+ const stream = ndJsonStream(writable, filteredReadable);
1947
+ const client = {
1948
+ sessionUpdate: async (params) => {
1949
+ this.handleSessionUpdate(params);
1950
+ },
1951
+ requestPermission: async (params) => {
1952
+ const extendedParams = params;
1953
+ const toolCall = extendedParams.toolCall;
1954
+ let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
1955
+ const toolCallId = toolCall?.id || randomUUID();
1956
+ const permissionId = toolCallId;
1957
+ let input = {};
1958
+ if (toolCall) {
1959
+ input = toolCall.input || toolCall.arguments || toolCall.content || {};
1960
+ } else {
1961
+ input = extendedParams.input || extendedParams.arguments || extendedParams.content || {};
1962
+ }
1963
+ const context = {
1964
+ toolCallCountSincePrompt: this.toolCallCountSincePrompt
1965
+ };
1966
+ toolName = this.transport.determineToolName?.(toolName, toolCallId, input, context) ?? toolName;
1967
+ if (toolName !== (toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool")) {
1968
+ logger.debug(`[AcpBackend] Detected tool name: ${toolName} from toolCallId: ${toolCallId}`);
1969
+ }
1970
+ this.toolCallCountSincePrompt++;
1971
+ const options = extendedParams.options || [];
1972
+ const isSelectionRequest = isSelectionPermissionRequest(extendedParams);
1973
+ logger.debug(`[AcpBackend] Permission request: tool=${toolName}, toolCallId=${toolCallId}, input=`, JSON.stringify(input));
1974
+ logger.debug(`[AcpBackend] Permission request params structure:`, JSON.stringify({
1975
+ hasToolCall: !!toolCall,
1976
+ toolCallKind: toolCall?.kind,
1977
+ toolCallId: toolCall?.id,
1978
+ paramsKind: extendedParams.kind,
1979
+ paramsKeys: Object.keys(params)
1980
+ }, null, 2));
1981
+ if (isSelectionRequest) {
1982
+ const selectionOptions = options.reduce((acc, option) => {
1983
+ if (!option.optionId) {
1984
+ return acc;
1985
+ }
1986
+ const displayOption = option;
1987
+ acc.push({
1988
+ optionId: option.optionId,
1989
+ label: displayOption.label || option.name || option.optionId,
1990
+ description: displayOption.description || option.name || option.optionId
1991
+ });
1992
+ return acc;
1993
+ }, []);
1994
+ if (selectionOptions.length === 0) {
1995
+ logger.debug("[AcpBackend] Selection request has no valid options, cancelling");
1996
+ return { outcome: { outcome: "selected", optionId: "cancel" } };
1997
+ }
1998
+ if (!this.options.selectionHandler) {
1999
+ logger.debug("[AcpBackend] No selection handler configured, cancelling selection request");
2000
+ return { outcome: { outcome: "selected", optionId: "cancel" } };
2001
+ }
2002
+ try {
2003
+ const selectionMessage = typeof extendedParams.message === "string" && extendedParams.message.trim().length > 0 ? extendedParams.message : formatDisplayMessage(input).trim() || toolName;
2004
+ const requestId = extendedParams.codex_event_id || extendedParams.codex_elicitation || toolCallId;
2005
+ const response = await this.options.selectionHandler.handleSelection({
2006
+ id: requestId,
2007
+ message: selectionMessage,
2008
+ options: selectionOptions,
2009
+ defaultOptionId: extendedParams.requestedSchema?.properties?.optionId?.default
2010
+ });
2011
+ return { outcome: { outcome: "selected", optionId: response.optionId } };
2012
+ } catch (error) {
2013
+ logger.debug("[AcpBackend] Error in selection handler:", error);
2014
+ return { outcome: { outcome: "selected", optionId: "cancel" } };
2015
+ }
2016
+ }
2017
+ this.emit({
2018
+ type: "permission-request",
2019
+ id: permissionId,
2020
+ reason: toolName,
2021
+ payload: {
2022
+ ...params,
2023
+ permissionId,
2024
+ toolCallId,
2025
+ toolName,
2026
+ input,
2027
+ options: options.map((opt) => ({
2028
+ id: opt.optionId,
2029
+ name: opt.name,
2030
+ kind: opt.kind
2031
+ }))
2032
+ }
2033
+ });
2034
+ if (this.options.permissionHandler) {
2035
+ try {
2036
+ const result = await this.options.permissionHandler.handleToolCall(
2037
+ toolCallId,
2038
+ toolName,
2039
+ input
2040
+ );
2041
+ let optionId = "cancel";
2042
+ if (result.decision === "approved" || result.decision === "approved_for_session") {
2043
+ const proceedOnceOption2 = options.find(
2044
+ (opt) => opt.optionId === "proceed_once" || opt.name?.toLowerCase().includes("once")
2045
+ );
2046
+ const proceedAlwaysOption = options.find(
2047
+ (opt) => opt.optionId === "proceed_always" || opt.name?.toLowerCase().includes("always")
2048
+ );
2049
+ if (result.decision === "approved_for_session" && proceedAlwaysOption) {
2050
+ optionId = proceedAlwaysOption.optionId || "proceed_always";
2051
+ } else if (proceedOnceOption2) {
2052
+ optionId = proceedOnceOption2.optionId || "proceed_once";
2053
+ } else if (options.length > 0) {
2054
+ optionId = options[0].optionId || "proceed_once";
2055
+ }
2056
+ this.emit({
2057
+ type: "tool-result",
2058
+ toolName,
2059
+ result: { status: "approved", decision: result.decision },
2060
+ callId: permissionId
2061
+ });
2062
+ } else {
2063
+ const cancelOption = options.find(
2064
+ (opt) => opt.optionId === "cancel" || opt.name?.toLowerCase().includes("cancel")
2065
+ );
2066
+ if (cancelOption) {
2067
+ optionId = cancelOption.optionId || "cancel";
2068
+ }
2069
+ this.emit({
2070
+ type: "tool-result",
2071
+ toolName,
2072
+ result: { status: "denied", decision: result.decision },
2073
+ callId: permissionId
2074
+ });
2075
+ }
2076
+ return { outcome: { outcome: "selected", optionId } };
2077
+ } catch (error) {
2078
+ logger.debug("[AcpBackend] Error in permission handler:", error);
2079
+ return { outcome: { outcome: "selected", optionId: "cancel" } };
2080
+ }
2081
+ }
2082
+ const proceedOnceOption = options.find(
2083
+ (opt) => opt.optionId === "proceed_once" || typeof opt.name === "string" && opt.name.toLowerCase().includes("once")
2084
+ );
2085
+ const defaultOptionId = proceedOnceOption?.optionId || (options.length > 0 && options[0].optionId ? options[0].optionId : "proceed_once");
2086
+ return { outcome: { outcome: "selected", optionId: defaultOptionId } };
2087
+ }
2088
+ };
2089
+ this.connection = new ClientSideConnection(
2090
+ (agent) => client,
2091
+ stream
2092
+ );
2093
+ const initRequest = {
2094
+ protocolVersion: 1,
2095
+ clientCapabilities: {
2096
+ _meta: {
2097
+ terminal_output: true
2098
+ },
2099
+ fs: {
2100
+ readTextFile: false,
2101
+ writeTextFile: false
2102
+ }
2103
+ },
2104
+ clientInfo: {
2105
+ name: "happy-cli",
2106
+ version: packageJson.version
2107
+ }
2108
+ };
2109
+ const initTimeout = this.transport.getInitTimeout();
2110
+ const initDelayMs = this.transport.getInitDelayMs?.() ?? 0;
2111
+ if (initDelayMs > 0) {
2112
+ logger.debug(`[AcpBackend] Waiting ${initDelayMs}ms before initialize (${this.transport.agentName})...`);
2113
+ await delay(initDelayMs);
2114
+ }
2115
+ logger.debug(`[AcpBackend] Initializing connection (timeout: ${initTimeout}ms)...`);
2116
+ const initializeResponse = await withRetry(
2117
+ async () => {
2118
+ let timeoutHandle = null;
2119
+ try {
2120
+ const result = await raceWithProcessExit(
2121
+ this.process,
2122
+ () => Promise.race([
2123
+ this.connection.initialize(initRequest).then((res) => {
2124
+ if (timeoutHandle) {
2125
+ clearTimeout(timeoutHandle);
2126
+ timeoutHandle = null;
2127
+ }
2128
+ return res;
2129
+ }),
2130
+ new Promise((_, reject) => {
2131
+ timeoutHandle = setTimeout(() => {
2132
+ reject(new Error(`Initialize timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
2133
+ }, initTimeout);
2134
+ })
2135
+ ]),
2136
+ {
2137
+ agentName: this.transport.agentName,
2138
+ operationName: "initialize",
2139
+ getStderrExcerpt: () => this.getRecentStderrExcerpt()
2140
+ }
2141
+ );
2142
+ return result;
2143
+ } finally {
2144
+ if (timeoutHandle) {
2145
+ clearTimeout(timeoutHandle);
2146
+ }
2147
+ }
2148
+ },
2149
+ {
2150
+ operationName: "Initialize",
2151
+ maxAttempts: RETRY_CONFIG.maxAttempts,
2152
+ baseDelayMs: RETRY_CONFIG.baseDelayMs,
2153
+ maxDelayMs: RETRY_CONFIG.maxDelayMs,
2154
+ shouldRetry: (error) => !(error instanceof AcpProcessStartupError)
2155
+ }
2156
+ );
2157
+ this.agentCapabilities = initializeResponse.agentCapabilities ?? null;
2158
+ logger.debug(`[AcpBackend] Initialize completed`);
2159
+ const mcpServers = this.options.mcpServers ? Object.entries(this.options.mcpServers).map(([name, config]) => ({
2160
+ name,
2161
+ command: config.command,
2162
+ args: config.args || [],
2163
+ env: config.env ? Object.entries(config.env).map(([envName, envValue]) => ({ name: envName, value: envValue })) : []
2164
+ })) : [];
2165
+ const newSessionRequest = {
2166
+ cwd: this.options.cwd,
2167
+ mcpServers
2168
+ };
2169
+ const requestedResumeSessionId = typeof this.options.resumeSessionId === "string" && this.options.resumeSessionId.trim() ? this.options.resumeSessionId.trim() : null;
2170
+ const sessionOperation = requestedResumeSessionId && this.supportsResumeSession() ? "resume" : requestedResumeSessionId && this.supportsLoadSession() ? "load" : "new";
2171
+ const sessionRequest = sessionOperation === "resume" ? {
2172
+ cwd: this.options.cwd,
2173
+ mcpServers,
2174
+ sessionId: requestedResumeSessionId
2175
+ } : sessionOperation === "load" ? {
2176
+ cwd: this.options.cwd,
2177
+ mcpServers,
2178
+ sessionId: requestedResumeSessionId
2179
+ } : newSessionRequest;
2180
+ if (requestedResumeSessionId && sessionOperation === "new") {
2181
+ throw new Error("ACP agent does not support session resume/load; refusing to fork the Codex conversation");
2182
+ }
2183
+ logger.debug(`[AcpBackend] ${sessionOperation === "new" ? "Creating new" : `${sessionOperation === "resume" ? "Resuming" : "Loading"} existing`} session...`);
2184
+ const sessionResponse = await withRetry(
2185
+ async () => {
2186
+ let timeoutHandle = null;
2187
+ try {
2188
+ const resumeSession = this.connection.resumeSession ?? this.connection.unstable_resumeSession;
2189
+ const result = await raceWithProcessExit(
2190
+ this.process,
2191
+ () => Promise.race([
2192
+ (sessionOperation === "resume" ? resumeSession ? resumeSession(sessionRequest).then((response) => ({
2193
+ ...response,
2194
+ sessionId: requestedResumeSessionId
2195
+ })) : Promise.reject(new Error("ACP agent advertised session resume, but this SDK connection does not expose resumeSession.")) : sessionOperation === "load" ? this.connection.loadSession(sessionRequest) : this.connection.newSession(sessionRequest)).then((res) => {
2196
+ if (timeoutHandle) {
2197
+ clearTimeout(timeoutHandle);
2198
+ timeoutHandle = null;
2199
+ }
2200
+ return res;
2201
+ }),
2202
+ new Promise((_, reject) => {
2203
+ timeoutHandle = setTimeout(() => {
2204
+ reject(new Error(`New session timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
2205
+ }, initTimeout);
2206
+ })
2207
+ ]),
2208
+ {
2209
+ agentName: this.transport.agentName,
2210
+ operationName: "new session",
2211
+ getStderrExcerpt: () => this.getRecentStderrExcerpt()
2212
+ }
2213
+ );
2214
+ return result;
2215
+ } finally {
2216
+ if (timeoutHandle) {
2217
+ clearTimeout(timeoutHandle);
2218
+ }
2219
+ }
2220
+ },
2221
+ {
2222
+ operationName: sessionOperation === "resume" ? "ResumeSession" : sessionOperation === "load" ? "LoadSession" : "NewSession",
2223
+ maxAttempts: RETRY_CONFIG.maxAttempts,
2224
+ baseDelayMs: RETRY_CONFIG.baseDelayMs,
2225
+ maxDelayMs: RETRY_CONFIG.maxDelayMs,
2226
+ shouldRetry: (error) => !(error instanceof AcpProcessStartupError)
2227
+ }
2228
+ );
2229
+ this.acpSessionId = requestedResumeSessionId ?? sessionResponse.sessionId;
2230
+ this.updateSessionConfigOptions(sessionResponse.configOptions);
2231
+ logger.debug(`[AcpBackend] Session created: ${this.acpSessionId}`);
2232
+ await this.applySessionConfigPresets();
2233
+ this.emitIdleStatus();
2234
+ if (initialPrompt) {
2235
+ this.sendPrompt(sessionId, initialPrompt).catch((error) => {
2236
+ logger.debug("[AcpBackend] Error sending initial prompt:", error);
2237
+ this.emit({ type: "status", status: "error", detail: formatAcpErrorMessage(error) });
2238
+ });
2239
+ }
2240
+ return { sessionId };
2241
+ } catch (error) {
2242
+ const enrichedError = enrichAcpError(error, this.getRecentStderrExcerpt());
2243
+ if (this.process) {
2244
+ try {
2245
+ await killProcessTree(this.process, { graceMs: 250 });
2246
+ } catch {
2247
+ } finally {
2248
+ this.process = null;
2249
+ }
2250
+ }
2251
+ this.connection = null;
2252
+ this.acpSessionId = null;
2253
+ await this.cleanupOwnedResources();
2254
+ logger.debug("[AcpBackend] Error starting session:", enrichedError);
2255
+ this.emit({
2256
+ type: "status",
2257
+ status: "error",
2258
+ detail: formatAcpErrorMessage(enrichedError)
2259
+ });
2260
+ throw enrichedError;
2261
+ }
2262
+ }
2263
+ /**
2264
+ * Create handler context for session update processing
2265
+ */
2266
+ createHandlerContext() {
2267
+ return {
2268
+ transport: this.transport,
2269
+ activeToolCalls: this.activeToolCalls,
2270
+ toolCallStartTimes: this.toolCallStartTimes,
2271
+ toolCallTimeouts: this.toolCallTimeouts,
2272
+ toolCallIdToNameMap: this.toolCallIdToNameMap,
2273
+ toolCallOutputs: this.toolCallOutputs,
2274
+ idleTimeout: this.idleTimeout,
2275
+ toolCallCountSincePrompt: this.toolCallCountSincePrompt,
2276
+ emit: (msg) => this.emit(msg),
2277
+ emitIdleStatus: () => this.emitIdleStatus(),
2278
+ failPendingResponseWait: (error) => this.failPendingResponseWait(error),
2279
+ armToolCallTimeout: (spec) => this.armToolCallTimeout(spec),
2280
+ clearToolCallTimeout: (toolCallId) => this.clearToolCallTimeout(toolCallId),
2281
+ clearIdleTimeout: () => {
2282
+ if (this.idleTimeout) {
2283
+ clearTimeout(this.idleTimeout);
2284
+ this.idleTimeout = null;
2285
+ }
2286
+ },
2287
+ setIdleTimeout: (callback, ms) => {
2288
+ this.idleTimeout = setTimeout(() => {
2289
+ callback();
2290
+ this.idleTimeout = null;
2291
+ }, ms);
2292
+ }
2293
+ };
2294
+ }
2295
+ emitUsageTelemetry(payload, source) {
2296
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
2297
+ return false;
2298
+ }
2299
+ const tokens = extractUsageTokens(payload);
2300
+ if (!tokens) {
2301
+ return false;
2302
+ }
2303
+ this.emit({
2304
+ type: "token-count",
2305
+ key: source,
2306
+ tokens,
2307
+ source
2308
+ });
2309
+ return true;
2310
+ }
2311
+ handleSessionUpdate(params) {
2312
+ const updates = getSessionUpdates(params);
2313
+ if (updates.length === 0) {
2314
+ logger.debug("[AcpBackend] Received session update without update field:", params);
2315
+ return;
2316
+ }
2317
+ this.sawSessionUpdateSincePrompt = true;
2318
+ this.clearPostPromptCompletionIdleTimeout();
2319
+ for (const update of updates) {
2320
+ const sessionUpdateType = update.sessionUpdate;
2321
+ if (sessionUpdateType !== "agent_message_chunk" && sessionUpdateType !== "tool_call_update") {
2322
+ logger.debug(`[AcpBackend] Received session update: ${sessionUpdateType}`, JSON.stringify({
2323
+ sessionUpdate: sessionUpdateType,
2324
+ toolCallId: update.toolCallId,
2325
+ status: update.status,
2326
+ kind: update.kind,
2327
+ hasContent: !!update.content,
2328
+ hasLocations: !!update.locations
2329
+ }, null, 2));
2330
+ }
2331
+ const ctx = this.createHandlerContext();
2332
+ const toolCallId = update.toolCallId;
2333
+ const wasToolCallActive = typeof toolCallId === "string" && this.activeToolCalls.has(toolCallId);
2334
+ const supplementalToolProgress = extractSupplementalToolProgress(update);
2335
+ if (sessionUpdateType === "agent_message_chunk") {
2336
+ if (handleAgentMessageChunk(update, ctx).handled) {
2337
+ this.markResponseProgress();
2338
+ }
2339
+ continue;
2340
+ }
2341
+ if (sessionUpdateType === "tool_call_update") {
2342
+ const result = handleToolCallUpdate(update, ctx, {
2343
+ supplementalOutputChunk: supplementalToolProgress && supplementalToolProgress.toolCallId === toolCallId ? supplementalToolProgress.outputChunk : null
2344
+ });
2345
+ if (result.toolCallCountSincePrompt !== void 0) {
2346
+ this.toolCallCountSincePrompt = result.toolCallCountSincePrompt;
2347
+ }
2348
+ const isToolCallActive = typeof toolCallId === "string" && this.activeToolCalls.has(toolCallId);
2349
+ if (shouldRefreshProgressForToolCallUpdate(
2350
+ update,
2351
+ wasToolCallActive,
2352
+ isToolCallActive,
2353
+ result.hadVisibleProgress
2354
+ )) {
2355
+ this.markResponseProgress();
2356
+ }
2357
+ continue;
2358
+ }
2359
+ if (sessionUpdateType === "agent_thought_chunk") {
2360
+ if (handleAgentThoughtChunk(update, ctx).handled) {
2361
+ this.markResponseProgress();
2362
+ }
2363
+ continue;
2364
+ }
2365
+ if (sessionUpdateType === "tool_call") {
2366
+ const result = handleToolCall(update, ctx);
2367
+ const isToolCallActive = typeof toolCallId === "string" && this.activeToolCalls.has(toolCallId);
2368
+ if (result.handled && !wasToolCallActive && isToolCallActive) {
2369
+ this.markResponseProgress();
2370
+ }
2371
+ continue;
2372
+ }
2373
+ if (sessionUpdateType === "usage_update") {
2374
+ this.emitUsageTelemetry(update, "acp-usage-update");
2375
+ continue;
2376
+ }
2377
+ if (sessionUpdateType === "config_option_update") {
2378
+ if (this.syncSessionConfigOptionsFromUpdate(update)) {
2379
+ this.markResponseProgress({ refreshToolTimeouts: false });
2380
+ }
2381
+ continue;
2382
+ }
2383
+ if (sessionUpdateType === "task_complete") {
2384
+ this.emitUsageTelemetry(update.usage, "acp-session-usage");
2385
+ this.finalizeDanglingToolCallsAfterTaskComplete();
2386
+ ctx.clearIdleTimeout();
2387
+ logger.debug("[AcpBackend] task_complete received, emitting idle status");
2388
+ this.emitIdleStatus();
2389
+ continue;
2390
+ }
2391
+ const handledLegacy = handleLegacyMessageChunk(update, ctx).handled;
2392
+ const handledPlan = handlePlanUpdate(update, ctx).handled;
2393
+ const handledThinking = handleThinkingUpdate(update, ctx).handled;
2394
+ const handledUsage = this.emitUsageTelemetry(update.usage, "acp-session-usage");
2395
+ if (handledLegacy || handledPlan || handledThinking) {
2396
+ this.markResponseProgress();
2397
+ }
2398
+ const updateTypeStr = sessionUpdateType;
2399
+ const handledTypes = [
2400
+ "agent_message_chunk",
2401
+ "tool_call_update",
2402
+ "agent_thought_chunk",
2403
+ "tool_call",
2404
+ "usage_update",
2405
+ "config_option_update",
2406
+ "task_complete"
2407
+ ];
2408
+ if (updateTypeStr && !handledTypes.includes(updateTypeStr) && !handledLegacy && !handledPlan && !handledThinking && !handledUsage) {
2409
+ logger.debug(`[AcpBackend] Unhandled session update type: ${updateTypeStr}`, JSON.stringify(update, null, 2));
2410
+ }
2411
+ }
2412
+ }
2413
+ async sendPrompt(sessionId, prompt) {
2414
+ this.toolCallCountSincePrompt = 0;
2415
+ if (this.disposed) {
2416
+ throw new Error("Backend has been disposed");
2417
+ }
2418
+ if (!this.connection || !this.acpSessionId) {
2419
+ throw new Error("Session not started");
2420
+ }
2421
+ this.resetResponseTrackingForNewPrompt();
2422
+ this.emit({ type: "status", status: "running" });
2423
+ this.waitingForResponse = true;
2424
+ this.markResponseProgress({ refreshToolTimeouts: false });
2425
+ try {
2426
+ logger.debug(`[AcpBackend] Sending prompt (length: ${prompt.length}): ${prompt.substring(0, 100)}...`);
2427
+ logger.debug(`[AcpBackend] Full prompt: ${prompt}`);
2428
+ const contentBlock = {
2429
+ type: "text",
2430
+ text: prompt
2431
+ };
2432
+ const promptRequest = {
2433
+ sessionId: this.acpSessionId,
2434
+ prompt: [contentBlock]
2435
+ };
2436
+ logger.debug(`[AcpBackend] Prompt request:`, JSON.stringify(promptRequest, null, 2));
2437
+ const promptWatchdog = this.createPromptCompletionWatchdog(this.getResponseWaitTimeoutMs());
2438
+ try {
2439
+ await raceWithProcessExit(
2440
+ this.process,
2441
+ () => Promise.race([
2442
+ this.connection.prompt(promptRequest),
2443
+ promptWatchdog.promise
2444
+ ]),
2445
+ {
2446
+ agentName: this.transport.agentName,
2447
+ operationName: "prompt",
2448
+ getStderrExcerpt: () => this.getRecentStderrExcerpt()
2449
+ }
2450
+ );
2451
+ } finally {
2452
+ promptWatchdog.cancel();
2453
+ }
2454
+ logger.debug("[AcpBackend] Prompt request sent to ACP connection");
2455
+ if (this.waitingForResponse && this.activeToolCalls.size === 0 && this.sawSessionUpdateSincePrompt === false) {
2456
+ const noUpdatesTimeoutMs = this.getPostPromptNoUpdatesTimeoutMs();
2457
+ this.postPromptCompletionIdleTimeout = setTimeout(() => {
2458
+ this.postPromptCompletionIdleTimeout = null;
2459
+ if (this.responseCompletionError || !this.waitingForResponse) {
2460
+ return;
2461
+ }
2462
+ if (this.sawSessionUpdateSincePrompt || this.activeToolCalls.size > 0) {
2463
+ return;
2464
+ }
2465
+ const exitCode = this.process?.exitCode;
2466
+ if (typeof exitCode === "number" && Number.isFinite(exitCode) && exitCode !== 0) {
2467
+ this.failPendingResponseWait(new Error(`Exit code: ${exitCode}`));
2468
+ return;
2469
+ }
2470
+ const signalCode = this.process?.signalCode;
2471
+ if (typeof signalCode === "string" && signalCode.trim().length > 0) {
2472
+ this.failPendingResponseWait(new Error(`Signal: ${signalCode}`));
2473
+ return;
2474
+ }
2475
+ this.emitIdleStatus();
2476
+ }, Math.max(100, noUpdatesTimeoutMs));
2477
+ }
2478
+ } catch (error) {
2479
+ logger.debug("[AcpBackend] Error sending prompt:", error);
2480
+ let errorDetail;
2481
+ if (error instanceof Error) {
2482
+ errorDetail = error.message;
2483
+ } else if (typeof error === "object" && error !== null) {
2484
+ errorDetail = formatAcpErrorMessage(error);
2485
+ } else {
2486
+ errorDetail = formatAcpErrorMessage(error);
2487
+ }
2488
+ this.emit({
2489
+ type: "status",
2490
+ status: "error",
2491
+ detail: errorDetail
2492
+ });
2493
+ this.failPendingResponseWait(error instanceof Error ? error : normalizeAcpError(error));
2494
+ throw error;
2495
+ }
2496
+ }
2497
+ /**
2498
+ * Wait for the response to complete (idle status after all chunks received)
2499
+ * Call this after sendPrompt to wait for Gemini to finish responding
2500
+ */
2501
+ async waitForResponseComplete(timeoutMs) {
2502
+ if (this.responseCompletionError) {
2503
+ throw this.responseCompletionError;
2504
+ }
2505
+ const pendingOutcome = this.responseCompletionOutcome;
2506
+ if (pendingOutcome) {
2507
+ this.responseCompletionOutcome = null;
2508
+ if (pendingOutcome.kind === "rejected") {
2509
+ throw pendingOutcome.error;
2510
+ }
2511
+ return;
2512
+ }
2513
+ if (!this.waitingForResponse) {
2514
+ return;
2515
+ }
2516
+ const effectiveTimeoutMs = this.getResponseWaitTimeoutMs(timeoutMs);
2517
+ return new Promise((resolve, reject) => {
2518
+ this.idleResolver = () => {
2519
+ this.idleResolver = null;
2520
+ this.idleRejecter = null;
2521
+ this.waitingForResponse = false;
2522
+ resolve();
2523
+ };
2524
+ this.idleRejecter = (error) => {
2525
+ this.idleResolver = null;
2526
+ this.idleRejecter = null;
2527
+ this.waitingForResponse = false;
2528
+ reject(error);
2529
+ };
2530
+ this.armResponseWaitTimeout(effectiveTimeoutMs);
2531
+ });
2532
+ }
2533
+ /**
2534
+ * Helper to emit idle status and resolve any waiting promises
2535
+ */
2536
+ emitIdleStatus() {
2537
+ this.clearPostPromptCompletionIdleTimeout();
2538
+ this.emit({ type: "status", status: "idle" });
2539
+ this.settleResponseWaiter({ kind: "resolved" });
2540
+ }
2541
+ async cancel(sessionId) {
2542
+ const cancelError = createAcpAbortError("Cancelled by user");
2543
+ this.clearIdleTimeoutState();
2544
+ this.clearPostPromptCompletionIdleTimeout();
2545
+ this.clearToolCallTracking();
2546
+ this.settleResponseWaiter({ kind: "rejected", error: cancelError });
2547
+ this.emit({ type: "status", status: "stopped", detail: "Cancelled by user" });
2548
+ if (!this.connection || !this.acpSessionId) {
2549
+ return;
2550
+ }
2551
+ try {
2552
+ await this.connection.cancel({ sessionId: this.acpSessionId });
2553
+ } catch (error) {
2554
+ logger.debug("[AcpBackend] Error cancelling:", error);
2555
+ }
2556
+ }
2557
+ /**
2558
+ * Emit permission response event for UI/logging purposes.
2559
+ *
2560
+ * **IMPORTANT:** For ACP backends, this method does NOT send the actual permission
2561
+ * response to the agent. The ACP protocol requires synchronous permission handling,
2562
+ * which is done inside the `requestPermission` RPC handler via `this.options.permissionHandler`.
2563
+ *
2564
+ * This method only emits a `permission-response` event for:
2565
+ * - UI updates (e.g., closing permission dialogs)
2566
+ * - Logging and debugging
2567
+ * - Other parts of the CLI that need to react to permission decisions
2568
+ *
2569
+ * @param requestId - The ID of the permission request
2570
+ * @param approved - Whether the permission was granted
2571
+ */
2572
+ async respondToPermission(requestId, approved) {
2573
+ logger.debug(`[AcpBackend] Permission response event (UI only): ${requestId} = ${approved}`);
2574
+ this.emit({ type: "permission-response", id: requestId, approved });
2575
+ }
2576
+ async dispose() {
2577
+ if (this.disposed) return;
2578
+ logger.debug("[AcpBackend] Disposing backend");
2579
+ this.disposed = true;
2580
+ this.settleResponseWaiter({
2581
+ kind: "rejected",
2582
+ error: createAcpAbortError("ACP backend disposed")
2583
+ });
2584
+ const processStillRunning = this.process ? this.process.exitCode === null && this.process.signalCode === null : false;
2585
+ if (this.connection && this.acpSessionId && processStillRunning) {
2586
+ try {
2587
+ await Promise.race([
2588
+ this.connection.cancel({ sessionId: this.acpSessionId }),
2589
+ new Promise((resolve) => setTimeout(resolve, 2e3))
2590
+ // 2s timeout for graceful shutdown
2591
+ ]);
2592
+ } catch (error) {
2593
+ logger.debug("[AcpBackend] Error during graceful shutdown:", error);
2594
+ }
2595
+ } else if (this.connection && this.acpSessionId && this.process && !processStillRunning) {
2596
+ logger.debug("[AcpBackend] Skipping graceful shutdown because ACP process already exited");
2597
+ }
2598
+ if (this.process) {
2599
+ try {
2600
+ await killProcessTree(this.process, { graceMs: 1e3 });
2601
+ } catch (error) {
2602
+ logger.debug("[AcpBackend] Failed to kill ACP process tree:", error);
2603
+ } finally {
2604
+ this.process = null;
2605
+ }
2606
+ }
2607
+ this.clearIdleTimeoutState();
2608
+ this.clearPostPromptCompletionIdleTimeout();
2609
+ this.listeners = [];
2610
+ this.connection = null;
2611
+ this.acpSessionId = null;
2612
+ this.sessionConfigOptions = null;
2613
+ this.clearToolCallTracking();
2614
+ this.pendingPermissions.clear();
2615
+ await this.cleanupOwnedResources();
2616
+ }
2617
+ }
2618
+
2619
+ export { AcpBackend as A };