acpx 0.1.15 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3738 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-CjeV3_4I.js";
2
+ import { n as isSessionUpdateNotification, t as isAcpJsonRpcMessage } from "./acp-jsonrpc-CGT_1Mel.js";
3
+ import { B as ClaudeAcpSessionCreateTimeoutError, E as normalizeOutputError, G as QueueConnectionError, H as GeminiAcpStartupTimeoutError, I as promptToDisplayText, J as SessionResolutionError, K as SessionModeReplayError, L as textPrompt, M as extractAcpError, N as isAcpResourceNotFoundError, R as AgentSpawnError, S as startPerfTimer, T as isAcpQueryClosedBeforeResponseError, U as PermissionDeniedError, V as CopilotAcpUnsupportedError, W as PermissionPromptUnavailableError, _ as getPerfMetricsSnapshot, a as trySetConfigOptionOnRunningOwner, b as resetPerfMetrics, c as SessionQueueOwner, d as releaseQueueOwnerLease, f as terminateProcess, g as formatPerfMetric, h as waitMs$1, i as tryCancelOnRunningOwner, j as SESSION_RECORD_SCHEMA, l as isProcessAlive, m as tryAcquireQueueOwnerLease, o as trySetModeOnRunningOwner, p as terminateQueueOwnerForSession, q as SessionNotFoundError, s as trySubmitToRunningOwner, t as QUEUE_CONNECT_RETRY_MS, u as refreshQueueOwnerLease, v as incrementPerfCounter, w as formatErrorMessage, x as setPerfGauge, y as measurePerf, z as AuthPolicyError } from "./queue-ipc-C8StWiZt.js";
4
+ import { n as normalizeRuntimeSessionId, t as extractRuntimeSessionId } from "./runtime-session-id-B03l5p1Q.js";
5
+ import fs, { realpathSync, statSync } from "node:fs";
6
+ import fs$1 from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { spawn } from "node:child_process";
9
+ import { Readable, Writable } from "node:stream";
10
+ import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk";
11
+ import readline from "node:readline/promises";
12
+ import { randomUUID } from "node:crypto";
13
+ import os from "node:os";
14
+
15
+ //#region src/permission-prompt.ts
16
+ async function promptForPermission(options) {
17
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return false;
18
+ if (options.header) process.stderr.write(`\n${options.header}\n`);
19
+ if (options.details && options.details.trim().length > 0) process.stderr.write(`${options.details}\n`);
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stderr
23
+ });
24
+ try {
25
+ const normalized = (await rl.question(options.prompt)).trim().toLowerCase();
26
+ return normalized === "y" || normalized === "yes";
27
+ } finally {
28
+ rl.close();
29
+ }
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/filesystem.ts
34
+ const WRITE_PREVIEW_MAX_LINES = 16;
35
+ const WRITE_PREVIEW_MAX_CHARS = 1200;
36
+ function nowIso$1() {
37
+ return (/* @__PURE__ */ new Date()).toISOString();
38
+ }
39
+ function isWithinRoot(rootDir, targetPath) {
40
+ const relative = path.relative(rootDir, targetPath);
41
+ return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
42
+ }
43
+ function toWritePreview(content) {
44
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
45
+ const visibleLines = lines.slice(0, WRITE_PREVIEW_MAX_LINES);
46
+ let preview = visibleLines.join("\n");
47
+ if (lines.length > visibleLines.length) preview += `\n... (${lines.length - visibleLines.length} more lines)`;
48
+ if (preview.length > WRITE_PREVIEW_MAX_CHARS) preview = `${preview.slice(0, WRITE_PREVIEW_MAX_CHARS - 3)}...`;
49
+ return preview;
50
+ }
51
+ async function defaultConfirmWrite(filePath, preview) {
52
+ return await promptForPermission({
53
+ header: `[permission] Allow write to ${filePath}?`,
54
+ details: preview,
55
+ prompt: "Allow write? (y/N) "
56
+ });
57
+ }
58
+ function canPromptForPermission$2() {
59
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
60
+ }
61
+ var FileSystemHandlers = class {
62
+ rootDir;
63
+ permissionMode;
64
+ nonInteractivePermissions;
65
+ onOperation;
66
+ usesDefaultConfirmWrite;
67
+ confirmWrite;
68
+ constructor(options) {
69
+ this.rootDir = path.resolve(options.cwd);
70
+ this.permissionMode = options.permissionMode;
71
+ this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
72
+ this.onOperation = options.onOperation;
73
+ this.usesDefaultConfirmWrite = options.confirmWrite == null;
74
+ this.confirmWrite = options.confirmWrite ?? defaultConfirmWrite;
75
+ }
76
+ updatePermissionPolicy(permissionMode, nonInteractivePermissions) {
77
+ this.permissionMode = permissionMode;
78
+ this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
79
+ }
80
+ async readTextFile(params) {
81
+ const filePath = this.resolvePathWithinRoot(params.path);
82
+ const summary = `read_text_file: ${filePath}`;
83
+ this.emitOperation({
84
+ method: "fs/read_text_file",
85
+ status: "running",
86
+ summary,
87
+ details: this.readWindowDetails(params.line, params.limit),
88
+ timestamp: nowIso$1()
89
+ });
90
+ try {
91
+ if (this.permissionMode === "deny-all") throw new PermissionDeniedError("Permission denied for fs/read_text_file (--deny-all)");
92
+ const content = await fs$1.readFile(filePath, "utf8");
93
+ const sliced = this.sliceContent(content, params.line, params.limit);
94
+ this.emitOperation({
95
+ method: "fs/read_text_file",
96
+ status: "completed",
97
+ summary,
98
+ details: this.readWindowDetails(params.line, params.limit),
99
+ timestamp: nowIso$1()
100
+ });
101
+ return { content: sliced };
102
+ } catch (error) {
103
+ const message = error instanceof Error ? error.message : String(error);
104
+ this.emitOperation({
105
+ method: "fs/read_text_file",
106
+ status: "failed",
107
+ summary,
108
+ details: message,
109
+ timestamp: nowIso$1()
110
+ });
111
+ throw error;
112
+ }
113
+ }
114
+ async writeTextFile(params) {
115
+ const filePath = this.resolvePathWithinRoot(params.path);
116
+ const preview = toWritePreview(params.content);
117
+ const summary = `write_text_file: ${filePath}`;
118
+ this.emitOperation({
119
+ method: "fs/write_text_file",
120
+ status: "running",
121
+ summary,
122
+ details: preview,
123
+ timestamp: nowIso$1()
124
+ });
125
+ try {
126
+ if (!await this.isWriteApproved(filePath, preview)) throw new PermissionDeniedError("Permission denied for fs/write_text_file");
127
+ await fs$1.mkdir(path.dirname(filePath), { recursive: true });
128
+ await fs$1.writeFile(filePath, params.content, "utf8");
129
+ this.emitOperation({
130
+ method: "fs/write_text_file",
131
+ status: "completed",
132
+ summary,
133
+ details: preview,
134
+ timestamp: nowIso$1()
135
+ });
136
+ return {};
137
+ } catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ this.emitOperation({
140
+ method: "fs/write_text_file",
141
+ status: "failed",
142
+ summary,
143
+ details: message,
144
+ timestamp: nowIso$1()
145
+ });
146
+ throw error;
147
+ }
148
+ }
149
+ async isWriteApproved(filePath, preview) {
150
+ if (this.permissionMode === "approve-all") return true;
151
+ if (this.permissionMode === "deny-all") return false;
152
+ if (this.usesDefaultConfirmWrite && this.nonInteractivePermissions === "fail" && !canPromptForPermission$2()) throw new PermissionPromptUnavailableError();
153
+ return await this.confirmWrite(filePath, preview);
154
+ }
155
+ resolvePathWithinRoot(rawPath) {
156
+ if (!path.isAbsolute(rawPath)) throw new Error(`Path must be absolute: ${rawPath}`);
157
+ const resolved = path.resolve(rawPath);
158
+ if (!isWithinRoot(this.rootDir, resolved)) throw new Error(`Path is outside allowed cwd subtree: ${resolved}`);
159
+ return resolved;
160
+ }
161
+ sliceContent(content, line, limit) {
162
+ if (line == null && limit == null) return content;
163
+ const lines = content.split("\n");
164
+ const startLine = line == null ? 1 : Math.max(1, Math.trunc(line));
165
+ const startIndex = Math.max(0, startLine - 1);
166
+ const maxLines = limit == null ? void 0 : Math.max(0, Math.trunc(limit));
167
+ if (maxLines === 0) return "";
168
+ const endIndex = maxLines == null ? lines.length : Math.min(lines.length, startIndex + maxLines);
169
+ return lines.slice(startIndex, endIndex).join("\n");
170
+ }
171
+ readWindowDetails(line, limit) {
172
+ if (line == null && limit == null) return;
173
+ return `line=${line == null ? 1 : Math.max(1, Math.trunc(line))}, limit=${limit == null ? "all" : Math.max(0, Math.trunc(limit))}`;
174
+ }
175
+ emitOperation(operation) {
176
+ this.onOperation?.(operation);
177
+ }
178
+ };
179
+
180
+ //#endregion
181
+ //#region src/permissions.ts
182
+ function selected(optionId) {
183
+ return { outcome: {
184
+ outcome: "selected",
185
+ optionId
186
+ } };
187
+ }
188
+ function cancelled() {
189
+ return { outcome: { outcome: "cancelled" } };
190
+ }
191
+ function pickOption(options, kinds) {
192
+ for (const kind of kinds) {
193
+ const match = options.find((option) => option.kind === kind);
194
+ if (match) return match;
195
+ }
196
+ }
197
+ function inferToolKind(params) {
198
+ if (params.toolCall.kind) return params.toolCall.kind;
199
+ const title = params.toolCall.title?.trim().toLowerCase();
200
+ if (!title) return;
201
+ const head = title.split(":", 1)[0]?.trim();
202
+ if (!head) return;
203
+ if (head.includes("read") || head.includes("cat")) return "read";
204
+ if (head.includes("search") || head.includes("find") || head.includes("grep")) return "search";
205
+ if (head.includes("write") || head.includes("edit") || head.includes("patch")) return "edit";
206
+ if (head.includes("delete") || head.includes("remove")) return "delete";
207
+ if (head.includes("move") || head.includes("rename")) return "move";
208
+ if (head.includes("run") || head.includes("execute") || head.includes("bash")) return "execute";
209
+ if (head.includes("fetch") || head.includes("http") || head.includes("url")) return "fetch";
210
+ if (head.includes("think")) return "think";
211
+ return "other";
212
+ }
213
+ function isAutoApprovedReadKind(kind) {
214
+ return kind === "read" || kind === "search";
215
+ }
216
+ async function promptForToolPermission(params) {
217
+ return await promptForPermission({ prompt: `\n[permission] Allow ${params.toolCall.title ?? "tool"} [${inferToolKind(params) ?? "other"}]? (y/N) ` });
218
+ }
219
+ function canPromptForPermission$1() {
220
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
221
+ }
222
+ async function resolvePermissionRequest(params, mode, nonInteractivePolicy = "deny") {
223
+ const options = params.options ?? [];
224
+ if (options.length === 0) return cancelled();
225
+ const allowOption = pickOption(options, ["allow_once", "allow_always"]);
226
+ const rejectOption = pickOption(options, ["reject_once", "reject_always"]);
227
+ if (mode === "approve-all") {
228
+ if (allowOption) return selected(allowOption.optionId);
229
+ return selected(options[0].optionId);
230
+ }
231
+ if (mode === "deny-all") {
232
+ if (rejectOption) return selected(rejectOption.optionId);
233
+ return cancelled();
234
+ }
235
+ if (isAutoApprovedReadKind(inferToolKind(params)) && allowOption) return selected(allowOption.optionId);
236
+ if (!canPromptForPermission$1()) {
237
+ if (nonInteractivePolicy === "fail") throw new PermissionPromptUnavailableError();
238
+ if (rejectOption) return selected(rejectOption.optionId);
239
+ return cancelled();
240
+ }
241
+ const approved = await promptForToolPermission(params);
242
+ if (approved && allowOption) return selected(allowOption.optionId);
243
+ if (!approved && rejectOption) return selected(rejectOption.optionId);
244
+ return cancelled();
245
+ }
246
+ function classifyPermissionDecision(params, response) {
247
+ if (response.outcome.outcome !== "selected") return "cancelled";
248
+ const selectedOptionId = response.outcome.optionId;
249
+ const selectedOption = params.options.find((option) => option.optionId === selectedOptionId);
250
+ if (!selectedOption) return "cancelled";
251
+ if (selectedOption.kind === "allow_once" || selectedOption.kind === "allow_always") return "approved";
252
+ return "denied";
253
+ }
254
+
255
+ //#endregion
256
+ //#region src/session-runtime-helpers.ts
257
+ var TimeoutError = class extends Error {
258
+ constructor(timeoutMs) {
259
+ super(`Timed out after ${timeoutMs}ms`);
260
+ this.name = "TimeoutError";
261
+ }
262
+ };
263
+ var InterruptedError = class extends Error {
264
+ constructor() {
265
+ super("Interrupted");
266
+ this.name = "InterruptedError";
267
+ }
268
+ };
269
+ async function withTimeout(promise, timeoutMs) {
270
+ if (timeoutMs == null || timeoutMs <= 0) return await promise;
271
+ let timer;
272
+ const timeoutPromise = new Promise((_resolve, reject) => {
273
+ timer = setTimeout(() => {
274
+ reject(new TimeoutError(timeoutMs));
275
+ }, timeoutMs);
276
+ });
277
+ try {
278
+ return await Promise.race([promise, timeoutPromise]);
279
+ } finally {
280
+ if (timer) clearTimeout(timer);
281
+ }
282
+ }
283
+ async function withInterrupt(run, onInterrupt) {
284
+ return await new Promise((resolve, reject) => {
285
+ let settled = false;
286
+ const finish = (cb) => {
287
+ if (settled) return;
288
+ settled = true;
289
+ process.off("SIGINT", onSigint);
290
+ process.off("SIGTERM", onSigterm);
291
+ cb();
292
+ };
293
+ const onSigint = () => {
294
+ onInterrupt().finally(() => {
295
+ finish(() => reject(new InterruptedError()));
296
+ });
297
+ };
298
+ const onSigterm = () => {
299
+ onInterrupt().finally(() => {
300
+ finish(() => reject(new InterruptedError()));
301
+ });
302
+ };
303
+ process.once("SIGINT", onSigint);
304
+ process.once("SIGTERM", onSigterm);
305
+ run().then((result) => finish(() => resolve(result)), (error) => finish(() => reject(error)));
306
+ });
307
+ }
308
+
309
+ //#endregion
310
+ //#region src/terminal.ts
311
+ const DEFAULT_TERMINAL_OUTPUT_LIMIT_BYTES = 64 * 1024;
312
+ const DEFAULT_KILL_GRACE_MS = 1500;
313
+ function nowIso() {
314
+ return (/* @__PURE__ */ new Date()).toISOString();
315
+ }
316
+ function toCommandLine(command, args) {
317
+ const renderedArgs = (args ?? []).map((arg) => JSON.stringify(arg)).join(" ");
318
+ return renderedArgs.length > 0 ? `${command} ${renderedArgs}` : command;
319
+ }
320
+ function toEnvObject(env) {
321
+ if (!env || env.length === 0) return;
322
+ const merged = { ...process.env };
323
+ for (const entry of env) merged[entry.name] = entry.value;
324
+ return merged;
325
+ }
326
+ function buildTerminalSpawnOptions(cwd, env) {
327
+ return {
328
+ cwd,
329
+ env: toEnvObject(env),
330
+ stdio: [
331
+ "ignore",
332
+ "pipe",
333
+ "pipe"
334
+ ],
335
+ windowsHide: true
336
+ };
337
+ }
338
+ function trimToUtf8Boundary(buffer, limit) {
339
+ if (limit <= 0) return Buffer.alloc(0);
340
+ if (buffer.length <= limit) return buffer;
341
+ let start = buffer.length - limit;
342
+ while (start < buffer.length && (buffer[start] & 192) === 128) start += 1;
343
+ if (start >= buffer.length) start = buffer.length - limit;
344
+ return buffer.subarray(start);
345
+ }
346
+ function waitForSpawn$1(process) {
347
+ return new Promise((resolve, reject) => {
348
+ const onSpawn = () => {
349
+ process.off("error", onError);
350
+ resolve();
351
+ };
352
+ const onError = (error) => {
353
+ process.off("spawn", onSpawn);
354
+ reject(error);
355
+ };
356
+ process.once("spawn", onSpawn);
357
+ process.once("error", onError);
358
+ });
359
+ }
360
+ async function defaultConfirmExecute(commandLine) {
361
+ return await promptForPermission({ prompt: `\n[permission] Allow terminal command "${commandLine}"? (y/N) ` });
362
+ }
363
+ function canPromptForPermission() {
364
+ return Boolean(process.stdin.isTTY && process.stderr.isTTY);
365
+ }
366
+ function waitMs(ms) {
367
+ return new Promise((resolve) => {
368
+ setTimeout(resolve, Math.max(0, ms));
369
+ });
370
+ }
371
+ var TerminalManager = class {
372
+ cwd;
373
+ permissionMode;
374
+ nonInteractivePermissions;
375
+ onOperation;
376
+ usesDefaultConfirmExecute;
377
+ confirmExecute;
378
+ killGraceMs;
379
+ terminals = /* @__PURE__ */ new Map();
380
+ constructor(options) {
381
+ this.cwd = options.cwd;
382
+ this.permissionMode = options.permissionMode;
383
+ this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
384
+ this.onOperation = options.onOperation;
385
+ this.usesDefaultConfirmExecute = options.confirmExecute == null;
386
+ this.confirmExecute = options.confirmExecute ?? defaultConfirmExecute;
387
+ this.killGraceMs = Math.max(0, Math.round(options.killGraceMs ?? DEFAULT_KILL_GRACE_MS));
388
+ }
389
+ updatePermissionPolicy(permissionMode, nonInteractivePermissions) {
390
+ this.permissionMode = permissionMode;
391
+ this.nonInteractivePermissions = nonInteractivePermissions ?? "deny";
392
+ }
393
+ async createTerminal(params) {
394
+ const commandLine = toCommandLine(params.command, params.args);
395
+ const summary = `terminal/create: ${commandLine}`;
396
+ this.emitOperation({
397
+ method: "terminal/create",
398
+ status: "running",
399
+ summary,
400
+ timestamp: nowIso()
401
+ });
402
+ try {
403
+ if (!await this.isExecuteApproved(commandLine)) throw new PermissionDeniedError("Permission denied for terminal/create");
404
+ const outputByteLimit = Math.max(0, Math.round(params.outputByteLimit ?? DEFAULT_TERMINAL_OUTPUT_LIMIT_BYTES));
405
+ const proc = spawn(params.command, params.args ?? [], buildTerminalSpawnOptions(params.cwd ?? this.cwd, params.env));
406
+ await waitForSpawn$1(proc);
407
+ let resolveExit = () => {};
408
+ const exitPromise = new Promise((resolve) => {
409
+ resolveExit = resolve;
410
+ });
411
+ const terminal = {
412
+ process: proc,
413
+ output: Buffer.alloc(0),
414
+ truncated: false,
415
+ outputByteLimit,
416
+ exitCode: void 0,
417
+ signal: void 0,
418
+ exitPromise,
419
+ resolveExit
420
+ };
421
+ const appendOutput = (chunk) => {
422
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
423
+ if (bytes.length === 0) return;
424
+ terminal.output = Buffer.concat([terminal.output, bytes]);
425
+ if (terminal.output.length > terminal.outputByteLimit) {
426
+ terminal.output = trimToUtf8Boundary(terminal.output, terminal.outputByteLimit);
427
+ terminal.truncated = true;
428
+ }
429
+ };
430
+ proc.stdout.on("data", appendOutput);
431
+ proc.stderr.on("data", appendOutput);
432
+ proc.once("exit", (exitCode, signal) => {
433
+ terminal.exitCode = exitCode;
434
+ terminal.signal = signal;
435
+ terminal.resolveExit({
436
+ exitCode: exitCode ?? null,
437
+ signal: signal ?? null
438
+ });
439
+ });
440
+ const terminalId = randomUUID();
441
+ this.terminals.set(terminalId, terminal);
442
+ this.emitOperation({
443
+ method: "terminal/create",
444
+ status: "completed",
445
+ summary,
446
+ details: `terminalId=${terminalId}`,
447
+ timestamp: nowIso()
448
+ });
449
+ return { terminalId };
450
+ } catch (error) {
451
+ const message = error instanceof Error ? error.message : String(error);
452
+ this.emitOperation({
453
+ method: "terminal/create",
454
+ status: "failed",
455
+ summary,
456
+ details: message,
457
+ timestamp: nowIso()
458
+ });
459
+ throw error;
460
+ }
461
+ }
462
+ async terminalOutput(params) {
463
+ const terminal = this.getTerminal(params.terminalId);
464
+ if (!terminal) throw new Error(`Unknown terminal: ${params.terminalId}`);
465
+ const hasExitStatus = terminal.exitCode !== void 0 || terminal.signal !== void 0;
466
+ this.emitOperation({
467
+ method: "terminal/output",
468
+ status: "completed",
469
+ summary: `terminal/output: ${params.terminalId}`,
470
+ timestamp: nowIso()
471
+ });
472
+ return {
473
+ output: terminal.output.toString("utf8"),
474
+ truncated: terminal.truncated,
475
+ exitStatus: hasExitStatus ? {
476
+ exitCode: terminal.exitCode ?? null,
477
+ signal: terminal.signal ?? null
478
+ } : void 0
479
+ };
480
+ }
481
+ async waitForTerminalExit(params) {
482
+ const terminal = this.getTerminal(params.terminalId);
483
+ if (!terminal) throw new Error(`Unknown terminal: ${params.terminalId}`);
484
+ const response = await terminal.exitPromise;
485
+ this.emitOperation({
486
+ method: "terminal/wait_for_exit",
487
+ status: "completed",
488
+ summary: `terminal/wait_for_exit: ${params.terminalId}`,
489
+ details: `exitCode=${response.exitCode ?? "null"}, signal=${response.signal ?? "null"}`,
490
+ timestamp: nowIso()
491
+ });
492
+ return response;
493
+ }
494
+ async killTerminal(params) {
495
+ const terminal = this.getTerminal(params.terminalId);
496
+ if (!terminal) throw new Error(`Unknown terminal: ${params.terminalId}`);
497
+ const summary = `terminal/kill: ${params.terminalId}`;
498
+ this.emitOperation({
499
+ method: "terminal/kill",
500
+ status: "running",
501
+ summary,
502
+ timestamp: nowIso()
503
+ });
504
+ try {
505
+ await this.killProcess(terminal);
506
+ this.emitOperation({
507
+ method: "terminal/kill",
508
+ status: "completed",
509
+ summary,
510
+ timestamp: nowIso()
511
+ });
512
+ return {};
513
+ } catch (error) {
514
+ const message = error instanceof Error ? error.message : String(error);
515
+ this.emitOperation({
516
+ method: "terminal/kill",
517
+ status: "failed",
518
+ summary,
519
+ details: message,
520
+ timestamp: nowIso()
521
+ });
522
+ throw error;
523
+ }
524
+ }
525
+ async releaseTerminal(params) {
526
+ const summary = `terminal/release: ${params.terminalId}`;
527
+ this.emitOperation({
528
+ method: "terminal/release",
529
+ status: "running",
530
+ summary,
531
+ timestamp: nowIso()
532
+ });
533
+ const terminal = this.getTerminal(params.terminalId);
534
+ if (!terminal) {
535
+ this.emitOperation({
536
+ method: "terminal/release",
537
+ status: "completed",
538
+ summary,
539
+ details: "already released",
540
+ timestamp: nowIso()
541
+ });
542
+ return {};
543
+ }
544
+ try {
545
+ await this.killProcess(terminal);
546
+ await terminal.exitPromise.catch(() => {});
547
+ terminal.output = Buffer.alloc(0);
548
+ this.terminals.delete(params.terminalId);
549
+ this.emitOperation({
550
+ method: "terminal/release",
551
+ status: "completed",
552
+ summary,
553
+ timestamp: nowIso()
554
+ });
555
+ return {};
556
+ } catch (error) {
557
+ const message = error instanceof Error ? error.message : String(error);
558
+ this.emitOperation({
559
+ method: "terminal/release",
560
+ status: "failed",
561
+ summary,
562
+ details: message,
563
+ timestamp: nowIso()
564
+ });
565
+ throw error;
566
+ }
567
+ }
568
+ async shutdown() {
569
+ for (const terminalId of Array.from(this.terminals.keys())) await this.releaseTerminal({
570
+ terminalId,
571
+ sessionId: "shutdown"
572
+ });
573
+ }
574
+ getTerminal(terminalId) {
575
+ return this.terminals.get(terminalId);
576
+ }
577
+ emitOperation(operation) {
578
+ this.onOperation?.(operation);
579
+ }
580
+ async isExecuteApproved(commandLine) {
581
+ if (this.permissionMode === "approve-all") return true;
582
+ if (this.permissionMode === "deny-all") return false;
583
+ if (this.usesDefaultConfirmExecute && this.nonInteractivePermissions === "fail" && !canPromptForPermission()) throw new PermissionPromptUnavailableError();
584
+ return await this.confirmExecute(commandLine);
585
+ }
586
+ isRunning(terminal) {
587
+ return terminal.exitCode === void 0 && terminal.signal === void 0;
588
+ }
589
+ async killProcess(terminal) {
590
+ if (!this.isRunning(terminal)) return;
591
+ try {
592
+ terminal.process.kill("SIGTERM");
593
+ } catch {
594
+ return;
595
+ }
596
+ if (await Promise.race([terminal.exitPromise.then(() => true), waitMs(this.killGraceMs).then(() => false)]) || !this.isRunning(terminal)) return;
597
+ try {
598
+ terminal.process.kill("SIGKILL");
599
+ } catch {
600
+ return;
601
+ }
602
+ await Promise.race([terminal.exitPromise.then(() => void 0), waitMs(this.killGraceMs)]);
603
+ }
604
+ };
605
+
606
+ //#endregion
607
+ //#region src/client.ts
608
+ const REPLAY_IDLE_MS = 80;
609
+ const REPLAY_DRAIN_TIMEOUT_MS = 5e3;
610
+ const DRAIN_POLL_INTERVAL_MS = 20;
611
+ const AGENT_CLOSE_AFTER_STDIN_END_MS = 100;
612
+ const AGENT_CLOSE_TERM_GRACE_MS = 1500;
613
+ const AGENT_CLOSE_KILL_GRACE_MS = 1e3;
614
+ const GEMINI_ACP_STARTUP_TIMEOUT_MS = 15e3;
615
+ const CLAUDE_ACP_SESSION_CREATE_TIMEOUT_MS = 6e4;
616
+ const GEMINI_VERSION_TIMEOUT_MS = 2e3;
617
+ const COPILOT_HELP_TIMEOUT_MS = 2e3;
618
+ function shouldSuppressSdkConsoleError(args) {
619
+ if (args.length === 0) return false;
620
+ return typeof args[0] === "string" && args[0] === "Error handling request";
621
+ }
622
+ function installSdkConsoleErrorSuppression() {
623
+ const originalConsoleError = console.error;
624
+ console.error = (...args) => {
625
+ if (shouldSuppressSdkConsoleError(args)) return;
626
+ originalConsoleError(...args);
627
+ };
628
+ return () => {
629
+ console.error = originalConsoleError;
630
+ };
631
+ }
632
+ function isoNow$2() {
633
+ return (/* @__PURE__ */ new Date()).toISOString();
634
+ }
635
+ function waitForSpawn(child) {
636
+ return new Promise((resolve, reject) => {
637
+ const onSpawn = () => {
638
+ child.off("error", onError);
639
+ resolve();
640
+ };
641
+ const onError = (error) => {
642
+ child.off("spawn", onSpawn);
643
+ reject(error);
644
+ };
645
+ child.once("spawn", onSpawn);
646
+ child.once("error", onError);
647
+ });
648
+ }
649
+ function isChildProcessRunning(child) {
650
+ return child.exitCode == null && child.signalCode == null;
651
+ }
652
+ function requireAgentStdio(child) {
653
+ if (!child.stdin || !child.stdout || !child.stderr) throw new Error("ACP agent must be spawned with piped stdin/stdout/stderr");
654
+ return child;
655
+ }
656
+ function waitForChildExit(child, timeoutMs) {
657
+ if (!isChildProcessRunning(child)) return Promise.resolve(true);
658
+ return new Promise((resolve) => {
659
+ let settled = false;
660
+ const timer = setTimeout(() => {
661
+ finish(false);
662
+ }, Math.max(0, timeoutMs));
663
+ const finish = (value) => {
664
+ if (settled) return;
665
+ settled = true;
666
+ child.off("close", onExitLike);
667
+ child.off("exit", onExitLike);
668
+ clearTimeout(timer);
669
+ resolve(value);
670
+ };
671
+ const onExitLike = () => {
672
+ finish(true);
673
+ };
674
+ child.once("close", onExitLike);
675
+ child.once("exit", onExitLike);
676
+ });
677
+ }
678
+ function splitCommandLine(value) {
679
+ const parts = [];
680
+ let current = "";
681
+ let quote = null;
682
+ let escaping = false;
683
+ for (const ch of value) {
684
+ if (escaping) {
685
+ current += ch;
686
+ escaping = false;
687
+ continue;
688
+ }
689
+ if (ch === "\\" && quote !== "'") {
690
+ escaping = true;
691
+ continue;
692
+ }
693
+ if (quote) {
694
+ if (ch === quote) quote = null;
695
+ else current += ch;
696
+ continue;
697
+ }
698
+ if (ch === "'" || ch === "\"") {
699
+ quote = ch;
700
+ continue;
701
+ }
702
+ if (/\s/.test(ch)) {
703
+ if (current.length > 0) {
704
+ parts.push(current);
705
+ current = "";
706
+ }
707
+ continue;
708
+ }
709
+ current += ch;
710
+ }
711
+ if (escaping) current += "\\";
712
+ if (quote) throw new Error("Invalid --agent command: unterminated quote");
713
+ if (current.length > 0) parts.push(current);
714
+ if (parts.length === 0) throw new Error("Invalid --agent command: empty command");
715
+ return {
716
+ command: parts[0],
717
+ args: parts.slice(1)
718
+ };
719
+ }
720
+ function asAbsoluteCwd(cwd) {
721
+ return path.resolve(cwd);
722
+ }
723
+ function basenameToken(value) {
724
+ return path.basename(value).toLowerCase().replace(/\.(cmd|exe|bat)$/u, "");
725
+ }
726
+ function isGeminiAcpCommand(command, args) {
727
+ return basenameToken(command) === "gemini" && args.includes("--experimental-acp");
728
+ }
729
+ function isClaudeAcpCommand(command, args) {
730
+ if (basenameToken(command) === "claude-agent-acp") return true;
731
+ return args.some((arg) => arg.includes("claude-agent-acp"));
732
+ }
733
+ function isCopilotAcpCommand(command, args) {
734
+ return basenameToken(command) === "copilot" && args.includes("--acp");
735
+ }
736
+ function resolveGeminiAcpStartupTimeoutMs() {
737
+ const raw = process.env.ACPX_GEMINI_ACP_STARTUP_TIMEOUT_MS;
738
+ if (typeof raw === "string" && raw.trim().length > 0) {
739
+ const parsed = Number(raw);
740
+ if (Number.isFinite(parsed) && parsed > 0) return Math.round(parsed);
741
+ }
742
+ return GEMINI_ACP_STARTUP_TIMEOUT_MS;
743
+ }
744
+ function resolveClaudeAcpSessionCreateTimeoutMs() {
745
+ const raw = process.env.ACPX_CLAUDE_ACP_SESSION_CREATE_TIMEOUT_MS;
746
+ if (typeof raw === "string" && raw.trim().length > 0) {
747
+ const parsed = Number(raw);
748
+ if (Number.isFinite(parsed) && parsed > 0) return Math.round(parsed);
749
+ }
750
+ return CLAUDE_ACP_SESSION_CREATE_TIMEOUT_MS;
751
+ }
752
+ async function detectGeminiVersion(command) {
753
+ return await new Promise((resolve) => {
754
+ const child = spawn(command, ["--version"], {
755
+ stdio: [
756
+ "ignore",
757
+ "pipe",
758
+ "pipe"
759
+ ],
760
+ windowsHide: true
761
+ });
762
+ let stdout = "";
763
+ let stderr = "";
764
+ let settled = false;
765
+ const finish = (value) => {
766
+ if (settled) return;
767
+ settled = true;
768
+ clearTimeout(timer);
769
+ child.removeAllListeners();
770
+ child.stdout?.removeAllListeners();
771
+ child.stderr?.removeAllListeners();
772
+ resolve(value);
773
+ };
774
+ const timer = setTimeout(() => {
775
+ child.kill("SIGKILL");
776
+ finish(void 0);
777
+ }, GEMINI_VERSION_TIMEOUT_MS);
778
+ child.stdout?.setEncoding("utf8");
779
+ child.stderr?.setEncoding("utf8");
780
+ child.stdout?.on("data", (chunk) => {
781
+ stdout += chunk;
782
+ });
783
+ child.stderr?.on("data", (chunk) => {
784
+ stderr += chunk;
785
+ });
786
+ child.once("error", () => {
787
+ finish(void 0);
788
+ });
789
+ child.once("close", () => {
790
+ finish(`${stdout}\n${stderr}`.split(/\r?\n/).map((line) => line.trim()).find((line) => /^\d+\.\d+\.\d+/.test(line)));
791
+ });
792
+ });
793
+ }
794
+ async function readCommandOutput(command, args, timeoutMs) {
795
+ return await new Promise((resolve) => {
796
+ const child = spawn(command, [...args], {
797
+ stdio: [
798
+ "ignore",
799
+ "pipe",
800
+ "pipe"
801
+ ],
802
+ windowsHide: true
803
+ });
804
+ let stdout = "";
805
+ let stderr = "";
806
+ let settled = false;
807
+ const finish = (value) => {
808
+ if (settled) return;
809
+ settled = true;
810
+ clearTimeout(timer);
811
+ child.removeAllListeners();
812
+ child.stdout?.removeAllListeners();
813
+ child.stderr?.removeAllListeners();
814
+ resolve(value);
815
+ };
816
+ const timer = setTimeout(() => {
817
+ child.kill("SIGKILL");
818
+ finish(void 0);
819
+ }, timeoutMs);
820
+ child.stdout?.setEncoding("utf8");
821
+ child.stderr?.setEncoding("utf8");
822
+ child.stdout?.on("data", (chunk) => {
823
+ stdout += chunk;
824
+ });
825
+ child.stderr?.on("data", (chunk) => {
826
+ stderr += chunk;
827
+ });
828
+ child.once("error", () => {
829
+ finish(void 0);
830
+ });
831
+ child.once("close", () => {
832
+ finish(`${stdout}\n${stderr}`);
833
+ });
834
+ });
835
+ }
836
+ async function buildGeminiAcpStartupTimeoutMessage(command) {
837
+ const parts = ["Gemini CLI ACP startup timed out before initialize completed.", "This usually means the local Gemini CLI is waiting on interactive OAuth or has incompatible ACP subprocess behavior."];
838
+ const version = await detectGeminiVersion(command);
839
+ if (version) parts.push(`Detected Gemini CLI version: ${version}.`);
840
+ if (!process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) parts.push("No GEMINI_API_KEY or GOOGLE_API_KEY was set for non-interactive auth.");
841
+ parts.push("Try upgrading Gemini CLI and using API-key-based auth for non-interactive ACP runs.");
842
+ return parts.join(" ");
843
+ }
844
+ function buildClaudeAcpSessionCreateTimeoutMessage() {
845
+ return [
846
+ "Claude ACP session creation timed out before session/new completed.",
847
+ "This matches the known persistent-session stall seen with some Claude Code and @zed-industries/claude-agent-acp combinations.",
848
+ "In harnessed or non-interactive runs, prefer --approve-all with nonInteractivePermissions=deny, upgrade Claude Code and the Claude ACP adapter, or use acpx claude exec as a one-shot fallback."
849
+ ].join(" ");
850
+ }
851
+ async function buildCopilotAcpUnsupportedMessage(command) {
852
+ const parts = ["GitHub Copilot CLI ACP stdio mode is not available in the installed copilot binary.", "acpx copilot expects a Copilot CLI release that supports --acp --stdio."];
853
+ const helpOutput = await readCommandOutput(command, ["--help"], COPILOT_HELP_TIMEOUT_MS);
854
+ if (typeof helpOutput === "string" && !helpOutput.includes("--acp")) parts.push("Detected copilot --help output without --acp support.");
855
+ parts.push("Upgrade GitHub Copilot CLI to a release with ACP stdio support, or use --agent with another ACP-compatible adapter in the meantime.");
856
+ return parts.join(" ");
857
+ }
858
+ async function ensureCopilotAcpSupport(command) {
859
+ const helpOutput = await readCommandOutput(command, ["--help"], COPILOT_HELP_TIMEOUT_MS);
860
+ if (typeof helpOutput === "string" && !helpOutput.includes("--acp")) throw new CopilotAcpUnsupportedError(await buildCopilotAcpUnsupportedMessage(command), { retryable: false });
861
+ }
862
+ function toEnvToken(value) {
863
+ return value.trim().replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
864
+ }
865
+ function authEnvKeys(methodId) {
866
+ const token = toEnvToken(methodId);
867
+ const keys = new Set([methodId]);
868
+ if (token) {
869
+ keys.add(token);
870
+ keys.add(`ACPX_AUTH_${token}`);
871
+ }
872
+ return [...keys];
873
+ }
874
+ function readEnvCredential(methodId) {
875
+ for (const key of authEnvKeys(methodId)) {
876
+ const value = process.env[key];
877
+ if (typeof value === "string" && value.trim().length > 0) return value;
878
+ }
879
+ }
880
+ function buildClaudeCodeOptionsMeta(options) {
881
+ if (!options) return;
882
+ const claudeCodeOptions = {};
883
+ if (typeof options.model === "string" && options.model.trim().length > 0) claudeCodeOptions.model = options.model;
884
+ if (Array.isArray(options.allowedTools)) claudeCodeOptions.allowedTools = [...options.allowedTools];
885
+ if (typeof options.maxTurns === "number") claudeCodeOptions.maxTurns = options.maxTurns;
886
+ if (Object.keys(claudeCodeOptions).length === 0) return;
887
+ return { claudeCode: { options: claudeCodeOptions } };
888
+ }
889
+ function buildAgentEnvironment(authCredentials) {
890
+ const env = { ...process.env };
891
+ if (!authCredentials) return env;
892
+ for (const [methodId, credential] of Object.entries(authCredentials)) {
893
+ if (typeof credential !== "string" || credential.trim().length === 0) continue;
894
+ if (!methodId.includes("=") && !methodId.includes("\0") && env[methodId] == null) env[methodId] = credential;
895
+ const normalized = toEnvToken(methodId);
896
+ if (normalized) {
897
+ const prefixed = `ACPX_AUTH_${normalized}`;
898
+ if (env[prefixed] == null) env[prefixed] = credential;
899
+ if (env[normalized] == null) env[normalized] = credential;
900
+ }
901
+ }
902
+ return env;
903
+ }
904
+ function buildAgentSpawnOptions(cwd, authCredentials) {
905
+ return {
906
+ cwd,
907
+ env: buildAgentEnvironment(authCredentials),
908
+ stdio: [
909
+ "pipe",
910
+ "pipe",
911
+ "pipe"
912
+ ],
913
+ windowsHide: true
914
+ };
915
+ }
916
+ var AcpClient = class {
917
+ options;
918
+ connection;
919
+ agent;
920
+ initResult;
921
+ loadedSessionId;
922
+ eventHandlers;
923
+ permissionStats = {
924
+ requested: 0,
925
+ approved: 0,
926
+ denied: 0,
927
+ cancelled: 0
928
+ };
929
+ filesystem;
930
+ terminalManager;
931
+ sessionUpdateChain = Promise.resolve();
932
+ observedSessionUpdates = 0;
933
+ processedSessionUpdates = 0;
934
+ suppressSessionUpdates = false;
935
+ suppressReplaySessionUpdateMessages = false;
936
+ activePrompt;
937
+ cancellingSessionIds = /* @__PURE__ */ new Set();
938
+ closing = false;
939
+ agentStartedAt;
940
+ lastAgentExit;
941
+ lastKnownPid;
942
+ promptPermissionFailures = /* @__PURE__ */ new Map();
943
+ constructor(options) {
944
+ this.options = {
945
+ ...options,
946
+ cwd: asAbsoluteCwd(options.cwd),
947
+ authPolicy: options.authPolicy ?? "skip"
948
+ };
949
+ this.eventHandlers = {
950
+ onAcpMessage: this.options.onAcpMessage,
951
+ onAcpOutputMessage: this.options.onAcpOutputMessage,
952
+ onSessionUpdate: this.options.onSessionUpdate,
953
+ onClientOperation: this.options.onClientOperation
954
+ };
955
+ this.filesystem = new FileSystemHandlers({
956
+ cwd: this.options.cwd,
957
+ permissionMode: this.options.permissionMode,
958
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
959
+ onOperation: (operation) => {
960
+ this.eventHandlers.onClientOperation?.(operation);
961
+ }
962
+ });
963
+ this.terminalManager = new TerminalManager({
964
+ cwd: this.options.cwd,
965
+ permissionMode: this.options.permissionMode,
966
+ nonInteractivePermissions: this.options.nonInteractivePermissions,
967
+ onOperation: (operation) => {
968
+ this.eventHandlers.onClientOperation?.(operation);
969
+ }
970
+ });
971
+ }
972
+ get initializeResult() {
973
+ return this.initResult;
974
+ }
975
+ getAgentPid() {
976
+ return this.agent?.pid ?? this.lastKnownPid;
977
+ }
978
+ getPermissionStats() {
979
+ return { ...this.permissionStats };
980
+ }
981
+ getAgentLifecycleSnapshot() {
982
+ const pid = this.agent?.pid ?? this.lastKnownPid;
983
+ const running = Boolean(this.agent) && this.agent?.exitCode == null && this.agent?.signalCode == null && !this.agent?.killed;
984
+ return {
985
+ pid,
986
+ startedAt: this.agentStartedAt,
987
+ running,
988
+ lastExit: this.lastAgentExit ? { ...this.lastAgentExit } : void 0
989
+ };
990
+ }
991
+ supportsLoadSession() {
992
+ return Boolean(this.initResult?.agentCapabilities?.loadSession);
993
+ }
994
+ setEventHandlers(handlers) {
995
+ this.eventHandlers = { ...handlers };
996
+ }
997
+ clearEventHandlers() {
998
+ this.eventHandlers = {};
999
+ }
1000
+ updateRuntimeOptions(options) {
1001
+ if (options.permissionMode) this.options.permissionMode = options.permissionMode;
1002
+ if (options.nonInteractivePermissions !== void 0) this.options.nonInteractivePermissions = options.nonInteractivePermissions;
1003
+ if (options.permissionMode || options.nonInteractivePermissions !== void 0) {
1004
+ this.filesystem.updatePermissionPolicy(this.options.permissionMode, this.options.nonInteractivePermissions);
1005
+ this.terminalManager.updatePermissionPolicy(this.options.permissionMode, this.options.nonInteractivePermissions);
1006
+ }
1007
+ if (options.suppressSdkConsoleErrors !== void 0) this.options.suppressSdkConsoleErrors = options.suppressSdkConsoleErrors;
1008
+ if (options.verbose !== void 0) this.options.verbose = options.verbose;
1009
+ }
1010
+ hasReusableSession(sessionId) {
1011
+ return this.connection != null && this.agent != null && isChildProcessRunning(this.agent) && this.loadedSessionId === sessionId;
1012
+ }
1013
+ hasActivePrompt(sessionId) {
1014
+ if (!this.activePrompt) return false;
1015
+ if (sessionId == null) return true;
1016
+ return this.activePrompt.sessionId === sessionId;
1017
+ }
1018
+ async start() {
1019
+ if (this.connection && this.agent && isChildProcessRunning(this.agent)) return;
1020
+ if (this.connection || this.agent) await this.close();
1021
+ const { command, args } = splitCommandLine(this.options.agentCommand);
1022
+ this.log(`spawning agent: ${command} ${args.join(" ")}`);
1023
+ const geminiAcp = isGeminiAcpCommand(command, args);
1024
+ if (isCopilotAcpCommand(command, args)) await ensureCopilotAcpSupport(command);
1025
+ const spawnedChild = spawn(command, args, buildAgentSpawnOptions(this.options.cwd, this.options.authCredentials));
1026
+ try {
1027
+ await waitForSpawn(spawnedChild);
1028
+ } catch (error) {
1029
+ throw new AgentSpawnError(this.options.agentCommand, error);
1030
+ }
1031
+ const child = requireAgentStdio(spawnedChild);
1032
+ this.closing = false;
1033
+ this.agentStartedAt = isoNow$2();
1034
+ this.lastAgentExit = void 0;
1035
+ this.lastKnownPid = child.pid ?? void 0;
1036
+ this.attachAgentLifecycleObservers(child);
1037
+ child.stderr.on("data", (chunk) => {
1038
+ if (!this.options.verbose) return;
1039
+ process.stderr.write(chunk);
1040
+ });
1041
+ const input = Writable.toWeb(child.stdin);
1042
+ const output = Readable.toWeb(child.stdout);
1043
+ const connection = new ClientSideConnection(() => ({
1044
+ sessionUpdate: async (params) => {
1045
+ await this.handleSessionUpdate(params);
1046
+ },
1047
+ requestPermission: async (params) => {
1048
+ return this.handlePermissionRequest(params);
1049
+ },
1050
+ readTextFile: async (params) => {
1051
+ return this.handleReadTextFile(params);
1052
+ },
1053
+ writeTextFile: async (params) => {
1054
+ return this.handleWriteTextFile(params);
1055
+ },
1056
+ createTerminal: async (params) => {
1057
+ return this.handleCreateTerminal(params);
1058
+ },
1059
+ terminalOutput: async (params) => {
1060
+ return this.handleTerminalOutput(params);
1061
+ },
1062
+ waitForTerminalExit: async (params) => {
1063
+ return this.handleWaitForTerminalExit(params);
1064
+ },
1065
+ killTerminal: async (params) => {
1066
+ return this.handleKillTerminal(params);
1067
+ },
1068
+ releaseTerminal: async (params) => {
1069
+ return this.handleReleaseTerminal(params);
1070
+ }
1071
+ }), this.createTappedStream(ndJsonStream(input, output)));
1072
+ connection.signal.addEventListener("abort", () => {
1073
+ this.recordAgentExit("connection_close", child.exitCode ?? null, child.signalCode ?? null);
1074
+ }, { once: true });
1075
+ try {
1076
+ const initializePromise = connection.initialize({
1077
+ protocolVersion: PROTOCOL_VERSION,
1078
+ clientCapabilities: {
1079
+ fs: {
1080
+ readTextFile: true,
1081
+ writeTextFile: true
1082
+ },
1083
+ terminal: true
1084
+ },
1085
+ clientInfo: {
1086
+ name: "acpx",
1087
+ version: "0.1.0"
1088
+ }
1089
+ });
1090
+ const initResult = geminiAcp ? await withTimeout(initializePromise, resolveGeminiAcpStartupTimeoutMs()) : await initializePromise;
1091
+ await this.authenticateIfRequired(connection, initResult.authMethods ?? []);
1092
+ this.connection = connection;
1093
+ this.agent = child;
1094
+ this.initResult = initResult;
1095
+ this.log(`initialized protocol version ${initResult.protocolVersion}`);
1096
+ } catch (error) {
1097
+ child.kill();
1098
+ if (geminiAcp && error instanceof TimeoutError) throw new GeminiAcpStartupTimeoutError(await buildGeminiAcpStartupTimeoutMessage(command), {
1099
+ cause: error,
1100
+ retryable: true
1101
+ });
1102
+ throw error;
1103
+ }
1104
+ }
1105
+ createTappedStream(base) {
1106
+ const onAcpMessage = () => this.eventHandlers.onAcpMessage;
1107
+ const onAcpOutputMessage = () => this.eventHandlers.onAcpOutputMessage;
1108
+ const shouldSuppressInboundReplaySessionUpdate = (message) => {
1109
+ return this.suppressReplaySessionUpdateMessages && isSessionUpdateNotification(message);
1110
+ };
1111
+ return {
1112
+ readable: new ReadableStream({ async start(controller) {
1113
+ const reader = base.readable.getReader();
1114
+ try {
1115
+ while (true) {
1116
+ const { value, done } = await reader.read();
1117
+ if (done) break;
1118
+ if (!value) continue;
1119
+ if (!shouldSuppressInboundReplaySessionUpdate(value)) {
1120
+ onAcpOutputMessage()?.("inbound", value);
1121
+ onAcpMessage()?.("inbound", value);
1122
+ }
1123
+ controller.enqueue(value);
1124
+ }
1125
+ } finally {
1126
+ reader.releaseLock();
1127
+ controller.close();
1128
+ }
1129
+ } }),
1130
+ writable: new WritableStream({ async write(message) {
1131
+ onAcpOutputMessage()?.("outbound", message);
1132
+ onAcpMessage()?.("outbound", message);
1133
+ const writer = base.writable.getWriter();
1134
+ try {
1135
+ await writer.write(message);
1136
+ } finally {
1137
+ writer.releaseLock();
1138
+ }
1139
+ } })
1140
+ };
1141
+ }
1142
+ async createSession(cwd = this.options.cwd) {
1143
+ const connection = this.getConnection();
1144
+ const { command, args } = splitCommandLine(this.options.agentCommand);
1145
+ const claudeAcp = isClaudeAcpCommand(command, args);
1146
+ let result;
1147
+ try {
1148
+ const createPromise = connection.newSession({
1149
+ cwd: asAbsoluteCwd(cwd),
1150
+ mcpServers: this.options.mcpServers ?? [],
1151
+ _meta: buildClaudeCodeOptionsMeta(this.options.sessionOptions)
1152
+ });
1153
+ result = claudeAcp ? await withTimeout(createPromise, resolveClaudeAcpSessionCreateTimeoutMs()) : await createPromise;
1154
+ } catch (error) {
1155
+ if (claudeAcp && error instanceof TimeoutError) throw new ClaudeAcpSessionCreateTimeoutError(buildClaudeAcpSessionCreateTimeoutMessage(), {
1156
+ cause: error,
1157
+ retryable: true
1158
+ });
1159
+ throw error;
1160
+ }
1161
+ this.loadedSessionId = result.sessionId;
1162
+ return {
1163
+ sessionId: result.sessionId,
1164
+ agentSessionId: extractRuntimeSessionId(result._meta)
1165
+ };
1166
+ }
1167
+ async loadSession(sessionId, cwd = this.options.cwd) {
1168
+ this.getConnection();
1169
+ return await this.loadSessionWithOptions(sessionId, cwd, {});
1170
+ }
1171
+ async loadSessionWithOptions(sessionId, cwd = this.options.cwd, options = {}) {
1172
+ const connection = this.getConnection();
1173
+ const previousSuppression = this.suppressSessionUpdates;
1174
+ const previousReplaySuppression = this.suppressReplaySessionUpdateMessages;
1175
+ this.suppressSessionUpdates = previousSuppression || Boolean(options.suppressReplayUpdates);
1176
+ this.suppressReplaySessionUpdateMessages = previousReplaySuppression || Boolean(options.suppressReplayUpdates);
1177
+ let response;
1178
+ try {
1179
+ response = await connection.loadSession({
1180
+ sessionId,
1181
+ cwd: asAbsoluteCwd(cwd),
1182
+ mcpServers: this.options.mcpServers ?? []
1183
+ });
1184
+ await this.waitForSessionUpdateDrain(options.replayIdleMs ?? REPLAY_IDLE_MS, options.replayDrainTimeoutMs ?? REPLAY_DRAIN_TIMEOUT_MS);
1185
+ } finally {
1186
+ this.suppressSessionUpdates = previousSuppression;
1187
+ this.suppressReplaySessionUpdateMessages = previousReplaySuppression;
1188
+ }
1189
+ this.loadedSessionId = sessionId;
1190
+ return { agentSessionId: extractRuntimeSessionId(response?._meta) };
1191
+ }
1192
+ async prompt(sessionId, prompt) {
1193
+ const connection = this.getConnection();
1194
+ const restoreConsoleError = this.options.suppressSdkConsoleErrors ? installSdkConsoleErrorSuppression() : void 0;
1195
+ let promptPromise;
1196
+ try {
1197
+ promptPromise = connection.prompt({
1198
+ sessionId,
1199
+ prompt: typeof prompt === "string" ? textPrompt(prompt) : prompt
1200
+ });
1201
+ } catch (error) {
1202
+ restoreConsoleError?.();
1203
+ throw error;
1204
+ }
1205
+ this.activePrompt = {
1206
+ sessionId,
1207
+ promise: promptPromise
1208
+ };
1209
+ try {
1210
+ const response = await promptPromise;
1211
+ const permissionFailure = this.consumePromptPermissionFailure(sessionId);
1212
+ if (permissionFailure) throw permissionFailure;
1213
+ return response;
1214
+ } catch (error) {
1215
+ const permissionFailure = this.consumePromptPermissionFailure(sessionId);
1216
+ if (permissionFailure) throw permissionFailure;
1217
+ throw error;
1218
+ } finally {
1219
+ restoreConsoleError?.();
1220
+ if (this.activePrompt?.promise === promptPromise) this.activePrompt = void 0;
1221
+ this.cancellingSessionIds.delete(sessionId);
1222
+ this.promptPermissionFailures.delete(sessionId);
1223
+ }
1224
+ }
1225
+ async setSessionMode(sessionId, modeId) {
1226
+ await this.getConnection().setSessionMode({
1227
+ sessionId,
1228
+ modeId
1229
+ });
1230
+ }
1231
+ async setSessionConfigOption(sessionId, configId, value) {
1232
+ return await this.getConnection().setSessionConfigOption({
1233
+ sessionId,
1234
+ configId,
1235
+ value
1236
+ });
1237
+ }
1238
+ async cancel(sessionId) {
1239
+ const connection = this.getConnection();
1240
+ this.cancellingSessionIds.add(sessionId);
1241
+ await connection.cancel({ sessionId });
1242
+ }
1243
+ async requestCancelActivePrompt() {
1244
+ const active = this.activePrompt;
1245
+ if (!active) return false;
1246
+ await this.cancel(active.sessionId);
1247
+ return true;
1248
+ }
1249
+ async cancelActivePrompt(waitMs = 2500) {
1250
+ const active = this.activePrompt;
1251
+ if (!active) return;
1252
+ try {
1253
+ await this.cancel(active.sessionId);
1254
+ } catch (error) {
1255
+ const message = error instanceof Error ? error.message : String(error);
1256
+ this.log(`failed to send session/cancel: ${message}`);
1257
+ }
1258
+ if (waitMs <= 0) return;
1259
+ let timer;
1260
+ const timeoutPromise = new Promise((resolve) => {
1261
+ timer = setTimeout(resolve, waitMs);
1262
+ });
1263
+ try {
1264
+ return await Promise.race([active.promise.then((response) => response, () => void 0), timeoutPromise]);
1265
+ } finally {
1266
+ if (timer) clearTimeout(timer);
1267
+ }
1268
+ }
1269
+ async close() {
1270
+ this.closing = true;
1271
+ await this.terminalManager.shutdown();
1272
+ const agent = this.agent;
1273
+ if (agent) await this.terminateAgentProcess(agent);
1274
+ this.sessionUpdateChain = Promise.resolve();
1275
+ this.observedSessionUpdates = 0;
1276
+ this.processedSessionUpdates = 0;
1277
+ this.suppressSessionUpdates = false;
1278
+ this.suppressReplaySessionUpdateMessages = false;
1279
+ this.activePrompt = void 0;
1280
+ this.cancellingSessionIds.clear();
1281
+ this.promptPermissionFailures.clear();
1282
+ this.loadedSessionId = void 0;
1283
+ this.initResult = void 0;
1284
+ this.connection = void 0;
1285
+ this.agent = void 0;
1286
+ }
1287
+ async terminateAgentProcess(child) {
1288
+ if (!child.stdin.destroyed) try {
1289
+ child.stdin.end();
1290
+ } catch {}
1291
+ let exited = await waitForChildExit(child, AGENT_CLOSE_AFTER_STDIN_END_MS);
1292
+ if (!exited && isChildProcessRunning(child)) {
1293
+ try {
1294
+ child.kill("SIGTERM");
1295
+ } catch {}
1296
+ exited = await waitForChildExit(child, AGENT_CLOSE_TERM_GRACE_MS);
1297
+ }
1298
+ if (!exited && isChildProcessRunning(child)) {
1299
+ this.log(`agent did not exit after ${AGENT_CLOSE_TERM_GRACE_MS}ms; forcing SIGKILL`);
1300
+ try {
1301
+ child.kill("SIGKILL");
1302
+ } catch {}
1303
+ exited = await waitForChildExit(child, AGENT_CLOSE_KILL_GRACE_MS);
1304
+ }
1305
+ this.detachAgentHandles(child, !exited);
1306
+ }
1307
+ detachAgentHandles(agent, unref) {
1308
+ const stdin = agent.stdin;
1309
+ const stdout = agent.stdout;
1310
+ const stderr = agent.stderr;
1311
+ stdin?.destroy();
1312
+ stdout?.destroy();
1313
+ stderr?.destroy();
1314
+ if (unref) try {
1315
+ agent.unref();
1316
+ } catch {}
1317
+ }
1318
+ getConnection() {
1319
+ if (!this.connection) throw new Error("ACP client not started");
1320
+ return this.connection;
1321
+ }
1322
+ log(message) {
1323
+ if (!this.options.verbose) return;
1324
+ process.stderr.write(`[acpx] ${message}\n`);
1325
+ }
1326
+ selectAuthMethod(methods) {
1327
+ const configCredentials = this.options.authCredentials ?? {};
1328
+ for (const method of methods) {
1329
+ const envCredential = readEnvCredential(method.id);
1330
+ if (envCredential) return {
1331
+ methodId: method.id,
1332
+ credential: envCredential,
1333
+ source: "env"
1334
+ };
1335
+ const configCredential = configCredentials[method.id] ?? configCredentials[toEnvToken(method.id)];
1336
+ if (typeof configCredential === "string" && configCredential.trim().length > 0) return {
1337
+ methodId: method.id,
1338
+ credential: configCredential,
1339
+ source: "config"
1340
+ };
1341
+ }
1342
+ }
1343
+ async authenticateIfRequired(connection, methods) {
1344
+ if (methods.length === 0) return;
1345
+ const selected = this.selectAuthMethod(methods);
1346
+ if (!selected) {
1347
+ if (this.options.authPolicy === "fail") throw new AuthPolicyError(`agent advertised auth methods [${methods.map((m) => m.id).join(", ")}] but no matching credentials found`);
1348
+ this.log(`agent advertised auth methods [${methods.map((m) => m.id).join(", ")}] but no matching credentials found — skipping (agent may handle auth internally)`);
1349
+ return;
1350
+ }
1351
+ await connection.authenticate({ methodId: selected.methodId });
1352
+ this.log(`authenticated with method ${selected.methodId} (${selected.source})`);
1353
+ }
1354
+ async handlePermissionRequest(params) {
1355
+ if (this.cancellingSessionIds.has(params.sessionId)) return { outcome: { outcome: "cancelled" } };
1356
+ let response;
1357
+ try {
1358
+ response = await resolvePermissionRequest(params, this.options.permissionMode, this.options.nonInteractivePermissions ?? "deny");
1359
+ } catch (error) {
1360
+ if (error instanceof PermissionPromptUnavailableError) {
1361
+ this.notePromptPermissionFailure(params.sessionId, error);
1362
+ this.permissionStats.requested += 1;
1363
+ this.permissionStats.cancelled += 1;
1364
+ return { outcome: { outcome: "cancelled" } };
1365
+ }
1366
+ throw error;
1367
+ }
1368
+ const decision = classifyPermissionDecision(params, response);
1369
+ this.permissionStats.requested += 1;
1370
+ if (decision === "approved") this.permissionStats.approved += 1;
1371
+ else if (decision === "denied") this.permissionStats.denied += 1;
1372
+ else this.permissionStats.cancelled += 1;
1373
+ return response;
1374
+ }
1375
+ attachAgentLifecycleObservers(child) {
1376
+ child.once("exit", (exitCode, signal) => {
1377
+ this.recordAgentExit("process_exit", exitCode, signal);
1378
+ });
1379
+ child.once("close", (exitCode, signal) => {
1380
+ this.recordAgentExit("process_close", exitCode, signal);
1381
+ });
1382
+ child.stdout.once("close", () => {
1383
+ this.recordAgentExit("pipe_close", child.exitCode ?? null, child.signalCode ?? null);
1384
+ });
1385
+ }
1386
+ recordAgentExit(reason, exitCode, signal) {
1387
+ if (this.lastAgentExit) return;
1388
+ this.lastAgentExit = {
1389
+ exitCode,
1390
+ signal,
1391
+ exitedAt: isoNow$2(),
1392
+ reason,
1393
+ unexpectedDuringPrompt: !this.closing && Boolean(this.activePrompt)
1394
+ };
1395
+ }
1396
+ notePromptPermissionFailure(sessionId, error) {
1397
+ if (!this.promptPermissionFailures.has(sessionId)) this.promptPermissionFailures.set(sessionId, error);
1398
+ }
1399
+ consumePromptPermissionFailure(sessionId) {
1400
+ const error = this.promptPermissionFailures.get(sessionId);
1401
+ if (error) this.promptPermissionFailures.delete(sessionId);
1402
+ return error;
1403
+ }
1404
+ async handleReadTextFile(params) {
1405
+ return await this.filesystem.readTextFile(params);
1406
+ }
1407
+ async handleWriteTextFile(params) {
1408
+ try {
1409
+ return await this.filesystem.writeTextFile(params);
1410
+ } catch (error) {
1411
+ if (error instanceof PermissionPromptUnavailableError) this.notePromptPermissionFailure(params.sessionId, error);
1412
+ throw error;
1413
+ }
1414
+ }
1415
+ async handleCreateTerminal(params) {
1416
+ try {
1417
+ return await this.terminalManager.createTerminal(params);
1418
+ } catch (error) {
1419
+ if (error instanceof PermissionPromptUnavailableError) this.notePromptPermissionFailure(params.sessionId, error);
1420
+ throw error;
1421
+ }
1422
+ }
1423
+ async handleTerminalOutput(params) {
1424
+ return await this.terminalManager.terminalOutput(params);
1425
+ }
1426
+ async handleWaitForTerminalExit(params) {
1427
+ return await this.terminalManager.waitForTerminalExit(params);
1428
+ }
1429
+ async handleKillTerminal(params) {
1430
+ return await this.terminalManager.killTerminal(params);
1431
+ }
1432
+ async handleReleaseTerminal(params) {
1433
+ return await this.terminalManager.releaseTerminal(params);
1434
+ }
1435
+ async handleSessionUpdate(notification) {
1436
+ const sequence = ++this.observedSessionUpdates;
1437
+ this.sessionUpdateChain = this.sessionUpdateChain.then(async () => {
1438
+ try {
1439
+ if (!this.suppressSessionUpdates) this.eventHandlers.onSessionUpdate?.(notification);
1440
+ } catch (error) {
1441
+ const message = error instanceof Error ? error.message : String(error);
1442
+ this.log(`session update handler failed: ${message}`);
1443
+ } finally {
1444
+ this.processedSessionUpdates = sequence;
1445
+ }
1446
+ });
1447
+ await this.sessionUpdateChain;
1448
+ }
1449
+ async waitForSessionUpdateDrain(idleMs, timeoutMs) {
1450
+ const normalizedIdleMs = Math.max(0, idleMs);
1451
+ const normalizedTimeoutMs = Math.max(normalizedIdleMs, timeoutMs);
1452
+ const deadline = Date.now() + normalizedTimeoutMs;
1453
+ let lastObserved = this.observedSessionUpdates;
1454
+ let idleSince = Date.now();
1455
+ while (Date.now() <= deadline) {
1456
+ const observed = this.observedSessionUpdates;
1457
+ if (observed !== lastObserved) {
1458
+ lastObserved = observed;
1459
+ idleSince = Date.now();
1460
+ }
1461
+ if (this.processedSessionUpdates === this.observedSessionUpdates && Date.now() - idleSince >= normalizedIdleMs) {
1462
+ await this.sessionUpdateChain;
1463
+ if (this.processedSessionUpdates === this.observedSessionUpdates) return;
1464
+ }
1465
+ await new Promise((resolve) => {
1466
+ setTimeout(resolve, DRAIN_POLL_INTERVAL_MS);
1467
+ });
1468
+ }
1469
+ throw new Error(`Timed out waiting for session replay drain after ${normalizedTimeoutMs}ms`);
1470
+ }
1471
+ };
1472
+
1473
+ //#endregion
1474
+ //#region src/perf-metrics-capture.ts
1475
+ const PERF_METRICS_FILE_ENV = "ACPX_PERF_METRICS_FILE";
1476
+ let installed = false;
1477
+ let flushed = false;
1478
+ let captureFilePath;
1479
+ let captureRole = "cli";
1480
+ let captureArgv = [];
1481
+ let captureSequence = 0;
1482
+ function shouldCapture() {
1483
+ return typeof captureFilePath === "string" && captureFilePath.trim().length > 0;
1484
+ }
1485
+ function buildPayload(reason) {
1486
+ return {
1487
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1488
+ pid: process.pid,
1489
+ ppid: process.ppid,
1490
+ role: captureRole,
1491
+ argv: captureArgv,
1492
+ cwd: process.cwd(),
1493
+ sequence: captureSequence,
1494
+ reason,
1495
+ metrics: getPerfMetricsSnapshot()
1496
+ };
1497
+ }
1498
+ function writePerfMetricsCapture(reason, resetAfterWrite) {
1499
+ if (!shouldCapture()) return false;
1500
+ const payload = buildPayload(reason);
1501
+ const metrics = payload.metrics;
1502
+ if (!(Object.keys(metrics.counters ?? {}).length > 0 || Object.keys(metrics.gauges ?? {}).length > 0 || Object.keys(metrics.timings ?? {}).length > 0)) return false;
1503
+ try {
1504
+ fs.mkdirSync(path.dirname(captureFilePath), { recursive: true });
1505
+ fs.appendFileSync(captureFilePath, `${JSON.stringify(payload)}\n`, "utf8");
1506
+ captureSequence += 1;
1507
+ if (resetAfterWrite) resetPerfMetrics();
1508
+ return true;
1509
+ } catch {
1510
+ return false;
1511
+ }
1512
+ }
1513
+ function checkpointPerfMetricsCapture() {
1514
+ flushed = false;
1515
+ writePerfMetricsCapture("checkpoint", true);
1516
+ }
1517
+ function flushPerfMetricsCapture(reason = "exit") {
1518
+ if (flushed || !shouldCapture()) return;
1519
+ flushed = true;
1520
+ writePerfMetricsCapture(reason, false);
1521
+ }
1522
+ function installPerfMetricsCapture(options = {}) {
1523
+ captureFilePath = options.filePath ?? process.env[PERF_METRICS_FILE_ENV];
1524
+ if (!shouldCapture()) return;
1525
+ resetPerfMetrics();
1526
+ captureRole = options.role ?? captureRole;
1527
+ captureArgv = options.argv ?? [];
1528
+ captureSequence = 0;
1529
+ flushed = false;
1530
+ if (installed) return;
1531
+ installed = true;
1532
+ process.once("exit", () => {
1533
+ flushPerfMetricsCapture("exit");
1534
+ });
1535
+ for (const signal of ["SIGINT", "SIGTERM"]) {
1536
+ const handler = () => {
1537
+ flushPerfMetricsCapture("signal");
1538
+ process.removeListener(signal, handler);
1539
+ process.kill(process.pid, signal);
1540
+ };
1541
+ process.once(signal, handler);
1542
+ }
1543
+ }
1544
+
1545
+ //#endregion
1546
+ //#region src/session-conversation-model.ts
1547
+ const MAX_RUNTIME_MESSAGES = 200;
1548
+ const MAX_RUNTIME_AGENT_TEXT_CHARS = 8e3;
1549
+ const MAX_RUNTIME_THINKING_CHARS = 4e3;
1550
+ const MAX_RUNTIME_TOOL_IO_CHARS = 4e3;
1551
+ const MAX_RUNTIME_REQUEST_TOKEN_USAGE = 100;
1552
+ function isoNow$1() {
1553
+ return (/* @__PURE__ */ new Date()).toISOString();
1554
+ }
1555
+ function deepClone(value) {
1556
+ try {
1557
+ return structuredClone(value);
1558
+ } catch {
1559
+ return value;
1560
+ }
1561
+ }
1562
+ function hasOwn$1(source, key) {
1563
+ return Object.prototype.hasOwnProperty.call(source, key);
1564
+ }
1565
+ function normalizeAgentName(value) {
1566
+ if (typeof value !== "string") return;
1567
+ const trimmed = value.trim();
1568
+ return trimmed.length > 0 ? trimmed : void 0;
1569
+ }
1570
+ function extractText(content) {
1571
+ if (content.type === "text") return content.text;
1572
+ if (content.type === "resource_link") return content.title ?? content.name ?? content.uri;
1573
+ if (content.type === "resource") {
1574
+ if ("text" in content.resource && typeof content.resource.text === "string") return content.resource.text;
1575
+ return content.resource.uri;
1576
+ }
1577
+ }
1578
+ function contentToUserContent(content) {
1579
+ if (content.type === "text") return { Text: content.text };
1580
+ if (content.type === "resource_link") {
1581
+ const value = content.title ?? content.name ?? content.uri;
1582
+ return { Mention: {
1583
+ uri: content.uri,
1584
+ content: value
1585
+ } };
1586
+ }
1587
+ if (content.type === "resource") {
1588
+ if ("text" in content.resource && typeof content.resource.text === "string") return { Text: content.resource.text };
1589
+ return { Mention: {
1590
+ uri: content.resource.uri,
1591
+ content: content.resource.uri
1592
+ } };
1593
+ }
1594
+ if (content.type === "image") return { Image: {
1595
+ source: content.data,
1596
+ size: null
1597
+ } };
1598
+ }
1599
+ function nextUserMessageId() {
1600
+ return randomUUID();
1601
+ }
1602
+ function isUserMessage$1(message) {
1603
+ return typeof message === "object" && message !== null && hasOwn$1(message, "User");
1604
+ }
1605
+ function isAgentMessage$1(message) {
1606
+ return typeof message === "object" && message !== null && hasOwn$1(message, "Agent");
1607
+ }
1608
+ function isAgentTextContent(content) {
1609
+ return hasOwn$1(content, "Text");
1610
+ }
1611
+ function isAgentThinkingContent(content) {
1612
+ return hasOwn$1(content, "Thinking");
1613
+ }
1614
+ function isAgentToolUseContent(content) {
1615
+ return hasOwn$1(content, "ToolUse");
1616
+ }
1617
+ function updateConversationTimestamp(conversation, timestamp) {
1618
+ conversation.updated_at = timestamp;
1619
+ }
1620
+ function ensureAgentMessage(conversation) {
1621
+ const last = conversation.messages.at(-1);
1622
+ if (last && isAgentMessage$1(last)) return last.Agent;
1623
+ const created = {
1624
+ content: [],
1625
+ tool_results: {}
1626
+ };
1627
+ conversation.messages.push({ Agent: created });
1628
+ return created;
1629
+ }
1630
+ function appendAgentText(agent, text) {
1631
+ if (!text.trim()) return;
1632
+ const last = agent.content.at(-1);
1633
+ if (last && isAgentTextContent(last)) {
1634
+ last.Text = trimRuntimeText(`${last.Text}${text}`, MAX_RUNTIME_AGENT_TEXT_CHARS);
1635
+ return;
1636
+ }
1637
+ const next = { Text: text };
1638
+ agent.content.push(next);
1639
+ }
1640
+ function appendAgentThinking(agent, text) {
1641
+ if (!text.trim()) return;
1642
+ const last = agent.content.at(-1);
1643
+ if (last && isAgentThinkingContent(last)) {
1644
+ last.Thinking.text = trimRuntimeText(`${last.Thinking.text}${text}`, MAX_RUNTIME_THINKING_CHARS);
1645
+ return;
1646
+ }
1647
+ const next = { Thinking: {
1648
+ text,
1649
+ signature: null
1650
+ } };
1651
+ agent.content.push(next);
1652
+ }
1653
+ function trimRuntimeText(value, maxChars) {
1654
+ if (value.length <= maxChars) return value;
1655
+ return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
1656
+ }
1657
+ function statusIndicatesComplete(status) {
1658
+ if (typeof status !== "string") return false;
1659
+ const normalized = status.toLowerCase();
1660
+ return normalized.includes("complete") || normalized.includes("done") || normalized.includes("success") || normalized.includes("failed") || normalized.includes("error") || normalized.includes("cancel");
1661
+ }
1662
+ function statusIndicatesError(status) {
1663
+ if (typeof status !== "string") return false;
1664
+ const normalized = status.toLowerCase();
1665
+ return normalized.includes("fail") || normalized.includes("error");
1666
+ }
1667
+ function toToolResultContent(value) {
1668
+ if (typeof value === "string") return { Text: trimRuntimeText(value, MAX_RUNTIME_TOOL_IO_CHARS) };
1669
+ if (value != null) try {
1670
+ return { Text: trimRuntimeText(JSON.stringify(value), MAX_RUNTIME_TOOL_IO_CHARS) };
1671
+ } catch {
1672
+ return { Text: "[Unserializable value]" };
1673
+ }
1674
+ return { Text: "" };
1675
+ }
1676
+ function toRawInput(value) {
1677
+ if (typeof value === "string") return trimRuntimeText(value, MAX_RUNTIME_TOOL_IO_CHARS);
1678
+ try {
1679
+ return trimRuntimeText(JSON.stringify(value ?? {}), MAX_RUNTIME_TOOL_IO_CHARS);
1680
+ } catch {
1681
+ return value == null ? "" : "[Unserializable input]";
1682
+ }
1683
+ }
1684
+ function ensureToolUseContent(agent, toolCallId) {
1685
+ for (const content of agent.content) if (isAgentToolUseContent(content) && content.ToolUse.id === toolCallId) return content.ToolUse;
1686
+ const created = {
1687
+ id: toolCallId,
1688
+ name: "tool_call",
1689
+ raw_input: "{}",
1690
+ input: {},
1691
+ is_input_complete: false,
1692
+ thought_signature: null
1693
+ };
1694
+ agent.content.push({ ToolUse: created });
1695
+ return created;
1696
+ }
1697
+ function upsertToolResult(agent, toolCallId, patch) {
1698
+ const existing = agent.tool_results[toolCallId];
1699
+ const next = {
1700
+ tool_use_id: toolCallId,
1701
+ tool_name: patch.tool_name ?? existing?.tool_name ?? "tool_call",
1702
+ is_error: patch.is_error ?? existing?.is_error ?? false,
1703
+ content: patch.content ?? existing?.content ?? { Text: "" },
1704
+ output: patch.output ?? existing?.output
1705
+ };
1706
+ agent.tool_results[toolCallId] = next;
1707
+ }
1708
+ function applyToolCallUpdate(agent, update) {
1709
+ const tool = ensureToolUseContent(agent, update.toolCallId);
1710
+ if (hasOwn$1(update, "title")) tool.name = normalizeAgentName(update.title) ?? tool.name ?? "tool_call";
1711
+ if (hasOwn$1(update, "kind")) {
1712
+ const kindName = normalizeAgentName(update.kind);
1713
+ if (!tool.name || tool.name === "tool_call") tool.name = kindName ?? tool.name;
1714
+ }
1715
+ if (hasOwn$1(update, "rawInput")) {
1716
+ const rawInput = deepClone(update.rawInput);
1717
+ tool.input = rawInput ?? {};
1718
+ tool.raw_input = toRawInput(rawInput);
1719
+ }
1720
+ if (hasOwn$1(update, "status")) tool.is_input_complete = statusIndicatesComplete(update.status);
1721
+ if (hasOwn$1(update, "rawOutput") || hasOwn$1(update, "status") || hasOwn$1(update, "title") || hasOwn$1(update, "kind")) {
1722
+ const status = update.status;
1723
+ const output = hasOwn$1(update, "rawOutput") ? deepClone(update.rawOutput) : void 0;
1724
+ upsertToolResult(agent, update.toolCallId, {
1725
+ tool_name: tool.name,
1726
+ is_error: statusIndicatesError(status),
1727
+ content: output === void 0 ? void 0 : toToolResultContent(output),
1728
+ output
1729
+ });
1730
+ }
1731
+ }
1732
+ function asRecord$2(value) {
1733
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
1734
+ return value;
1735
+ }
1736
+ function numberField(source, keys) {
1737
+ for (const key of keys) {
1738
+ const value = source[key];
1739
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
1740
+ }
1741
+ }
1742
+ function usageToTokenUsage(update) {
1743
+ const updateRecord = asRecord$2(update);
1744
+ const usageMeta = asRecord$2(updateRecord?._meta)?.usage;
1745
+ const source = asRecord$2(usageMeta) ?? updateRecord;
1746
+ if (!source) return;
1747
+ const normalized = {
1748
+ input_tokens: numberField(source, ["input_tokens", "inputTokens"]),
1749
+ output_tokens: numberField(source, ["output_tokens", "outputTokens"]),
1750
+ cache_creation_input_tokens: numberField(source, [
1751
+ "cache_creation_input_tokens",
1752
+ "cacheCreationInputTokens",
1753
+ "cachedWriteTokens"
1754
+ ]),
1755
+ cache_read_input_tokens: numberField(source, [
1756
+ "cache_read_input_tokens",
1757
+ "cacheReadInputTokens",
1758
+ "cachedReadTokens"
1759
+ ])
1760
+ };
1761
+ if (normalized.input_tokens === void 0 && normalized.output_tokens === void 0 && normalized.cache_creation_input_tokens === void 0 && normalized.cache_read_input_tokens === void 0) return;
1762
+ return normalized;
1763
+ }
1764
+ function ensureAcpxState$1(state) {
1765
+ return state ?? {};
1766
+ }
1767
+ function lastUserMessageId(conversation) {
1768
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
1769
+ const message = conversation.messages[index];
1770
+ if (message && isUserMessage$1(message)) return message.User.id;
1771
+ }
1772
+ }
1773
+ function createSessionConversation(timestamp = isoNow$1()) {
1774
+ return {
1775
+ title: null,
1776
+ messages: [],
1777
+ updated_at: timestamp,
1778
+ cumulative_token_usage: {},
1779
+ request_token_usage: {}
1780
+ };
1781
+ }
1782
+ function cloneSessionConversation(conversation) {
1783
+ if (!conversation) return createSessionConversation();
1784
+ return {
1785
+ title: conversation.title,
1786
+ messages: deepClone(conversation.messages ?? []),
1787
+ updated_at: conversation.updated_at,
1788
+ cumulative_token_usage: deepClone(conversation.cumulative_token_usage ?? {}),
1789
+ request_token_usage: deepClone(conversation.request_token_usage ?? {})
1790
+ };
1791
+ }
1792
+ function cloneSessionAcpxState(state) {
1793
+ if (!state) return;
1794
+ return {
1795
+ current_mode_id: state.current_mode_id,
1796
+ desired_mode_id: state.desired_mode_id,
1797
+ available_commands: state.available_commands ? [...state.available_commands] : void 0,
1798
+ config_options: state.config_options ? deepClone(state.config_options) : void 0
1799
+ };
1800
+ }
1801
+ function recordPromptSubmission(conversation, prompt, timestamp = isoNow$1()) {
1802
+ const userContent = (typeof prompt === "string" ? textPrompt(prompt) : prompt).map((content) => contentToUserContent(content)).filter((content) => content !== void 0);
1803
+ if (userContent.length === 0) return;
1804
+ conversation.messages.push({ User: {
1805
+ id: nextUserMessageId(),
1806
+ content: userContent.map((content) => {
1807
+ if ("Text" in content) return { Text: trimRuntimeText(content.Text, MAX_RUNTIME_AGENT_TEXT_CHARS) };
1808
+ return content;
1809
+ })
1810
+ } });
1811
+ updateConversationTimestamp(conversation, timestamp);
1812
+ trimConversationForRuntime(conversation);
1813
+ }
1814
+ function recordSessionUpdate(conversation, state, notification, timestamp = isoNow$1()) {
1815
+ const acpx = ensureAcpxState$1(state);
1816
+ const update = notification.update;
1817
+ switch (update.sessionUpdate) {
1818
+ case "user_message_chunk": {
1819
+ const userContent = contentToUserContent(update.content);
1820
+ if (userContent) conversation.messages.push({ User: {
1821
+ id: nextUserMessageId(),
1822
+ content: [userContent]
1823
+ } });
1824
+ break;
1825
+ }
1826
+ case "agent_message_chunk": {
1827
+ const text = extractText(update.content);
1828
+ if (text) appendAgentText(ensureAgentMessage(conversation), text);
1829
+ break;
1830
+ }
1831
+ case "agent_thought_chunk": {
1832
+ const text = extractText(update.content);
1833
+ if (text) appendAgentThinking(ensureAgentMessage(conversation), text);
1834
+ break;
1835
+ }
1836
+ case "tool_call":
1837
+ case "tool_call_update":
1838
+ applyToolCallUpdate(ensureAgentMessage(conversation), update);
1839
+ break;
1840
+ case "usage_update": {
1841
+ const usage = usageToTokenUsage(update);
1842
+ if (usage) {
1843
+ conversation.cumulative_token_usage = usage;
1844
+ const userId = lastUserMessageId(conversation);
1845
+ if (userId) conversation.request_token_usage[userId] = usage;
1846
+ }
1847
+ break;
1848
+ }
1849
+ case "session_info_update":
1850
+ if (hasOwn$1(update, "title")) conversation.title = update.title ?? null;
1851
+ if (hasOwn$1(update, "updatedAt")) conversation.updated_at = update.updatedAt ?? conversation.updated_at;
1852
+ break;
1853
+ case "available_commands_update":
1854
+ acpx.available_commands = update.availableCommands.map((entry) => entry.name).filter((entry) => typeof entry === "string" && entry.trim().length > 0);
1855
+ break;
1856
+ case "current_mode_update":
1857
+ acpx.current_mode_id = update.currentModeId;
1858
+ break;
1859
+ case "config_option_update":
1860
+ acpx.config_options = deepClone(update.configOptions);
1861
+ break;
1862
+ default: break;
1863
+ }
1864
+ updateConversationTimestamp(conversation, timestamp);
1865
+ trimConversationForRuntime(conversation);
1866
+ return acpx;
1867
+ }
1868
+ function recordClientOperation(conversation, state, operation, timestamp = isoNow$1()) {
1869
+ const acpx = ensureAcpxState$1(state);
1870
+ updateConversationTimestamp(conversation, timestamp);
1871
+ trimConversationForRuntime(conversation);
1872
+ return acpx;
1873
+ }
1874
+ function trimConversationForRuntime(conversation) {
1875
+ if (conversation.messages.length > MAX_RUNTIME_MESSAGES) conversation.messages = conversation.messages.slice(-MAX_RUNTIME_MESSAGES);
1876
+ for (const message of conversation.messages) {
1877
+ if (!isAgentMessage$1(message)) {
1878
+ if (isUserMessage$1(message)) message.User.content = message.User.content.map((content) => {
1879
+ if ("Text" in content) return { Text: trimRuntimeText(content.Text, MAX_RUNTIME_AGENT_TEXT_CHARS) };
1880
+ return content;
1881
+ });
1882
+ continue;
1883
+ }
1884
+ for (const content of message.Agent.content) if ("Text" in content) content.Text = trimRuntimeText(content.Text, MAX_RUNTIME_AGENT_TEXT_CHARS);
1885
+ else if ("Thinking" in content) content.Thinking.text = trimRuntimeText(content.Thinking.text, MAX_RUNTIME_THINKING_CHARS);
1886
+ else if ("ToolUse" in content) content.ToolUse.raw_input = trimRuntimeText(content.ToolUse.raw_input, MAX_RUNTIME_TOOL_IO_CHARS);
1887
+ for (const result of Object.values(message.Agent.tool_results)) {
1888
+ if ("Text" in result.content) result.content.Text = trimRuntimeText(result.content.Text, MAX_RUNTIME_TOOL_IO_CHARS);
1889
+ if (typeof result.output === "string") result.output = trimRuntimeText(result.output, MAX_RUNTIME_TOOL_IO_CHARS);
1890
+ }
1891
+ }
1892
+ const requestUsageEntries = Object.entries(conversation.request_token_usage);
1893
+ if (requestUsageEntries.length > MAX_RUNTIME_REQUEST_TOKEN_USAGE) conversation.request_token_usage = Object.fromEntries(requestUsageEntries.slice(-MAX_RUNTIME_REQUEST_TOKEN_USAGE));
1894
+ }
1895
+
1896
+ //#endregion
1897
+ //#region src/session-event-log.ts
1898
+ const DEFAULT_EVENT_SEGMENT_MAX_BYTES = 64 * 1024 * 1024;
1899
+ const DEFAULT_EVENT_MAX_SEGMENTS = 5;
1900
+ function sessionBaseDir$1() {
1901
+ return path.join(os.homedir(), ".acpx", "sessions");
1902
+ }
1903
+ function safeSessionId(sessionId) {
1904
+ return encodeURIComponent(sessionId);
1905
+ }
1906
+ function sessionEventActivePath(sessionId) {
1907
+ return path.join(sessionBaseDir$1(), `${safeSessionId(sessionId)}.stream.ndjson`);
1908
+ }
1909
+ function sessionEventSegmentPath(sessionId, segment) {
1910
+ return path.join(sessionBaseDir$1(), `${safeSessionId(sessionId)}.stream.${segment}.ndjson`);
1911
+ }
1912
+ function sessionEventLockPath(sessionId) {
1913
+ return path.join(sessionBaseDir$1(), `${safeSessionId(sessionId)}.stream.lock`);
1914
+ }
1915
+ function defaultSessionEventLog(sessionId) {
1916
+ return {
1917
+ active_path: sessionEventActivePath(sessionId),
1918
+ segment_count: DEFAULT_EVENT_MAX_SEGMENTS,
1919
+ max_segment_bytes: DEFAULT_EVENT_SEGMENT_MAX_BYTES,
1920
+ max_segments: DEFAULT_EVENT_MAX_SEGMENTS,
1921
+ last_write_at: void 0,
1922
+ last_write_error: null
1923
+ };
1924
+ }
1925
+
1926
+ //#endregion
1927
+ //#region src/session-persistence/serialize.ts
1928
+ function serializeSessionRecordForDisk(record) {
1929
+ const canonical = {
1930
+ ...record,
1931
+ schema: SESSION_RECORD_SCHEMA
1932
+ };
1933
+ return {
1934
+ schema: canonical.schema,
1935
+ acpx_record_id: canonical.acpxRecordId,
1936
+ acp_session_id: canonical.acpSessionId,
1937
+ agent_session_id: normalizeRuntimeSessionId(canonical.agentSessionId),
1938
+ agent_command: canonical.agentCommand,
1939
+ cwd: canonical.cwd,
1940
+ name: canonical.name,
1941
+ created_at: canonical.createdAt,
1942
+ last_used_at: canonical.lastUsedAt,
1943
+ last_seq: canonical.lastSeq,
1944
+ last_request_id: canonical.lastRequestId,
1945
+ event_log: canonical.eventLog,
1946
+ closed: canonical.closed,
1947
+ closed_at: canonical.closedAt,
1948
+ pid: canonical.pid,
1949
+ agent_started_at: canonical.agentStartedAt,
1950
+ last_prompt_at: canonical.lastPromptAt,
1951
+ last_agent_exit_code: canonical.lastAgentExitCode,
1952
+ last_agent_exit_signal: canonical.lastAgentExitSignal,
1953
+ last_agent_exit_at: canonical.lastAgentExitAt,
1954
+ last_agent_disconnect_reason: canonical.lastAgentDisconnectReason,
1955
+ protocol_version: canonical.protocolVersion,
1956
+ agent_capabilities: canonical.agentCapabilities,
1957
+ title: canonical.title,
1958
+ messages: canonical.messages,
1959
+ updated_at: canonical.updated_at,
1960
+ cumulative_token_usage: canonical.cumulative_token_usage,
1961
+ request_token_usage: canonical.request_token_usage,
1962
+ acpx: canonical.acpx
1963
+ };
1964
+ }
1965
+
1966
+ //#endregion
1967
+ //#region src/persisted-key-policy.ts
1968
+ const SNAKE_CASE_KEY = /^[a-z][a-z0-9_]*$/;
1969
+ const ZED_TAG_KEYS = new Set([
1970
+ "User",
1971
+ "Agent",
1972
+ "Resume",
1973
+ "Text",
1974
+ "Mention",
1975
+ "Image",
1976
+ "Thinking",
1977
+ "RedactedThinking",
1978
+ "ToolUse"
1979
+ ]);
1980
+ const MAP_OBJECT_PATHS = new Set(["request_token_usage", "messages.Agent.tool_results"]);
1981
+ const OPAQUE_VALUE_PATHS = new Set([
1982
+ "agent_capabilities",
1983
+ "messages.Agent.content.ToolUse.input",
1984
+ "acpx.config_options"
1985
+ ]);
1986
+ function isRecord(value) {
1987
+ return !!value && typeof value === "object" && !Array.isArray(value);
1988
+ }
1989
+ function joinPath(path) {
1990
+ return path.join(".");
1991
+ }
1992
+ function isAllowedKey(path, key) {
1993
+ if (ZED_TAG_KEYS.has(key)) return true;
1994
+ return false;
1995
+ }
1996
+ function shouldSkipKeyRule(path) {
1997
+ return MAP_OBJECT_PATHS.has(joinPath(path));
1998
+ }
1999
+ function shouldSkipDescend(path) {
2000
+ return OPAQUE_VALUE_PATHS.has(joinPath(path)) || isToolResultOutputPath(path);
2001
+ }
2002
+ function isToolResultOutputPath(path) {
2003
+ if (path.length < 5 || path[path.length - 1] !== "output") return false;
2004
+ const toolResultsIndex = path.lastIndexOf("tool_results");
2005
+ if (toolResultsIndex === -1 || toolResultsIndex + 2 !== path.length - 1) return false;
2006
+ return path.slice(0, toolResultsIndex + 1).join(".") === "messages.Agent.tool_results";
2007
+ }
2008
+ function collectViolations(value, path, violations) {
2009
+ if (Array.isArray(value)) {
2010
+ for (const entry of value) collectViolations(entry, path, violations);
2011
+ return;
2012
+ }
2013
+ if (!isRecord(value)) return;
2014
+ const skipKeyRule = shouldSkipKeyRule(path);
2015
+ for (const [key, child] of Object.entries(value)) {
2016
+ if (!skipKeyRule && !SNAKE_CASE_KEY.test(key) && !isAllowedKey(path, key)) violations.push(`${joinPath(path)}.${key}`.replace(/^\./, ""));
2017
+ const childPath = [...path, key];
2018
+ if (shouldSkipDescend(childPath)) continue;
2019
+ collectViolations(child, childPath, violations);
2020
+ }
2021
+ }
2022
+ function findPersistedKeyPolicyViolations(value) {
2023
+ const violations = [];
2024
+ collectViolations(value, [], violations);
2025
+ return violations;
2026
+ }
2027
+ function assertPersistedKeyPolicy(value) {
2028
+ const violations = findPersistedKeyPolicyViolations(value);
2029
+ if (violations.length === 0) return;
2030
+ throw new Error(`Persisted key policy violation (expected snake_case keys): ${violations.join(", ")}`);
2031
+ }
2032
+
2033
+ //#endregion
2034
+ //#region src/session-persistence/parse.ts
2035
+ function asRecord$1(value) {
2036
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2037
+ return value;
2038
+ }
2039
+ function hasOwn(source, key) {
2040
+ return Object.prototype.hasOwnProperty.call(source, key);
2041
+ }
2042
+ function isStringArray(value) {
2043
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
2044
+ }
2045
+ function parseTokenUsage(raw) {
2046
+ if (raw === void 0 || raw === null) return;
2047
+ const record = asRecord$1(raw);
2048
+ if (!record) return null;
2049
+ const usage = {};
2050
+ for (const field of [
2051
+ "input_tokens",
2052
+ "output_tokens",
2053
+ "cache_creation_input_tokens",
2054
+ "cache_read_input_tokens"
2055
+ ]) {
2056
+ const value = record[field];
2057
+ if (value === void 0) continue;
2058
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
2059
+ usage[field] = value;
2060
+ }
2061
+ return usage;
2062
+ }
2063
+ function parseRequestTokenUsage(raw) {
2064
+ if (raw === void 0 || raw === null) return;
2065
+ const record = asRecord$1(raw);
2066
+ if (!record) return null;
2067
+ const usage = {};
2068
+ for (const [key, value] of Object.entries(record)) {
2069
+ const parsed = parseTokenUsage(value);
2070
+ if (parsed == null) return null;
2071
+ usage[key] = parsed;
2072
+ }
2073
+ return usage;
2074
+ }
2075
+ function isSessionMessageImage(raw) {
2076
+ const record = asRecord$1(raw);
2077
+ if (!record || typeof record.source !== "string") return false;
2078
+ if (record.size === void 0 || record.size === null) return true;
2079
+ const size = asRecord$1(record.size);
2080
+ return !!size && typeof size.width === "number" && Number.isFinite(size.width) && typeof size.height === "number" && Number.isFinite(size.height);
2081
+ }
2082
+ function isUserContent(raw) {
2083
+ const record = asRecord$1(raw);
2084
+ if (!record) return false;
2085
+ if (typeof record.Text === "string") return true;
2086
+ if (record.Mention !== void 0) {
2087
+ const mention = asRecord$1(record.Mention);
2088
+ return !!mention && typeof mention.uri === "string" && typeof mention.content === "string";
2089
+ }
2090
+ if (record.Image !== void 0) return isSessionMessageImage(record.Image);
2091
+ return false;
2092
+ }
2093
+ function isToolUse(raw) {
2094
+ const record = asRecord$1(raw);
2095
+ return !!record && typeof record.id === "string" && typeof record.name === "string" && typeof record.raw_input === "string" && hasOwn(record, "input") && typeof record.is_input_complete === "boolean" && (record.thought_signature === void 0 || record.thought_signature === null || typeof record.thought_signature === "string");
2096
+ }
2097
+ function isToolResultContent(raw) {
2098
+ const record = asRecord$1(raw);
2099
+ if (!record) return false;
2100
+ if (typeof record.Text === "string") return true;
2101
+ if (record.Image !== void 0) return isSessionMessageImage(record.Image);
2102
+ return false;
2103
+ }
2104
+ function isToolResult(raw) {
2105
+ const record = asRecord$1(raw);
2106
+ return !!record && typeof record.tool_use_id === "string" && typeof record.tool_name === "string" && typeof record.is_error === "boolean" && isToolResultContent(record.content);
2107
+ }
2108
+ function isAgentContent(raw) {
2109
+ const record = asRecord$1(raw);
2110
+ if (!record) return false;
2111
+ if (typeof record.Text === "string") return true;
2112
+ if (record.Thinking !== void 0) {
2113
+ const thinking = asRecord$1(record.Thinking);
2114
+ return !!thinking && typeof thinking.text === "string" && (thinking.signature === void 0 || thinking.signature === null || typeof thinking.signature === "string");
2115
+ }
2116
+ if (typeof record.RedactedThinking === "string") return true;
2117
+ if (record.ToolUse !== void 0) return isToolUse(record.ToolUse);
2118
+ return false;
2119
+ }
2120
+ function isUserMessage(raw) {
2121
+ const record = asRecord$1(raw);
2122
+ if (!record || record.User === void 0) return false;
2123
+ const user = asRecord$1(record.User);
2124
+ return !!user && typeof user.id === "string" && Array.isArray(user.content) && user.content.every((entry) => isUserContent(entry));
2125
+ }
2126
+ function isAgentMessage(raw) {
2127
+ const record = asRecord$1(raw);
2128
+ if (!record || record.Agent === void 0) return false;
2129
+ const agent = asRecord$1(record.Agent);
2130
+ if (!agent || !Array.isArray(agent.content) || !agent.content.every(isAgentContent)) return false;
2131
+ const toolResults = asRecord$1(agent.tool_results);
2132
+ if (!toolResults) return false;
2133
+ return Object.values(toolResults).every(isToolResult);
2134
+ }
2135
+ function isConversationMessage(raw) {
2136
+ return raw === "Resume" || isUserMessage(raw) || isAgentMessage(raw);
2137
+ }
2138
+ function parseConversationRecord(record) {
2139
+ if (!Array.isArray(record.messages) || !record.messages.every(isConversationMessage) || typeof record.updated_at !== "string") return;
2140
+ if (record.title !== void 0 && record.title !== null && typeof record.title !== "string") return;
2141
+ const cumulativeTokenUsage = parseTokenUsage(record.cumulative_token_usage);
2142
+ const requestTokenUsage = parseRequestTokenUsage(record.request_token_usage);
2143
+ if (cumulativeTokenUsage === null || requestTokenUsage === null) return;
2144
+ return {
2145
+ title: record.title === void 0 || record.title === null || typeof record.title === "string" ? record.title : null,
2146
+ messages: record.messages,
2147
+ updated_at: record.updated_at,
2148
+ cumulative_token_usage: cumulativeTokenUsage ?? {},
2149
+ request_token_usage: requestTokenUsage ?? {}
2150
+ };
2151
+ }
2152
+ function parseAcpxState(raw) {
2153
+ const record = asRecord$1(raw);
2154
+ if (!record) return;
2155
+ const state = {};
2156
+ if (typeof record.current_mode_id === "string") state.current_mode_id = record.current_mode_id;
2157
+ if (typeof record.desired_mode_id === "string") state.desired_mode_id = record.desired_mode_id;
2158
+ if (isStringArray(record.available_commands)) state.available_commands = [...record.available_commands];
2159
+ if (Array.isArray(record.config_options)) state.config_options = record.config_options;
2160
+ return state;
2161
+ }
2162
+ function parseEventLog(raw, sessionId) {
2163
+ const record = asRecord$1(raw);
2164
+ if (!record) return defaultSessionEventLog(sessionId);
2165
+ if (typeof record.active_path !== "string" || typeof record.segment_count !== "number" || !Number.isInteger(record.segment_count) || record.segment_count < 1 || typeof record.max_segment_bytes !== "number" || !Number.isInteger(record.max_segment_bytes) || record.max_segment_bytes < 1 || typeof record.max_segments !== "number" || !Number.isInteger(record.max_segments) || record.max_segments < 1) return defaultSessionEventLog(sessionId);
2166
+ return {
2167
+ active_path: record.active_path,
2168
+ segment_count: record.segment_count,
2169
+ max_segment_bytes: record.max_segment_bytes,
2170
+ max_segments: record.max_segments,
2171
+ last_write_at: typeof record.last_write_at === "string" ? record.last_write_at : void 0,
2172
+ last_write_error: record.last_write_error == null || typeof record.last_write_error === "string" ? record.last_write_error : null
2173
+ };
2174
+ }
2175
+ function normalizeOptionalName(value) {
2176
+ if (value == null) return;
2177
+ if (typeof value !== "string") return null;
2178
+ const trimmed = value.trim();
2179
+ return trimmed.length > 0 ? trimmed : void 0;
2180
+ }
2181
+ function normalizeOptionalPid(value) {
2182
+ if (value == null) return;
2183
+ if (!Number.isInteger(value) || value <= 0) return null;
2184
+ return value;
2185
+ }
2186
+ function normalizeOptionalBoolean(value, fallback = false) {
2187
+ if (value == null) return fallback;
2188
+ return typeof value === "boolean" ? value : null;
2189
+ }
2190
+ function normalizeOptionalString(value) {
2191
+ if (value == null) return;
2192
+ return typeof value === "string" ? value : null;
2193
+ }
2194
+ function normalizeOptionalExitCode(value) {
2195
+ if (value === void 0) return;
2196
+ if (value === null) return null;
2197
+ if (Number.isInteger(value)) return value;
2198
+ return Symbol("invalid");
2199
+ }
2200
+ function normalizeOptionalSignal(value) {
2201
+ if (value === void 0) return;
2202
+ if (value === null) return null;
2203
+ if (typeof value === "string") return value;
2204
+ return Symbol("invalid");
2205
+ }
2206
+ function parseSessionRecord(raw) {
2207
+ const record = asRecord$1(raw);
2208
+ if (!record) return null;
2209
+ if (record.schema !== SESSION_RECORD_SCHEMA) return null;
2210
+ const name = normalizeOptionalName(record.name);
2211
+ const pid = normalizeOptionalPid(record.pid);
2212
+ const closed = normalizeOptionalBoolean(record.closed, false);
2213
+ const closedAt = normalizeOptionalString(record.closed_at);
2214
+ const agentStartedAt = normalizeOptionalString(record.agent_started_at);
2215
+ const lastPromptAt = normalizeOptionalString(record.last_prompt_at);
2216
+ const lastAgentExitCode = normalizeOptionalExitCode(record.last_agent_exit_code);
2217
+ const lastAgentExitSignal = normalizeOptionalSignal(record.last_agent_exit_signal);
2218
+ const lastAgentExitAt = normalizeOptionalString(record.last_agent_exit_at);
2219
+ const lastAgentDisconnectReason = normalizeOptionalString(record.last_agent_disconnect_reason);
2220
+ if (typeof record.acpx_record_id !== "string" || typeof record.acp_session_id !== "string" || typeof record.agent_command !== "string" || typeof record.cwd !== "string" || typeof record.created_at !== "string" || typeof record.last_used_at !== "string" || typeof record.last_seq !== "number" || !Number.isInteger(record.last_seq) || record.last_seq < 0 || name === null || pid === null || closed === null || closedAt === null || agentStartedAt === null || lastPromptAt === null || typeof lastAgentExitCode === "symbol" || typeof lastAgentExitSignal === "symbol" || lastAgentExitAt === null || lastAgentDisconnectReason === null) return null;
2221
+ const conversation = parseConversationRecord(record);
2222
+ if (!conversation) return null;
2223
+ const eventLog = parseEventLog(record.event_log, record.acpx_record_id);
2224
+ const lastRequestId = normalizeOptionalString(record.last_request_id);
2225
+ if (lastRequestId === null) return null;
2226
+ return {
2227
+ schema: SESSION_RECORD_SCHEMA,
2228
+ acpxRecordId: record.acpx_record_id,
2229
+ acpSessionId: record.acp_session_id,
2230
+ agentSessionId: normalizeRuntimeSessionId(record.agent_session_id),
2231
+ agentCommand: record.agent_command,
2232
+ cwd: record.cwd,
2233
+ name,
2234
+ createdAt: record.created_at,
2235
+ lastUsedAt: record.last_used_at,
2236
+ lastSeq: record.last_seq,
2237
+ lastRequestId,
2238
+ eventLog,
2239
+ closed,
2240
+ closedAt,
2241
+ pid,
2242
+ agentStartedAt,
2243
+ lastPromptAt,
2244
+ lastAgentExitCode,
2245
+ lastAgentExitSignal,
2246
+ lastAgentExitAt,
2247
+ lastAgentDisconnectReason,
2248
+ protocolVersion: typeof record.protocol_version === "number" ? record.protocol_version : void 0,
2249
+ agentCapabilities: asRecord$1(record.agent_capabilities),
2250
+ title: conversation.title,
2251
+ messages: conversation.messages,
2252
+ updated_at: conversation.updated_at,
2253
+ cumulative_token_usage: conversation.cumulative_token_usage,
2254
+ request_token_usage: conversation.request_token_usage,
2255
+ acpx: parseAcpxState(record.acpx)
2256
+ };
2257
+ }
2258
+
2259
+ //#endregion
2260
+ //#region src/session-persistence/index.ts
2261
+ const SESSION_INDEX_SCHEMA = "acpx.session-index.v1";
2262
+ function asRecord(value) {
2263
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2264
+ return value;
2265
+ }
2266
+ function parseIndexEntry(raw) {
2267
+ const record = asRecord(raw);
2268
+ if (!record) return;
2269
+ if (typeof record.file !== "string" || typeof record.acpxRecordId !== "string" || typeof record.acpSessionId !== "string" || typeof record.agentCommand !== "string" || typeof record.cwd !== "string" || typeof record.lastUsedAt !== "string" || typeof record.closed !== "boolean") return;
2270
+ if (record.name !== void 0 && typeof record.name !== "string") return;
2271
+ return {
2272
+ file: record.file,
2273
+ acpxRecordId: record.acpxRecordId,
2274
+ acpSessionId: record.acpSessionId,
2275
+ agentCommand: record.agentCommand,
2276
+ cwd: record.cwd,
2277
+ name: record.name,
2278
+ closed: record.closed,
2279
+ lastUsedAt: record.lastUsedAt
2280
+ };
2281
+ }
2282
+ function sessionIndexPath(sessionDir) {
2283
+ return path.join(sessionDir, "index.json");
2284
+ }
2285
+ function toSessionIndexEntry(record, fileName) {
2286
+ return {
2287
+ file: fileName,
2288
+ acpxRecordId: record.acpxRecordId,
2289
+ acpSessionId: record.acpSessionId,
2290
+ agentCommand: record.agentCommand,
2291
+ cwd: record.cwd,
2292
+ name: record.name,
2293
+ closed: record.closed === true,
2294
+ lastUsedAt: record.lastUsedAt
2295
+ };
2296
+ }
2297
+ async function readSessionIndex(sessionDir) {
2298
+ const filePath = sessionIndexPath(sessionDir);
2299
+ try {
2300
+ const payload = await fs$1.readFile(filePath, "utf8");
2301
+ const record = asRecord(JSON.parse(payload));
2302
+ if (!record || record.schema !== SESSION_INDEX_SCHEMA || !Array.isArray(record.files)) return;
2303
+ const files = record.files.filter((entry) => typeof entry === "string");
2304
+ if (files.length !== record.files.length || !Array.isArray(record.entries)) return;
2305
+ const entries = record.entries.map((entry) => parseIndexEntry(entry)).filter((entry) => Boolean(entry));
2306
+ if (entries.length !== record.entries.length) return;
2307
+ return {
2308
+ schema: SESSION_INDEX_SCHEMA,
2309
+ files,
2310
+ entries
2311
+ };
2312
+ } catch {
2313
+ return;
2314
+ }
2315
+ }
2316
+ async function writeSessionIndex(sessionDir, index) {
2317
+ const filePath = sessionIndexPath(sessionDir);
2318
+ const tempFile = `${filePath}.${process.pid}.${Date.now()}.tmp`;
2319
+ const payload = JSON.stringify({
2320
+ schema: SESSION_INDEX_SCHEMA,
2321
+ files: [...index.files].toSorted(),
2322
+ entries: [...index.entries].toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt))
2323
+ }, null, 2);
2324
+ await fs$1.writeFile(tempFile, `${payload}\n`, "utf8");
2325
+ await fs$1.rename(tempFile, filePath);
2326
+ }
2327
+ async function rebuildSessionIndex(sessionDir) {
2328
+ const files = (await fs$1.readdir(sessionDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json").map((entry) => entry.name).toSorted();
2329
+ const indexEntries = [];
2330
+ for (const file of files) try {
2331
+ const payload = await fs$1.readFile(path.join(sessionDir, file), "utf8");
2332
+ const parsed = parseSessionRecord(JSON.parse(payload));
2333
+ if (!parsed) continue;
2334
+ indexEntries.push(toSessionIndexEntry(parsed, file));
2335
+ } catch {}
2336
+ const index = {
2337
+ schema: SESSION_INDEX_SCHEMA,
2338
+ files,
2339
+ entries: indexEntries
2340
+ };
2341
+ await writeSessionIndex(sessionDir, index);
2342
+ return index;
2343
+ }
2344
+ async function loadOrRebuildSessionIndex(sessionDir) {
2345
+ const files = (await fs$1.readdir(sessionDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && entry.name !== "index.json").map((entry) => entry.name).toSorted();
2346
+ const existing = await readSessionIndex(sessionDir);
2347
+ if (existing && existing.files.length === files.length && existing.files.every((file, index) => file === files[index])) return existing;
2348
+ return await rebuildSessionIndex(sessionDir);
2349
+ }
2350
+
2351
+ //#endregion
2352
+ //#region src/session-persistence/repository.ts
2353
+ const DEFAULT_HISTORY_LIMIT = 20;
2354
+ function sessionFilePath(acpxRecordId) {
2355
+ const safeId = encodeURIComponent(acpxRecordId);
2356
+ return path.join(sessionBaseDir(), `${safeId}.json`);
2357
+ }
2358
+ function sessionBaseDir() {
2359
+ return path.join(os.homedir(), ".acpx", "sessions");
2360
+ }
2361
+ async function ensureSessionDir$1() {
2362
+ await fs$1.mkdir(sessionBaseDir(), { recursive: true });
2363
+ }
2364
+ async function loadRecordFromIndexEntry(entry) {
2365
+ try {
2366
+ const payload = await fs$1.readFile(path.join(sessionBaseDir(), entry.file), "utf8");
2367
+ return parseSessionRecord(JSON.parse(payload)) ?? void 0;
2368
+ } catch {
2369
+ return;
2370
+ }
2371
+ }
2372
+ async function loadSessionIndexEntries() {
2373
+ await ensureSessionDir$1();
2374
+ return (await measurePerf("session.index_load", async () => {
2375
+ return await loadOrRebuildSessionIndex(sessionBaseDir());
2376
+ })).entries;
2377
+ }
2378
+ function matchesSessionEntry(session, normalizedCwd, normalizedName, includeClosed = false) {
2379
+ if (session.cwd !== normalizedCwd) return false;
2380
+ if (!includeClosed && session.closed) return false;
2381
+ if (normalizedName == null) return session.name == null;
2382
+ return session.name === normalizedName;
2383
+ }
2384
+ async function writeSessionRecord(record) {
2385
+ await measurePerf("session.write_record", async () => {
2386
+ await ensureSessionDir$1();
2387
+ const persisted = serializeSessionRecordForDisk(record);
2388
+ assertPersistedKeyPolicy(persisted);
2389
+ const file = sessionFilePath(record.acpxRecordId);
2390
+ const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
2391
+ const payload = JSON.stringify(persisted, null, 2);
2392
+ await fs$1.writeFile(tempFile, `${payload}\n`, "utf8");
2393
+ await fs$1.rename(tempFile, file);
2394
+ const sessionDir = sessionBaseDir();
2395
+ const index = await loadOrRebuildSessionIndex(sessionDir);
2396
+ const fileName = path.basename(file);
2397
+ const entries = index.entries.filter((entry) => entry.file !== fileName);
2398
+ entries.push(toSessionIndexEntry(record, fileName));
2399
+ await writeSessionIndex(sessionDir, {
2400
+ files: [...new Set([...index.files.filter((entry) => entry !== fileName), fileName])],
2401
+ entries
2402
+ });
2403
+ });
2404
+ }
2405
+ async function resolveSessionRecord(sessionId) {
2406
+ await ensureSessionDir$1();
2407
+ const directPath = sessionFilePath(sessionId);
2408
+ try {
2409
+ const directPayload = await measurePerf("session.resolve_direct", async () => {
2410
+ return await fs$1.readFile(directPath, "utf8");
2411
+ });
2412
+ const directRecord = parseSessionRecord(JSON.parse(directPayload));
2413
+ if (directRecord) return directRecord;
2414
+ } catch {}
2415
+ const entries = await loadSessionIndexEntries();
2416
+ const exactEntries = entries.filter((entry) => entry.acpxRecordId === sessionId || entry.acpSessionId === sessionId);
2417
+ const exactRecords = (await Promise.all(exactEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
2418
+ if (exactRecords.length === 1) return exactRecords[0];
2419
+ if (exactRecords.length > 1) throw new SessionResolutionError(`Multiple sessions match id: ${sessionId}`);
2420
+ const suffixEntries = entries.filter((entry) => entry.acpxRecordId.endsWith(sessionId) || entry.acpSessionId.endsWith(sessionId));
2421
+ const suffixRecords = (await Promise.all(suffixEntries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry));
2422
+ if (suffixRecords.length === 1) return suffixRecords[0];
2423
+ if (suffixRecords.length > 1) throw new SessionResolutionError(`Session id is ambiguous: ${sessionId}`);
2424
+ incrementPerfCounter("session.resolve_miss");
2425
+ throw new SessionNotFoundError(sessionId);
2426
+ }
2427
+ function hasGitDirectory(dir) {
2428
+ const gitPath = path.join(dir, ".git");
2429
+ try {
2430
+ return statSync(gitPath).isDirectory();
2431
+ } catch {
2432
+ return false;
2433
+ }
2434
+ }
2435
+ function isWithinBoundary(boundary, target) {
2436
+ const relative = path.relative(boundary, target);
2437
+ return relative.length === 0 || !relative.startsWith("..") && !path.isAbsolute(relative);
2438
+ }
2439
+ function absolutePath(value) {
2440
+ return path.resolve(value);
2441
+ }
2442
+ function findGitRepositoryRoot(startDir) {
2443
+ let current = absolutePath(startDir);
2444
+ const root = path.parse(current).root;
2445
+ for (;;) {
2446
+ if (hasGitDirectory(current)) return current;
2447
+ if (current === root) return;
2448
+ const parent = path.dirname(current);
2449
+ if (parent === current) return;
2450
+ current = parent;
2451
+ }
2452
+ }
2453
+ function normalizeName(value) {
2454
+ if (value == null) return;
2455
+ const trimmed = value.trim();
2456
+ return trimmed.length > 0 ? trimmed : void 0;
2457
+ }
2458
+ function isoNow() {
2459
+ return (/* @__PURE__ */ new Date()).toISOString();
2460
+ }
2461
+ async function listSessions() {
2462
+ await ensureSessionDir$1();
2463
+ const entries = await loadSessionIndexEntries();
2464
+ const records = [];
2465
+ for (const entry of entries) {
2466
+ const parsed = await loadRecordFromIndexEntry(entry);
2467
+ if (parsed) records.push(parsed);
2468
+ }
2469
+ records.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
2470
+ return records;
2471
+ }
2472
+ async function listSessionsForAgent(agentCommand) {
2473
+ const entries = (await loadSessionIndexEntries()).filter((session) => session.agentCommand === agentCommand);
2474
+ return (await Promise.all(entries.map((entry) => loadRecordFromIndexEntry(entry)))).filter((entry) => Boolean(entry)).toSorted((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
2475
+ }
2476
+ async function findSession(options) {
2477
+ const normalizedCwd = absolutePath(options.cwd);
2478
+ const normalizedName = normalizeName(options.name);
2479
+ const match = (await loadSessionIndexEntries()).find((session) => session.agentCommand === options.agentCommand && matchesSessionEntry(session, normalizedCwd, normalizedName, options.includeClosed));
2480
+ if (!match) return;
2481
+ return await loadRecordFromIndexEntry(match);
2482
+ }
2483
+ async function findSessionByDirectoryWalk(options) {
2484
+ const normalizedName = normalizeName(options.name);
2485
+ const normalizedStart = absolutePath(options.cwd);
2486
+ const normalizedBoundary = absolutePath(options.boundary ?? normalizedStart);
2487
+ const walkBoundary = isWithinBoundary(normalizedBoundary, normalizedStart) ? normalizedBoundary : normalizedStart;
2488
+ const sessions = (await loadSessionIndexEntries()).filter((session) => session.agentCommand === options.agentCommand);
2489
+ let current = normalizedStart;
2490
+ const walkRoot = path.parse(current).root;
2491
+ for (;;) {
2492
+ const match = sessions.find((session) => matchesSessionEntry(session, current, normalizedName));
2493
+ if (match) return await loadRecordFromIndexEntry(match);
2494
+ if (current === walkBoundary || current === walkRoot) return;
2495
+ const parent = path.dirname(current);
2496
+ if (parent === current) return;
2497
+ current = parent;
2498
+ if (!isWithinBoundary(walkBoundary, current)) return;
2499
+ }
2500
+ }
2501
+
2502
+ //#endregion
2503
+ //#region src/session-events.ts
2504
+ const LOCK_RETRY_MS = 15;
2505
+ const EVENT_LOCK_STALE_MS = 15e3;
2506
+ async function ensureSessionDir() {
2507
+ await fs$1.mkdir(sessionBaseDir$1(), { recursive: true });
2508
+ }
2509
+ async function pathExists(filePath) {
2510
+ try {
2511
+ await fs$1.access(filePath);
2512
+ return true;
2513
+ } catch {
2514
+ return false;
2515
+ }
2516
+ }
2517
+ async function statSize(filePath) {
2518
+ try {
2519
+ return (await fs$1.stat(filePath)).size;
2520
+ } catch {
2521
+ return 0;
2522
+ }
2523
+ }
2524
+ async function countExistingSegments(sessionId, maxSegments) {
2525
+ let count = 0;
2526
+ for (let segment = 1; segment <= maxSegments; segment += 1) if (await pathExists(sessionEventSegmentPath(sessionId, segment))) count += 1;
2527
+ if (await pathExists(sessionEventActivePath(sessionId))) count += 1;
2528
+ return count;
2529
+ }
2530
+ async function rotateSegments(sessionId, maxSegments) {
2531
+ const active = sessionEventActivePath(sessionId);
2532
+ const overflow = sessionEventSegmentPath(sessionId, maxSegments);
2533
+ await fs$1.unlink(overflow).catch((error) => {
2534
+ if (error.code !== "ENOENT") throw error;
2535
+ });
2536
+ for (let segment = maxSegments - 1; segment >= 1; segment -= 1) {
2537
+ const from = sessionEventSegmentPath(sessionId, segment);
2538
+ const to = sessionEventSegmentPath(sessionId, segment + 1);
2539
+ if (!await pathExists(from)) continue;
2540
+ await fs$1.rename(from, to);
2541
+ }
2542
+ if (await pathExists(active)) await fs$1.rename(active, sessionEventSegmentPath(sessionId, 1));
2543
+ }
2544
+ function parseEventLockPayload(raw) {
2545
+ try {
2546
+ const parsed = JSON.parse(raw);
2547
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
2548
+ const record = parsed;
2549
+ return {
2550
+ pid: typeof record.pid === "number" ? record.pid : void 0,
2551
+ created_at: typeof record.created_at === "string" ? record.created_at : void 0
2552
+ };
2553
+ } catch {
2554
+ return {};
2555
+ }
2556
+ }
2557
+ async function removeStaleEventLock(lockPath) {
2558
+ try {
2559
+ const parsed = parseEventLockPayload(await fs$1.readFile(lockPath, "utf8"));
2560
+ const createdAtMs = parsed.created_at ? Date.parse(parsed.created_at) : NaN;
2561
+ const lockAgeMs = Number.isFinite(createdAtMs) ? Date.now() - createdAtMs : Number.POSITIVE_INFINITY;
2562
+ if (isProcessAlive(parsed.pid) && lockAgeMs <= EVENT_LOCK_STALE_MS) return false;
2563
+ await fs$1.unlink(lockPath);
2564
+ incrementPerfCounter("session.events.stale_lock_recovered");
2565
+ return true;
2566
+ } catch (error) {
2567
+ if (error.code === "ENOENT") return true;
2568
+ return false;
2569
+ }
2570
+ }
2571
+ async function acquireEventsLock(sessionId) {
2572
+ await ensureSessionDir();
2573
+ const lockPath = sessionEventLockPath(sessionId);
2574
+ const payload = JSON.stringify({
2575
+ pid: process.pid,
2576
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
2577
+ }, null, 2);
2578
+ for (;;) try {
2579
+ await fs$1.writeFile(lockPath, `${payload}\n`, {
2580
+ encoding: "utf8",
2581
+ flag: "wx"
2582
+ });
2583
+ return { filePath: lockPath };
2584
+ } catch (error) {
2585
+ if (error.code !== "EEXIST") throw error;
2586
+ if (await removeStaleEventLock(lockPath)) continue;
2587
+ await new Promise((resolve) => {
2588
+ setTimeout(resolve, LOCK_RETRY_MS);
2589
+ });
2590
+ }
2591
+ }
2592
+ async function releaseEventsLock(lock) {
2593
+ await fs$1.unlink(lock.filePath).catch((error) => {
2594
+ if (error.code !== "ENOENT") throw error;
2595
+ });
2596
+ }
2597
+ var SessionEventWriter = class SessionEventWriter {
2598
+ record;
2599
+ lock;
2600
+ maxSegmentBytes;
2601
+ maxSegments;
2602
+ activePath;
2603
+ activeSizeBytes;
2604
+ segmentCount;
2605
+ closed = false;
2606
+ constructor(record, lock, options, state) {
2607
+ this.record = record;
2608
+ this.lock = lock;
2609
+ this.maxSegmentBytes = options.maxSegmentBytes;
2610
+ this.maxSegments = options.maxSegments;
2611
+ this.activePath = state.activePath;
2612
+ this.activeSizeBytes = state.activeSizeBytes;
2613
+ this.segmentCount = state.segmentCount;
2614
+ }
2615
+ static async open(record, options = {}) {
2616
+ const lock = await acquireEventsLock(record.acpxRecordId);
2617
+ const maxSegmentBytes = options.maxSegmentBytes ?? record.eventLog.max_segment_bytes ?? DEFAULT_EVENT_SEGMENT_MAX_BYTES;
2618
+ const maxSegments = options.maxSegments ?? record.eventLog.max_segments ?? DEFAULT_EVENT_MAX_SEGMENTS;
2619
+ const activePath = sessionEventActivePath(record.acpxRecordId);
2620
+ const activeSizeBytes = await statSize(activePath);
2621
+ const segmentCount = Number.isInteger(record.eventLog.segment_count) && record.eventLog.segment_count > 0 ? record.eventLog.segment_count : await countExistingSegments(record.acpxRecordId, maxSegments) || 1;
2622
+ return new SessionEventWriter(record, lock, {
2623
+ maxSegmentBytes,
2624
+ maxSegments
2625
+ }, {
2626
+ activePath,
2627
+ activeSizeBytes,
2628
+ segmentCount
2629
+ });
2630
+ }
2631
+ getRecord() {
2632
+ return this.record;
2633
+ }
2634
+ async appendMessage(message, options = {}) {
2635
+ await this.appendMessages([message], options);
2636
+ }
2637
+ async appendMessages(messages, options = {}) {
2638
+ if (this.closed) throw new Error("SessionEventWriter is closed");
2639
+ if (messages.length === 0) return;
2640
+ await ensureSessionDir();
2641
+ await measurePerf("session.events.append_batch", async () => {
2642
+ for (const message of messages) {
2643
+ if (!isAcpJsonRpcMessage(message)) throw new Error("Attempted to persist invalid ACP JSON-RPC payload");
2644
+ const line = `${JSON.stringify(message)}\n`;
2645
+ const lineBytes = Buffer.byteLength(line);
2646
+ if (this.activeSizeBytes > 0 && this.activeSizeBytes + lineBytes > this.maxSegmentBytes) {
2647
+ await rotateSegments(this.record.acpxRecordId, this.maxSegments);
2648
+ this.activePath = sessionEventActivePath(this.record.acpxRecordId);
2649
+ this.activeSizeBytes = 0;
2650
+ this.segmentCount = Math.min(this.segmentCount + 1, this.maxSegments);
2651
+ incrementPerfCounter("session.events.rotate");
2652
+ }
2653
+ await fs$1.appendFile(this.activePath, line, "utf8");
2654
+ this.activeSizeBytes += lineBytes;
2655
+ this.record.lastSeq += 1;
2656
+ if (Object.hasOwn(message, "id")) {
2657
+ const id = message.id;
2658
+ if (typeof id === "string" || typeof id === "number") this.record.lastRequestId = String(id);
2659
+ }
2660
+ const writeTs = (/* @__PURE__ */ new Date()).toISOString();
2661
+ this.record.lastUsedAt = writeTs;
2662
+ this.record.eventLog = {
2663
+ active_path: this.activePath,
2664
+ segment_count: this.segmentCount,
2665
+ max_segment_bytes: this.maxSegmentBytes,
2666
+ max_segments: this.maxSegments,
2667
+ last_write_at: writeTs,
2668
+ last_write_error: null
2669
+ };
2670
+ }
2671
+ });
2672
+ if (options.checkpoint === true) await writeSessionRecord(this.record);
2673
+ }
2674
+ async checkpoint() {
2675
+ if (this.closed) throw new Error("SessionEventWriter is closed");
2676
+ await writeSessionRecord(this.record);
2677
+ }
2678
+ async close(options = {}) {
2679
+ if (this.closed) return;
2680
+ try {
2681
+ if (options.checkpoint !== false) await writeSessionRecord(this.record);
2682
+ } finally {
2683
+ this.closed = true;
2684
+ await releaseEventsLock(this.lock);
2685
+ }
2686
+ }
2687
+ };
2688
+
2689
+ //#endregion
2690
+ //#region src/queue-owner-turn-controller.ts
2691
+ var QueueOwnerTurnController = class {
2692
+ options;
2693
+ state = "idle";
2694
+ pendingCancel = false;
2695
+ activeController;
2696
+ constructor(options) {
2697
+ this.options = options;
2698
+ }
2699
+ get lifecycleState() {
2700
+ return this.state;
2701
+ }
2702
+ get hasPendingCancel() {
2703
+ return this.pendingCancel;
2704
+ }
2705
+ beginTurn() {
2706
+ this.state = "starting";
2707
+ this.pendingCancel = false;
2708
+ }
2709
+ markPromptActive() {
2710
+ if (this.state === "starting" || this.state === "active") this.state = "active";
2711
+ }
2712
+ endTurn() {
2713
+ this.state = "idle";
2714
+ this.pendingCancel = false;
2715
+ }
2716
+ beginClosing() {
2717
+ this.state = "closing";
2718
+ this.pendingCancel = false;
2719
+ this.activeController = void 0;
2720
+ }
2721
+ setActiveController(controller) {
2722
+ this.activeController = controller;
2723
+ }
2724
+ clearActiveController() {
2725
+ this.activeController = void 0;
2726
+ }
2727
+ assertCanHandleControlRequest() {
2728
+ if (this.state === "closing") throw new QueueConnectionError("Queue owner is closing", {
2729
+ detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
2730
+ origin: "queue",
2731
+ retryable: true
2732
+ });
2733
+ }
2734
+ async requestCancel() {
2735
+ const activeController = this.activeController;
2736
+ if (activeController?.hasActivePrompt()) {
2737
+ const cancelled = await activeController.requestCancelActivePrompt();
2738
+ if (cancelled) this.pendingCancel = false;
2739
+ return cancelled;
2740
+ }
2741
+ if (this.state === "starting" || this.state === "active") {
2742
+ this.pendingCancel = true;
2743
+ return true;
2744
+ }
2745
+ return false;
2746
+ }
2747
+ async applyPendingCancel() {
2748
+ const activeController = this.activeController;
2749
+ if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) return false;
2750
+ const cancelled = await activeController.requestCancelActivePrompt();
2751
+ if (cancelled) this.pendingCancel = false;
2752
+ return cancelled;
2753
+ }
2754
+ async setSessionMode(modeId, timeoutMs) {
2755
+ this.assertCanHandleControlRequest();
2756
+ const activeController = this.activeController;
2757
+ if (activeController) {
2758
+ await this.options.withTimeout(async () => await activeController.setSessionMode(modeId), timeoutMs);
2759
+ return;
2760
+ }
2761
+ await this.options.setSessionModeFallback(modeId, timeoutMs);
2762
+ }
2763
+ async setSessionConfigOption(configId, value, timeoutMs) {
2764
+ this.assertCanHandleControlRequest();
2765
+ const activeController = this.activeController;
2766
+ if (activeController) return await this.options.withTimeout(async () => await activeController.setSessionConfigOption(configId, value), timeoutMs);
2767
+ return await this.options.setSessionConfigOptionFallback(configId, value, timeoutMs);
2768
+ }
2769
+ };
2770
+
2771
+ //#endregion
2772
+ //#region src/session-mode-preference.ts
2773
+ function ensureAcpxState(state) {
2774
+ return state ?? {};
2775
+ }
2776
+ function normalizeModeId(modeId) {
2777
+ if (typeof modeId !== "string") return;
2778
+ const trimmed = modeId.trim();
2779
+ return trimmed.length > 0 ? trimmed : void 0;
2780
+ }
2781
+ function getDesiredModeId(state) {
2782
+ return normalizeModeId(state?.desired_mode_id);
2783
+ }
2784
+ function setDesiredModeId(record, modeId) {
2785
+ const acpx = ensureAcpxState(record.acpx);
2786
+ const normalized = normalizeModeId(modeId);
2787
+ if (normalized) acpx.desired_mode_id = normalized;
2788
+ else delete acpx.desired_mode_id;
2789
+ record.acpx = acpx;
2790
+ }
2791
+
2792
+ //#endregion
2793
+ //#region src/session-runtime/lifecycle.ts
2794
+ function applyLifecycleSnapshotToRecord(record, snapshot) {
2795
+ record.pid = snapshot.pid;
2796
+ record.agentStartedAt = snapshot.startedAt;
2797
+ if (snapshot.lastExit) {
2798
+ record.lastAgentExitCode = snapshot.lastExit.exitCode;
2799
+ record.lastAgentExitSignal = snapshot.lastExit.signal;
2800
+ record.lastAgentExitAt = snapshot.lastExit.exitedAt;
2801
+ record.lastAgentDisconnectReason = snapshot.lastExit.reason;
2802
+ return;
2803
+ }
2804
+ record.lastAgentExitCode = void 0;
2805
+ record.lastAgentExitSignal = void 0;
2806
+ record.lastAgentExitAt = void 0;
2807
+ record.lastAgentDisconnectReason = void 0;
2808
+ }
2809
+ function reconcileAgentSessionId(record, agentSessionId) {
2810
+ const normalized = normalizeRuntimeSessionId(agentSessionId);
2811
+ if (!normalized) return;
2812
+ record.agentSessionId = normalized;
2813
+ }
2814
+ function sessionHasAgentMessages(record) {
2815
+ return record.messages.some((message) => typeof message === "object" && message !== null && "Agent" in message);
2816
+ }
2817
+ function applyConversation(record, conversation) {
2818
+ record.title = conversation.title;
2819
+ record.messages = conversation.messages;
2820
+ record.updated_at = conversation.updated_at;
2821
+ record.cumulative_token_usage = conversation.cumulative_token_usage;
2822
+ record.request_token_usage = conversation.request_token_usage;
2823
+ }
2824
+
2825
+ //#endregion
2826
+ //#region src/session-runtime/connect-load.ts
2827
+ function shouldFallbackToNewSession(error, record) {
2828
+ if (error instanceof TimeoutError || error instanceof InterruptedError) return false;
2829
+ if (isAcpResourceNotFoundError(error)) return true;
2830
+ if (!sessionHasAgentMessages(record)) {
2831
+ if (isAcpQueryClosedBeforeResponseError(error)) return true;
2832
+ if (extractAcpError(error)?.code === -32603) return true;
2833
+ }
2834
+ return false;
2835
+ }
2836
+ async function connectAndLoadSession(options) {
2837
+ const record = options.record;
2838
+ const client = options.client;
2839
+ const originalSessionId = record.acpSessionId;
2840
+ const originalAgentSessionId = record.agentSessionId;
2841
+ const desiredModeId = getDesiredModeId(record.acpx);
2842
+ const storedProcessAlive = isProcessAlive(record.pid);
2843
+ const shouldReconnect = Boolean(record.pid) && !storedProcessAlive;
2844
+ if (options.verbose) {
2845
+ if (storedProcessAlive) process.stderr.write(`[acpx] saved session pid ${record.pid} is running; reconnecting with loadSession\n`);
2846
+ else if (shouldReconnect) process.stderr.write(`[acpx] saved session pid ${record.pid} is dead; respawning agent and attempting session/load\n`);
2847
+ }
2848
+ const reusingLoadedSession = client.hasReusableSession(record.acpSessionId);
2849
+ if (reusingLoadedSession) incrementPerfCounter("runtime.connect_and_load.reused_session");
2850
+ else await withTimeout(client.start(), options.timeoutMs);
2851
+ options.onClientAvailable?.(options.activeController);
2852
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
2853
+ record.closed = false;
2854
+ record.closedAt = void 0;
2855
+ options.onConnectedRecord?.(record);
2856
+ let resumed = false;
2857
+ let loadError;
2858
+ let sessionId = record.acpSessionId;
2859
+ let createdFreshSession = false;
2860
+ let pendingAgentSessionId = record.agentSessionId;
2861
+ if (reusingLoadedSession) resumed = true;
2862
+ else if (client.supportsLoadSession()) try {
2863
+ reconcileAgentSessionId(record, (await withTimeout(client.loadSessionWithOptions(record.acpSessionId, record.cwd, { suppressReplayUpdates: true }), options.timeoutMs)).agentSessionId);
2864
+ resumed = true;
2865
+ } catch (error) {
2866
+ loadError = formatErrorMessage(error);
2867
+ if (!shouldFallbackToNewSession(error, record)) throw error;
2868
+ const createdSession = await withTimeout(client.createSession(record.cwd), options.timeoutMs);
2869
+ sessionId = createdSession.sessionId;
2870
+ createdFreshSession = true;
2871
+ pendingAgentSessionId = createdSession.agentSessionId;
2872
+ }
2873
+ else {
2874
+ const createdSession = await withTimeout(client.createSession(record.cwd), options.timeoutMs);
2875
+ sessionId = createdSession.sessionId;
2876
+ createdFreshSession = true;
2877
+ pendingAgentSessionId = createdSession.agentSessionId;
2878
+ }
2879
+ if (createdFreshSession && desiredModeId) try {
2880
+ await withTimeout(client.setSessionMode(sessionId, desiredModeId), options.timeoutMs);
2881
+ if (options.verbose) process.stderr.write(`[acpx] replayed desired mode ${desiredModeId} on fresh ACP session ${sessionId} (previous ${originalSessionId})\n`);
2882
+ } catch (error) {
2883
+ const message = `Failed to replay saved session mode ${desiredModeId} on fresh ACP session ${sessionId}: ` + formatErrorMessage(error);
2884
+ record.acpSessionId = originalSessionId;
2885
+ record.agentSessionId = originalAgentSessionId;
2886
+ if (options.verbose) process.stderr.write(`[acpx] ${message}\n`);
2887
+ throw new SessionModeReplayError(message, {
2888
+ cause: error instanceof Error ? error : void 0,
2889
+ retryable: true
2890
+ });
2891
+ }
2892
+ if (createdFreshSession) {
2893
+ record.acpSessionId = sessionId;
2894
+ reconcileAgentSessionId(record, pendingAgentSessionId);
2895
+ }
2896
+ options.onSessionIdResolved?.(sessionId);
2897
+ return {
2898
+ sessionId,
2899
+ agentSessionId: record.agentSessionId,
2900
+ resumed,
2901
+ loadError
2902
+ };
2903
+ }
2904
+
2905
+ //#endregion
2906
+ //#region src/session-runtime/prompt-runner.ts
2907
+ async function withConnectedSession(options) {
2908
+ const record = await resolveSessionRecord(options.sessionRecordId);
2909
+ const client = new AcpClient({
2910
+ agentCommand: record.agentCommand,
2911
+ cwd: absolutePath(record.cwd),
2912
+ mcpServers: options.mcpServers,
2913
+ permissionMode: options.permissionMode ?? "approve-reads",
2914
+ nonInteractivePermissions: options.nonInteractivePermissions,
2915
+ authCredentials: options.authCredentials,
2916
+ authPolicy: options.authPolicy,
2917
+ verbose: options.verbose
2918
+ });
2919
+ let activeSessionIdForControl = record.acpSessionId;
2920
+ let notifiedClientAvailable = false;
2921
+ const activeController = {
2922
+ hasActivePrompt: () => client.hasActivePrompt(),
2923
+ requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
2924
+ setSessionMode: async (modeId) => {
2925
+ await client.setSessionMode(activeSessionIdForControl, modeId);
2926
+ },
2927
+ setSessionConfigOption: async (configId, value) => {
2928
+ return await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
2929
+ }
2930
+ };
2931
+ try {
2932
+ return await withInterrupt(async () => {
2933
+ const { sessionId: activeSessionId, resumed, loadError } = await connectAndLoadSession({
2934
+ client,
2935
+ record,
2936
+ timeoutMs: options.timeoutMs,
2937
+ verbose: options.verbose,
2938
+ activeController,
2939
+ onClientAvailable: (controller) => {
2940
+ options.onClientAvailable?.(controller);
2941
+ notifiedClientAvailable = true;
2942
+ },
2943
+ onSessionIdResolved: (sessionId) => {
2944
+ activeSessionIdForControl = sessionId;
2945
+ }
2946
+ });
2947
+ const value = await options.run(client, activeSessionId, record);
2948
+ record.lastUsedAt = isoNow();
2949
+ record.closed = false;
2950
+ record.closedAt = void 0;
2951
+ record.protocolVersion = client.initializeResult?.protocolVersion;
2952
+ record.agentCapabilities = client.initializeResult?.agentCapabilities;
2953
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
2954
+ await writeSessionRecord(record);
2955
+ return {
2956
+ value,
2957
+ record,
2958
+ resumed,
2959
+ loadError
2960
+ };
2961
+ }, async () => {
2962
+ await client.cancelActivePrompt(2500);
2963
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
2964
+ record.lastUsedAt = isoNow();
2965
+ await writeSessionRecord(record).catch(() => {});
2966
+ await client.close();
2967
+ });
2968
+ } finally {
2969
+ if (notifiedClientAvailable) options.onClientClosed?.();
2970
+ await client.close();
2971
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
2972
+ await writeSessionRecord(record).catch(() => {});
2973
+ }
2974
+ }
2975
+ async function runSessionSetModeDirect(options) {
2976
+ const result = await withConnectedSession({
2977
+ sessionRecordId: options.sessionRecordId,
2978
+ mcpServers: options.mcpServers,
2979
+ nonInteractivePermissions: options.nonInteractivePermissions,
2980
+ authCredentials: options.authCredentials,
2981
+ authPolicy: options.authPolicy,
2982
+ timeoutMs: options.timeoutMs,
2983
+ verbose: options.verbose,
2984
+ onClientAvailable: options.onClientAvailable,
2985
+ onClientClosed: options.onClientClosed,
2986
+ run: async (client, sessionId, record) => {
2987
+ await withTimeout(client.setSessionMode(sessionId, options.modeId), options.timeoutMs);
2988
+ setDesiredModeId(record, options.modeId);
2989
+ }
2990
+ });
2991
+ return {
2992
+ record: result.record,
2993
+ resumed: result.resumed,
2994
+ loadError: result.loadError
2995
+ };
2996
+ }
2997
+ async function runSessionSetConfigOptionDirect(options) {
2998
+ const result = await withConnectedSession({
2999
+ sessionRecordId: options.sessionRecordId,
3000
+ mcpServers: options.mcpServers,
3001
+ nonInteractivePermissions: options.nonInteractivePermissions,
3002
+ authCredentials: options.authCredentials,
3003
+ authPolicy: options.authPolicy,
3004
+ timeoutMs: options.timeoutMs,
3005
+ verbose: options.verbose,
3006
+ onClientAvailable: options.onClientAvailable,
3007
+ onClientClosed: options.onClientClosed,
3008
+ run: async (client, sessionId, record) => {
3009
+ const response = await withTimeout(client.setSessionConfigOption(sessionId, options.configId, options.value), options.timeoutMs);
3010
+ if (options.configId === "mode") setDesiredModeId(record, options.value);
3011
+ return response;
3012
+ }
3013
+ });
3014
+ return {
3015
+ record: result.record,
3016
+ response: result.value,
3017
+ resumed: result.resumed,
3018
+ loadError: result.loadError
3019
+ };
3020
+ }
3021
+
3022
+ //#endregion
3023
+ //#region src/session-runtime/queue-owner-process.ts
3024
+ function resolveQueueOwnerSpawnArgs(argv = process.argv) {
3025
+ const entry = argv[1];
3026
+ if (!entry || entry.trim().length === 0) throw new Error("acpx self-spawn failed: missing CLI entry path");
3027
+ return [realpathSync(entry), "__queue-owner"];
3028
+ }
3029
+ function queueOwnerRuntimeOptionsFromSend(options) {
3030
+ return {
3031
+ sessionId: options.sessionId,
3032
+ mcpServers: options.mcpServers,
3033
+ permissionMode: options.permissionMode,
3034
+ nonInteractivePermissions: options.nonInteractivePermissions,
3035
+ authCredentials: options.authCredentials,
3036
+ authPolicy: options.authPolicy,
3037
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3038
+ verbose: options.verbose,
3039
+ ttlMs: options.ttlMs,
3040
+ maxQueueDepth: options.maxQueueDepth
3041
+ };
3042
+ }
3043
+ function buildQueueOwnerSpawnOptions(payload) {
3044
+ return {
3045
+ detached: true,
3046
+ stdio: "ignore",
3047
+ env: {
3048
+ ...process.env,
3049
+ ACPX_QUEUE_OWNER_PAYLOAD: payload
3050
+ },
3051
+ windowsHide: true
3052
+ };
3053
+ }
3054
+ function spawnQueueOwnerProcess(options) {
3055
+ const payload = JSON.stringify(options);
3056
+ spawn(process.execPath, resolveQueueOwnerSpawnArgs(), buildQueueOwnerSpawnOptions(payload)).unref();
3057
+ }
3058
+
3059
+ //#endregion
3060
+ //#region src/session-runtime.ts
3061
+ const DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
3062
+ const INTERRUPT_CANCEL_WAIT_MS = 2500;
3063
+ const QUEUE_OWNER_STARTUP_MAX_ATTEMPTS = 120;
3064
+ const QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
3065
+ function toPromptResult(stopReason, sessionId, client) {
3066
+ return {
3067
+ stopReason,
3068
+ sessionId,
3069
+ permissionStats: client.getPermissionStats()
3070
+ };
3071
+ }
3072
+ var QueueTaskOutputFormatter = class {
3073
+ requestId;
3074
+ send;
3075
+ constructor(task) {
3076
+ this.requestId = task.requestId;
3077
+ this.send = task.send;
3078
+ }
3079
+ setContext(_context) {}
3080
+ onAcpMessage(message) {
3081
+ this.send({
3082
+ type: "event",
3083
+ requestId: this.requestId,
3084
+ message
3085
+ });
3086
+ }
3087
+ onError(params) {
3088
+ this.send({
3089
+ type: "error",
3090
+ requestId: this.requestId,
3091
+ code: params.code,
3092
+ detailCode: params.detailCode,
3093
+ origin: params.origin,
3094
+ message: params.message,
3095
+ retryable: params.retryable,
3096
+ acp: params.acp
3097
+ });
3098
+ }
3099
+ flush() {}
3100
+ };
3101
+ const DISCARD_OUTPUT_FORMATTER = {
3102
+ setContext(_context) {},
3103
+ onAcpMessage() {},
3104
+ onError() {},
3105
+ flush() {}
3106
+ };
3107
+ function normalizeQueueOwnerTtlMs(ttlMs) {
3108
+ if (ttlMs == null) return DEFAULT_QUEUE_OWNER_TTL_MS;
3109
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) return DEFAULT_QUEUE_OWNER_TTL_MS;
3110
+ return Math.round(ttlMs);
3111
+ }
3112
+ async function runQueuedTask(sessionRecordId, task, options) {
3113
+ const outputFormatter = task.waitForCompletion ? new QueueTaskOutputFormatter(task) : DISCARD_OUTPUT_FORMATTER;
3114
+ try {
3115
+ const result = await runSessionPrompt({
3116
+ sessionRecordId,
3117
+ mcpServers: options.mcpServers,
3118
+ prompt: task.prompt ?? textPrompt(task.message),
3119
+ permissionMode: task.permissionMode,
3120
+ nonInteractivePermissions: task.nonInteractivePermissions ?? options.nonInteractivePermissions,
3121
+ authCredentials: options.authCredentials,
3122
+ authPolicy: options.authPolicy,
3123
+ outputFormatter,
3124
+ timeoutMs: task.timeoutMs,
3125
+ suppressSdkConsoleErrors: task.suppressSdkConsoleErrors ?? options.suppressSdkConsoleErrors,
3126
+ verbose: options.verbose,
3127
+ onClientAvailable: options.onClientAvailable,
3128
+ onClientClosed: options.onClientClosed,
3129
+ onPromptActive: options.onPromptActive,
3130
+ client: options.sharedClient
3131
+ });
3132
+ if (task.waitForCompletion) task.send({
3133
+ type: "result",
3134
+ requestId: task.requestId,
3135
+ result
3136
+ });
3137
+ } catch (error) {
3138
+ const normalizedError = normalizeOutputError(error, {
3139
+ origin: "runtime",
3140
+ detailCode: "QUEUE_RUNTIME_PROMPT_FAILED"
3141
+ });
3142
+ const alreadyEmitted = error.outputAlreadyEmitted === true;
3143
+ if (task.waitForCompletion) task.send({
3144
+ type: "error",
3145
+ requestId: task.requestId,
3146
+ code: normalizedError.code,
3147
+ detailCode: normalizedError.detailCode,
3148
+ origin: normalizedError.origin,
3149
+ message: normalizedError.message,
3150
+ retryable: normalizedError.retryable,
3151
+ acp: normalizedError.acp,
3152
+ outputAlreadyEmitted: alreadyEmitted
3153
+ });
3154
+ if (error instanceof InterruptedError) throw error;
3155
+ } finally {
3156
+ task.close();
3157
+ }
3158
+ }
3159
+ async function runSessionPrompt(options) {
3160
+ const stopTotalTimer = startPerfTimer("runtime.prompt.total");
3161
+ const output = options.outputFormatter;
3162
+ const record = await measurePerf("session.resolve_prompt_record", async () => {
3163
+ return await resolveSessionRecord(options.sessionRecordId);
3164
+ });
3165
+ const conversation = cloneSessionConversation(record);
3166
+ let acpxState = cloneSessionAcpxState(record.acpx);
3167
+ recordPromptSubmission(conversation, options.prompt, isoNow());
3168
+ output.setContext({ sessionId: record.acpxRecordId });
3169
+ const eventWriter = await measurePerf("session.events.open", async () => {
3170
+ return await SessionEventWriter.open(record);
3171
+ });
3172
+ const pendingMessages = [];
3173
+ let sawAcpMessage = false;
3174
+ let eventWriterClosed = false;
3175
+ const closeEventWriter = async (checkpoint) => {
3176
+ if (eventWriterClosed) return;
3177
+ eventWriterClosed = true;
3178
+ await eventWriter.close({ checkpoint });
3179
+ };
3180
+ const flushPendingMessages = async (checkpoint = false) => {
3181
+ if (pendingMessages.length === 0) return;
3182
+ const batch = pendingMessages.splice(0, pendingMessages.length);
3183
+ await measurePerf("session.events.flush_pending", async () => {
3184
+ await eventWriter.appendMessages(batch, { checkpoint });
3185
+ });
3186
+ };
3187
+ const ownClient = options.client == null;
3188
+ const client = options.client ?? new AcpClient({
3189
+ agentCommand: record.agentCommand,
3190
+ cwd: absolutePath(record.cwd),
3191
+ mcpServers: options.mcpServers,
3192
+ permissionMode: options.permissionMode,
3193
+ nonInteractivePermissions: options.nonInteractivePermissions,
3194
+ authCredentials: options.authCredentials,
3195
+ authPolicy: options.authPolicy,
3196
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3197
+ verbose: options.verbose
3198
+ });
3199
+ client.updateRuntimeOptions({
3200
+ permissionMode: options.permissionMode,
3201
+ nonInteractivePermissions: options.nonInteractivePermissions,
3202
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3203
+ verbose: options.verbose
3204
+ });
3205
+ client.setEventHandlers({
3206
+ onAcpMessage: (_direction, message) => {
3207
+ sawAcpMessage = true;
3208
+ pendingMessages.push(message);
3209
+ },
3210
+ onAcpOutputMessage: (_direction, message) => {
3211
+ output.onAcpMessage(message);
3212
+ },
3213
+ onSessionUpdate: (notification) => {
3214
+ acpxState = recordSessionUpdate(conversation, acpxState, notification);
3215
+ trimConversationForRuntime(conversation);
3216
+ },
3217
+ onClientOperation: (operation) => {
3218
+ acpxState = recordClientOperation(conversation, acpxState, operation);
3219
+ trimConversationForRuntime(conversation);
3220
+ }
3221
+ });
3222
+ let activeSessionIdForControl = record.acpSessionId;
3223
+ let notifiedClientAvailable = false;
3224
+ const activeController = {
3225
+ hasActivePrompt: () => client.hasActivePrompt(),
3226
+ requestCancelActivePrompt: async () => await client.requestCancelActivePrompt(),
3227
+ setSessionMode: async (modeId) => {
3228
+ await client.setSessionMode(activeSessionIdForControl, modeId);
3229
+ },
3230
+ setSessionConfigOption: async (configId, value) => {
3231
+ return await client.setSessionConfigOption(activeSessionIdForControl, configId, value);
3232
+ }
3233
+ };
3234
+ try {
3235
+ return await withInterrupt(async () => {
3236
+ const connectStartedAt = Date.now();
3237
+ const { sessionId: activeSessionId, resumed, loadError } = await measurePerf("runtime.connect_and_load", async () => await connectAndLoadSession({
3238
+ client,
3239
+ record,
3240
+ timeoutMs: options.timeoutMs,
3241
+ verbose: options.verbose,
3242
+ activeController,
3243
+ onClientAvailable: (controller) => {
3244
+ options.onClientAvailable?.(controller);
3245
+ notifiedClientAvailable = true;
3246
+ },
3247
+ onConnectedRecord: (connectedRecord) => {
3248
+ connectedRecord.lastPromptAt = isoNow();
3249
+ },
3250
+ onSessionIdResolved: (sessionId) => {
3251
+ activeSessionIdForControl = sessionId;
3252
+ }
3253
+ }));
3254
+ if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.connect_and_load", Date.now() - connectStartedAt)}\n`);
3255
+ output.setContext({ sessionId: record.acpxRecordId });
3256
+ await flushPendingMessages(false);
3257
+ let response;
3258
+ try {
3259
+ const promptStartedAt = Date.now();
3260
+ const promptPromise = client.prompt(activeSessionId, options.prompt);
3261
+ if (options.onPromptActive) try {
3262
+ await options.onPromptActive();
3263
+ } catch (error) {
3264
+ if (options.verbose) process.stderr.write("[acpx] onPromptActive hook failed: " + formatErrorMessage(error) + "\n");
3265
+ }
3266
+ response = await measurePerf("runtime.prompt.agent_turn", async () => {
3267
+ return await withTimeout(promptPromise, options.timeoutMs);
3268
+ });
3269
+ if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.agent_turn", Date.now() - promptStartedAt)}\n`);
3270
+ } catch (error) {
3271
+ const snapshot = client.getAgentLifecycleSnapshot();
3272
+ applyLifecycleSnapshotToRecord(record, snapshot);
3273
+ if (snapshot.lastExit?.unexpectedDuringPrompt && options.verbose) process.stderr.write("[acpx] agent disconnected during prompt (" + snapshot.lastExit.reason + ", exit=" + snapshot.lastExit.exitCode + ", signal=" + (snapshot.lastExit.signal ?? "none") + ")\n");
3274
+ const normalizedError = normalizeOutputError(error, { origin: "runtime" });
3275
+ await flushPendingMessages(false).catch(() => {});
3276
+ output.flush();
3277
+ record.lastUsedAt = isoNow();
3278
+ applyConversation(record, conversation);
3279
+ record.acpx = acpxState;
3280
+ const propagated = error instanceof Error ? error : new Error(formatErrorMessage(error));
3281
+ propagated.outputAlreadyEmitted = sawAcpMessage;
3282
+ propagated.normalizedOutputError = normalizedError;
3283
+ throw propagated;
3284
+ }
3285
+ await flushPendingMessages(false);
3286
+ output.flush();
3287
+ record.lastUsedAt = isoNow();
3288
+ record.closed = false;
3289
+ record.closedAt = void 0;
3290
+ record.protocolVersion = client.initializeResult?.protocolVersion;
3291
+ record.agentCapabilities = client.initializeResult?.agentCapabilities;
3292
+ applyConversation(record, conversation);
3293
+ record.acpx = acpxState;
3294
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3295
+ stopTotalTimer();
3296
+ return {
3297
+ ...toPromptResult(response.stopReason, record.acpxRecordId, client),
3298
+ record,
3299
+ resumed,
3300
+ loadError
3301
+ };
3302
+ }, async () => {
3303
+ await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3304
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3305
+ record.lastUsedAt = isoNow();
3306
+ applyConversation(record, conversation);
3307
+ record.acpx = acpxState;
3308
+ await flushPendingMessages(false).catch(() => {});
3309
+ if (ownClient) await client.close();
3310
+ });
3311
+ } finally {
3312
+ if (options.verbose) process.stderr.write(`[acpx] ${formatPerfMetric("prompt.total", stopTotalTimer())}\n`);
3313
+ else stopTotalTimer();
3314
+ if (notifiedClientAvailable) options.onClientClosed?.();
3315
+ client.clearEventHandlers();
3316
+ if (ownClient) await client.close();
3317
+ applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
3318
+ applyConversation(record, conversation);
3319
+ record.acpx = acpxState;
3320
+ await flushPendingMessages(false).catch(() => {});
3321
+ await closeEventWriter(true).catch(() => {});
3322
+ }
3323
+ }
3324
+ async function runOnce(options) {
3325
+ const output = options.outputFormatter;
3326
+ const client = new AcpClient({
3327
+ agentCommand: options.agentCommand,
3328
+ cwd: absolutePath(options.cwd),
3329
+ mcpServers: options.mcpServers,
3330
+ permissionMode: options.permissionMode,
3331
+ nonInteractivePermissions: options.nonInteractivePermissions,
3332
+ authCredentials: options.authCredentials,
3333
+ authPolicy: options.authPolicy,
3334
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3335
+ verbose: options.verbose,
3336
+ onAcpOutputMessage: (_direction, message) => output.onAcpMessage(message),
3337
+ sessionOptions: options.sessionOptions
3338
+ });
3339
+ try {
3340
+ return await withInterrupt(async () => {
3341
+ await measurePerf("runtime.exec.start", async () => {
3342
+ await withTimeout(client.start(), options.timeoutMs);
3343
+ });
3344
+ const sessionId = (await measurePerf("runtime.exec.create_session", async () => {
3345
+ return await withTimeout(client.createSession(absolutePath(options.cwd)), options.timeoutMs);
3346
+ })).sessionId;
3347
+ output.setContext({ sessionId });
3348
+ const response = await measurePerf("runtime.exec.prompt", async () => {
3349
+ return await withTimeout(client.prompt(sessionId, options.prompt), options.timeoutMs);
3350
+ });
3351
+ output.flush();
3352
+ return toPromptResult(response.stopReason, sessionId, client);
3353
+ }, async () => {
3354
+ await client.cancelActivePrompt(INTERRUPT_CANCEL_WAIT_MS);
3355
+ await client.close();
3356
+ });
3357
+ } finally {
3358
+ await client.close();
3359
+ }
3360
+ }
3361
+ async function createSession(options) {
3362
+ const client = new AcpClient({
3363
+ agentCommand: options.agentCommand,
3364
+ cwd: absolutePath(options.cwd),
3365
+ mcpServers: options.mcpServers,
3366
+ permissionMode: options.permissionMode,
3367
+ nonInteractivePermissions: options.nonInteractivePermissions,
3368
+ authCredentials: options.authCredentials,
3369
+ authPolicy: options.authPolicy,
3370
+ verbose: options.verbose,
3371
+ sessionOptions: options.sessionOptions
3372
+ });
3373
+ try {
3374
+ return await withInterrupt(async () => {
3375
+ const cwd = absolutePath(options.cwd);
3376
+ await measurePerf("runtime.session_create.start", async () => {
3377
+ await withTimeout(client.start(), options.timeoutMs);
3378
+ });
3379
+ let sessionId;
3380
+ let agentSessionId;
3381
+ if (options.resumeSessionId) {
3382
+ if (!client.supportsLoadSession()) throw new Error(`Agent command "${options.agentCommand}" does not support session/load; cannot resume session ${options.resumeSessionId}`);
3383
+ try {
3384
+ const loadedSession = await withTimeout(client.loadSession(options.resumeSessionId, cwd), options.timeoutMs);
3385
+ sessionId = options.resumeSessionId;
3386
+ agentSessionId = normalizeRuntimeSessionId(loadedSession.agentSessionId);
3387
+ } catch (error) {
3388
+ throw new Error(`Failed to resume ACP session ${options.resumeSessionId}: ${formatErrorMessage(error)}`, { cause: error });
3389
+ }
3390
+ } else {
3391
+ const createdSession = await measurePerf("runtime.session_create.create_session", async () => await withTimeout(client.createSession(cwd), options.timeoutMs));
3392
+ sessionId = createdSession.sessionId;
3393
+ agentSessionId = normalizeRuntimeSessionId(createdSession.agentSessionId);
3394
+ }
3395
+ const lifecycle = client.getAgentLifecycleSnapshot();
3396
+ const now = isoNow();
3397
+ const record = {
3398
+ schema: SESSION_RECORD_SCHEMA,
3399
+ acpxRecordId: sessionId,
3400
+ acpSessionId: sessionId,
3401
+ agentSessionId,
3402
+ agentCommand: options.agentCommand,
3403
+ cwd,
3404
+ name: normalizeName(options.name),
3405
+ createdAt: now,
3406
+ lastUsedAt: now,
3407
+ lastSeq: 0,
3408
+ lastRequestId: void 0,
3409
+ eventLog: defaultSessionEventLog(sessionId),
3410
+ closed: false,
3411
+ closedAt: void 0,
3412
+ pid: lifecycle.pid,
3413
+ agentStartedAt: lifecycle.startedAt,
3414
+ protocolVersion: client.initializeResult?.protocolVersion,
3415
+ agentCapabilities: client.initializeResult?.agentCapabilities,
3416
+ ...createSessionConversation(now),
3417
+ acpx: {}
3418
+ };
3419
+ await writeSessionRecord(record);
3420
+ return record;
3421
+ }, async () => {
3422
+ await client.close();
3423
+ });
3424
+ } finally {
3425
+ await client.close();
3426
+ }
3427
+ }
3428
+ async function ensureSession(options) {
3429
+ const cwd = absolutePath(options.cwd);
3430
+ const gitRoot = findGitRepositoryRoot(cwd);
3431
+ const walkBoundary = options.walkBoundary ?? gitRoot ?? cwd;
3432
+ const existing = await findSessionByDirectoryWalk({
3433
+ agentCommand: options.agentCommand,
3434
+ cwd,
3435
+ name: options.name,
3436
+ boundary: walkBoundary
3437
+ });
3438
+ if (existing) return {
3439
+ record: existing,
3440
+ created: false
3441
+ };
3442
+ return {
3443
+ record: await createSession({
3444
+ agentCommand: options.agentCommand,
3445
+ cwd,
3446
+ name: options.name,
3447
+ resumeSessionId: options.resumeSessionId,
3448
+ mcpServers: options.mcpServers,
3449
+ permissionMode: options.permissionMode,
3450
+ nonInteractivePermissions: options.nonInteractivePermissions,
3451
+ authCredentials: options.authCredentials,
3452
+ authPolicy: options.authPolicy,
3453
+ timeoutMs: options.timeoutMs,
3454
+ verbose: options.verbose,
3455
+ sessionOptions: options.sessionOptions
3456
+ }),
3457
+ created: true
3458
+ };
3459
+ }
3460
+ async function submitToRunningOwner(options, waitForCompletion) {
3461
+ return await trySubmitToRunningOwner({
3462
+ sessionId: options.sessionId,
3463
+ message: promptToDisplayText(options.prompt),
3464
+ prompt: options.prompt,
3465
+ permissionMode: options.permissionMode,
3466
+ nonInteractivePermissions: options.nonInteractivePermissions,
3467
+ outputFormatter: options.outputFormatter,
3468
+ errorEmissionPolicy: options.errorEmissionPolicy,
3469
+ timeoutMs: options.timeoutMs,
3470
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3471
+ waitForCompletion,
3472
+ verbose: options.verbose
3473
+ });
3474
+ }
3475
+ async function runSessionQueueOwner(options) {
3476
+ const lease = await tryAcquireQueueOwnerLease(options.sessionId);
3477
+ if (!lease) return;
3478
+ const sessionRecord = await resolveSessionRecord(options.sessionId);
3479
+ let owner;
3480
+ let heartbeatTimer;
3481
+ const sharedClient = new AcpClient({
3482
+ agentCommand: sessionRecord.agentCommand,
3483
+ cwd: absolutePath(sessionRecord.cwd),
3484
+ mcpServers: options.mcpServers,
3485
+ permissionMode: "approve-reads",
3486
+ nonInteractivePermissions: options.nonInteractivePermissions,
3487
+ authCredentials: options.authCredentials,
3488
+ authPolicy: options.authPolicy,
3489
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3490
+ verbose: options.verbose
3491
+ });
3492
+ const ttlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
3493
+ const maxQueueDepth = Math.max(1, Math.round(options.maxQueueDepth ?? 16));
3494
+ const taskPollTimeoutMs = ttlMs === 0 ? void 0 : ttlMs;
3495
+ const initialTaskPollTimeoutMs = taskPollTimeoutMs == null ? void 0 : Math.max(taskPollTimeoutMs, 1e3);
3496
+ const turnController = new QueueOwnerTurnController({
3497
+ withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
3498
+ setSessionModeFallback: async (modeId, timeoutMs) => {
3499
+ await runSessionSetModeDirect({
3500
+ sessionRecordId: options.sessionId,
3501
+ modeId,
3502
+ mcpServers: options.mcpServers,
3503
+ nonInteractivePermissions: options.nonInteractivePermissions,
3504
+ authCredentials: options.authCredentials,
3505
+ authPolicy: options.authPolicy,
3506
+ timeoutMs,
3507
+ verbose: options.verbose
3508
+ });
3509
+ },
3510
+ setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
3511
+ return (await runSessionSetConfigOptionDirect({
3512
+ sessionRecordId: options.sessionId,
3513
+ configId,
3514
+ value,
3515
+ mcpServers: options.mcpServers,
3516
+ nonInteractivePermissions: options.nonInteractivePermissions,
3517
+ authCredentials: options.authCredentials,
3518
+ authPolicy: options.authPolicy,
3519
+ timeoutMs,
3520
+ verbose: options.verbose
3521
+ })).response;
3522
+ }
3523
+ });
3524
+ const applyPendingCancel = async () => {
3525
+ return await turnController.applyPendingCancel();
3526
+ };
3527
+ const scheduleApplyPendingCancel = () => {
3528
+ applyPendingCancel().catch((error) => {
3529
+ if (options.verbose) process.stderr.write(`[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}\n`);
3530
+ });
3531
+ };
3532
+ const setActiveController = (controller) => {
3533
+ turnController.setActiveController(controller);
3534
+ scheduleApplyPendingCancel();
3535
+ };
3536
+ const clearActiveController = () => {
3537
+ turnController.clearActiveController();
3538
+ };
3539
+ const runPromptTurn = async (run) => {
3540
+ turnController.beginTurn();
3541
+ try {
3542
+ return await run();
3543
+ } finally {
3544
+ turnController.endTurn();
3545
+ }
3546
+ };
3547
+ try {
3548
+ owner = await SessionQueueOwner.start(lease, {
3549
+ cancelPrompt: async () => {
3550
+ if (!await turnController.requestCancel()) return false;
3551
+ await applyPendingCancel();
3552
+ return true;
3553
+ },
3554
+ setSessionMode: async (modeId, timeoutMs) => {
3555
+ await turnController.setSessionMode(modeId, timeoutMs);
3556
+ },
3557
+ setSessionConfigOption: async (configId, value, timeoutMs) => {
3558
+ return await turnController.setSessionConfigOption(configId, value, timeoutMs);
3559
+ }
3560
+ }, {
3561
+ maxQueueDepth,
3562
+ onQueueDepthChanged: (queueDepth) => {
3563
+ setPerfGauge("queue.owner.depth", queueDepth);
3564
+ refreshQueueOwnerLease(lease, { queueDepth }).catch(() => {});
3565
+ }
3566
+ });
3567
+ if (options.verbose) process.stderr.write(`[acpx] queue owner ready for session ${options.sessionId} (ttlMs=${ttlMs}, maxQueueDepth=${maxQueueDepth})\n`);
3568
+ await refreshQueueOwnerLease(lease, { queueDepth: owner.queueDepth() }).catch(() => {});
3569
+ heartbeatTimer = setInterval(() => {
3570
+ refreshQueueOwnerLease(lease, { queueDepth: owner?.queueDepth() ?? 0 }).catch(() => {});
3571
+ }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
3572
+ let isFirstTask = true;
3573
+ while (true) {
3574
+ const pollTimeoutMs = isFirstTask ? initialTaskPollTimeoutMs : taskPollTimeoutMs;
3575
+ const task = await owner.nextTask(pollTimeoutMs);
3576
+ if (!task) break;
3577
+ isFirstTask = false;
3578
+ await runPromptTurn(async () => {
3579
+ try {
3580
+ await runQueuedTask(options.sessionId, task, {
3581
+ sharedClient,
3582
+ verbose: options.verbose,
3583
+ mcpServers: options.mcpServers,
3584
+ nonInteractivePermissions: options.nonInteractivePermissions,
3585
+ authCredentials: options.authCredentials,
3586
+ authPolicy: options.authPolicy,
3587
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3588
+ onClientAvailable: setActiveController,
3589
+ onClientClosed: clearActiveController,
3590
+ onPromptActive: async () => {
3591
+ turnController.markPromptActive();
3592
+ await applyPendingCancel();
3593
+ }
3594
+ });
3595
+ } finally {
3596
+ checkpointPerfMetricsCapture();
3597
+ }
3598
+ });
3599
+ }
3600
+ } finally {
3601
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
3602
+ turnController.beginClosing();
3603
+ if (owner) await owner.close();
3604
+ await sharedClient.close().catch(() => {});
3605
+ try {
3606
+ const record = await resolveSessionRecord(options.sessionId);
3607
+ applyLifecycleSnapshotToRecord(record, sharedClient.getAgentLifecycleSnapshot());
3608
+ await writeSessionRecord(record);
3609
+ } catch {}
3610
+ await releaseQueueOwnerLease(lease);
3611
+ if (options.verbose) process.stderr.write(`[acpx] queue owner stopped for session ${options.sessionId}\n`);
3612
+ }
3613
+ }
3614
+ async function sendSession(options) {
3615
+ const waitForCompletion = options.waitForCompletion !== false;
3616
+ const queuedToOwner = await submitToRunningOwner(options, waitForCompletion);
3617
+ if (queuedToOwner) return queuedToOwner;
3618
+ spawnQueueOwnerProcess(queueOwnerRuntimeOptionsFromSend(options));
3619
+ for (let attempt = 0; attempt < QUEUE_OWNER_STARTUP_MAX_ATTEMPTS; attempt += 1) {
3620
+ const queued = await submitToRunningOwner(options, waitForCompletion);
3621
+ if (queued) return queued;
3622
+ await waitMs$1(QUEUE_CONNECT_RETRY_MS);
3623
+ }
3624
+ throw new Error(`Session queue owner failed to start for session ${options.sessionId}`);
3625
+ }
3626
+ async function cancelSessionPrompt(options) {
3627
+ const cancelled = await tryCancelOnRunningOwner(options);
3628
+ return {
3629
+ sessionId: options.sessionId,
3630
+ cancelled: cancelled === true
3631
+ };
3632
+ }
3633
+ async function setSessionMode(options) {
3634
+ if (await trySetModeOnRunningOwner(options.sessionId, options.modeId, options.timeoutMs, options.verbose)) {
3635
+ const record = await resolveSessionRecord(options.sessionId);
3636
+ setDesiredModeId(record, options.modeId);
3637
+ await writeSessionRecord(record);
3638
+ return {
3639
+ record,
3640
+ resumed: false
3641
+ };
3642
+ }
3643
+ return await runSessionSetModeDirect({
3644
+ sessionRecordId: options.sessionId,
3645
+ modeId: options.modeId,
3646
+ mcpServers: options.mcpServers,
3647
+ nonInteractivePermissions: options.nonInteractivePermissions,
3648
+ authCredentials: options.authCredentials,
3649
+ authPolicy: options.authPolicy,
3650
+ timeoutMs: options.timeoutMs,
3651
+ verbose: options.verbose
3652
+ });
3653
+ }
3654
+ async function setSessionConfigOption(options) {
3655
+ const ownerResponse = await trySetConfigOptionOnRunningOwner(options.sessionId, options.configId, options.value, options.timeoutMs, options.verbose);
3656
+ if (ownerResponse) {
3657
+ const record = await resolveSessionRecord(options.sessionId);
3658
+ if (options.configId === "mode") {
3659
+ setDesiredModeId(record, options.value);
3660
+ await writeSessionRecord(record);
3661
+ }
3662
+ return {
3663
+ record,
3664
+ response: ownerResponse,
3665
+ resumed: false
3666
+ };
3667
+ }
3668
+ return await runSessionSetConfigOptionDirect({
3669
+ sessionRecordId: options.sessionId,
3670
+ configId: options.configId,
3671
+ value: options.value,
3672
+ mcpServers: options.mcpServers,
3673
+ nonInteractivePermissions: options.nonInteractivePermissions,
3674
+ authCredentials: options.authCredentials,
3675
+ authPolicy: options.authPolicy,
3676
+ timeoutMs: options.timeoutMs,
3677
+ verbose: options.verbose
3678
+ });
3679
+ }
3680
+ function firstAgentCommandToken(command) {
3681
+ const trimmed = command.trim();
3682
+ if (!trimmed) return;
3683
+ const token = trimmed.split(/\s+/, 1)[0];
3684
+ return token.length > 0 ? token : void 0;
3685
+ }
3686
+ async function isLikelyMatchingProcess(pid, agentCommand) {
3687
+ const expectedToken = firstAgentCommandToken(agentCommand);
3688
+ if (!expectedToken) return false;
3689
+ const procCmdline = `/proc/${pid}/cmdline`;
3690
+ try {
3691
+ const argv = (await fs$1.readFile(procCmdline, "utf8")).split("\0").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
3692
+ if (argv.length === 0) return false;
3693
+ const executableBase = path.basename(argv[0]);
3694
+ const expectedBase = path.basename(expectedToken);
3695
+ return executableBase === expectedBase || argv.some((entry) => path.basename(entry) === expectedBase);
3696
+ } catch {
3697
+ return true;
3698
+ }
3699
+ }
3700
+ async function closeSession(sessionId) {
3701
+ const record = await resolveSessionRecord(sessionId);
3702
+ await terminateQueueOwnerForSession(record.acpxRecordId);
3703
+ if (record.pid != null && isProcessAlive(record.pid) && await isLikelyMatchingProcess(record.pid, record.agentCommand)) await terminateProcess(record.pid);
3704
+ record.pid = void 0;
3705
+ record.closed = true;
3706
+ record.closedAt = isoNow();
3707
+ await writeSessionRecord(record);
3708
+ return record;
3709
+ }
3710
+
3711
+ //#endregion
3712
+ //#region src/session.ts
3713
+ var session_exports = /* @__PURE__ */ __exportAll({
3714
+ DEFAULT_HISTORY_LIMIT: () => DEFAULT_HISTORY_LIMIT,
3715
+ DEFAULT_QUEUE_OWNER_TTL_MS: () => DEFAULT_QUEUE_OWNER_TTL_MS,
3716
+ InterruptedError: () => InterruptedError,
3717
+ TimeoutError: () => TimeoutError,
3718
+ cancelSessionPrompt: () => cancelSessionPrompt,
3719
+ closeSession: () => closeSession,
3720
+ createSession: () => createSession,
3721
+ ensureSession: () => ensureSession,
3722
+ findGitRepositoryRoot: () => findGitRepositoryRoot,
3723
+ findSession: () => findSession,
3724
+ findSessionByDirectoryWalk: () => findSessionByDirectoryWalk,
3725
+ isProcessAlive: () => isProcessAlive,
3726
+ listSessions: () => listSessions,
3727
+ listSessionsForAgent: () => listSessionsForAgent,
3728
+ normalizeQueueOwnerTtlMs: () => normalizeQueueOwnerTtlMs,
3729
+ runOnce: () => runOnce,
3730
+ runSessionQueueOwner: () => runSessionQueueOwner,
3731
+ sendSession: () => sendSession,
3732
+ setSessionConfigOption: () => setSessionConfigOption,
3733
+ setSessionMode: () => setSessionMode
3734
+ });
3735
+
3736
+ //#endregion
3737
+ export { findGitRepositoryRoot as a, flushPerfMetricsCapture as c, DEFAULT_HISTORY_LIMIT as i, installPerfMetricsCapture as l, DEFAULT_QUEUE_OWNER_TTL_MS as n, findSession as o, runSessionQueueOwner as r, findSessionByDirectoryWalk as s, session_exports as t, InterruptedError as u };
3738
+ //# sourceMappingURL=session-C6nyqSfk.js.map