acpx 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1682 -1216
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command, CommanderError, InvalidArgumentError } from "commander";
4
+ import { Command, CommanderError, InvalidArgumentError as InvalidArgumentError3 } from "commander";
5
5
  import { realpathSync } from "fs";
6
6
  import fs6 from "fs/promises";
7
- import path7 from "path";
7
+ import path8 from "path";
8
8
  import { pathToFileURL } from "url";
9
9
  import { findSkillsRoot, maybeHandleSkillflag } from "skillflag";
10
10
 
@@ -13,7 +13,7 @@ var AGENT_REGISTRY = {
13
13
  codex: "npx @zed-industries/codex-acp",
14
14
  claude: "npx -y @zed-industries/claude-agent-acp",
15
15
  gemini: "gemini",
16
- opencode: "npx opencode-ai",
16
+ opencode: "npx -y opencode-ai acp",
17
17
  pi: "npx pi-acp"
18
18
  };
19
19
  var DEFAULT_AGENT_NAME = "codex";
@@ -43,6 +43,223 @@ function listBuiltInAgents(overrides) {
43
43
  return Object.keys(mergeAgentRegistry(overrides));
44
44
  }
45
45
 
46
+ // src/cli-internal-owner.ts
47
+ import { InvalidArgumentError } from "commander";
48
+
49
+ // src/types.ts
50
+ var EXIT_CODES = {
51
+ SUCCESS: 0,
52
+ ERROR: 1,
53
+ USAGE: 2,
54
+ TIMEOUT: 3,
55
+ NO_SESSION: 4,
56
+ PERMISSION_DENIED: 5,
57
+ INTERRUPTED: 130
58
+ };
59
+ var OUTPUT_FORMATS = ["text", "json", "quiet"];
60
+ var PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"];
61
+ var AUTH_POLICIES = ["skip", "fail"];
62
+ var NON_INTERACTIVE_PERMISSION_POLICIES = ["deny", "fail"];
63
+ var OUTPUT_ERROR_CODES = [
64
+ "NO_SESSION",
65
+ "TIMEOUT",
66
+ "PERMISSION_DENIED",
67
+ "PERMISSION_PROMPT_UNAVAILABLE",
68
+ "RUNTIME",
69
+ "USAGE"
70
+ ];
71
+ var OUTPUT_ERROR_ORIGINS = ["cli", "runtime", "queue", "acp"];
72
+
73
+ // src/cli-internal-owner.ts
74
+ function parseNonEmptyValue(label, value) {
75
+ const trimmed = value.trim();
76
+ if (trimmed.length === 0) {
77
+ throw new InvalidArgumentError(`${label} must not be empty`);
78
+ }
79
+ return trimmed;
80
+ }
81
+ function parseNonNegativeMilliseconds(value) {
82
+ const parsed = Number(value);
83
+ if (!Number.isFinite(parsed) || parsed < 0) {
84
+ throw new InvalidArgumentError("TTL must be a non-negative number of milliseconds");
85
+ }
86
+ return Math.round(parsed);
87
+ }
88
+ function parseTimeoutMilliseconds(value) {
89
+ const parsed = Number(value);
90
+ if (!Number.isFinite(parsed) || parsed <= 0) {
91
+ throw new InvalidArgumentError("Timeout must be a positive number of milliseconds");
92
+ }
93
+ return Math.round(parsed);
94
+ }
95
+ function parsePermissionMode(value) {
96
+ if (!PERMISSION_MODES.includes(value)) {
97
+ throw new InvalidArgumentError(
98
+ `Invalid permission mode "${value}". Expected one of: ${PERMISSION_MODES.join(", ")}`
99
+ );
100
+ }
101
+ return value;
102
+ }
103
+ function parseAuthPolicy(value) {
104
+ if (!AUTH_POLICIES.includes(value)) {
105
+ throw new InvalidArgumentError(
106
+ `Invalid auth policy "${value}". Expected one of: ${AUTH_POLICIES.join(", ")}`
107
+ );
108
+ }
109
+ return value;
110
+ }
111
+ function parseNonInteractivePermissionPolicy(value) {
112
+ if (!NON_INTERACTIVE_PERMISSION_POLICIES.includes(
113
+ value
114
+ )) {
115
+ throw new InvalidArgumentError(
116
+ `Invalid non-interactive permission policy "${value}". Expected one of: ${NON_INTERACTIVE_PERMISSION_POLICIES.join(", ")}`
117
+ );
118
+ }
119
+ return value;
120
+ }
121
+ function parseQueueOwnerFlags(argv, defaultTtlMs) {
122
+ if (argv[0] !== "__queue-owner") {
123
+ return void 0;
124
+ }
125
+ const flags = {
126
+ ttlMs: defaultTtlMs
127
+ };
128
+ const consumeValue = (index, token) => {
129
+ if (token.includes("=")) {
130
+ return {
131
+ value: token.slice(token.indexOf("=") + 1),
132
+ next: index
133
+ };
134
+ }
135
+ const value = argv[index + 1];
136
+ if (!value || value.startsWith("-")) {
137
+ throw new InvalidArgumentError(`${token} requires a value`);
138
+ }
139
+ return {
140
+ value,
141
+ next: index + 1
142
+ };
143
+ };
144
+ for (let index = 1; index < argv.length; index += 1) {
145
+ const token = argv[index];
146
+ if (token === "--session-id" || token.startsWith("--session-id=")) {
147
+ const consumed = consumeValue(index, token);
148
+ flags.sessionId = parseNonEmptyValue("Session id", consumed.value);
149
+ index = consumed.next;
150
+ continue;
151
+ }
152
+ if (token === "--ttl-ms" || token.startsWith("--ttl-ms=")) {
153
+ const consumed = consumeValue(index, token);
154
+ flags.ttlMs = parseNonNegativeMilliseconds(consumed.value);
155
+ index = consumed.next;
156
+ continue;
157
+ }
158
+ if (token === "--permission-mode" || token.startsWith("--permission-mode=")) {
159
+ const consumed = consumeValue(index, token);
160
+ flags.permissionMode = parsePermissionMode(consumed.value);
161
+ index = consumed.next;
162
+ continue;
163
+ }
164
+ if (token === "--non-interactive-permissions" || token.startsWith("--non-interactive-permissions=")) {
165
+ const consumed = consumeValue(index, token);
166
+ flags.nonInteractivePermissions = parseNonInteractivePermissionPolicy(
167
+ consumed.value
168
+ );
169
+ index = consumed.next;
170
+ continue;
171
+ }
172
+ if (token === "--auth-policy" || token.startsWith("--auth-policy=")) {
173
+ const consumed = consumeValue(index, token);
174
+ flags.authPolicy = parseAuthPolicy(consumed.value);
175
+ index = consumed.next;
176
+ continue;
177
+ }
178
+ if (token === "--timeout-ms" || token.startsWith("--timeout-ms=")) {
179
+ const consumed = consumeValue(index, token);
180
+ flags.timeoutMs = parseTimeoutMilliseconds(consumed.value);
181
+ index = consumed.next;
182
+ continue;
183
+ }
184
+ if (token === "--verbose") {
185
+ flags.verbose = true;
186
+ continue;
187
+ }
188
+ if (token === "--suppress-sdk-console-errors") {
189
+ flags.suppressSdkConsoleErrors = true;
190
+ continue;
191
+ }
192
+ throw new InvalidArgumentError(`Unknown __queue-owner option: ${token}`);
193
+ }
194
+ if (!flags.sessionId) {
195
+ throw new InvalidArgumentError("__queue-owner requires --session-id");
196
+ }
197
+ if (!flags.permissionMode) {
198
+ throw new InvalidArgumentError("__queue-owner requires --permission-mode");
199
+ }
200
+ return {
201
+ sessionId: flags.sessionId,
202
+ ttlMs: flags.ttlMs ?? defaultTtlMs,
203
+ permissionMode: flags.permissionMode,
204
+ nonInteractivePermissions: flags.nonInteractivePermissions,
205
+ authPolicy: flags.authPolicy,
206
+ timeoutMs: flags.timeoutMs,
207
+ verbose: flags.verbose,
208
+ suppressSdkConsoleErrors: flags.suppressSdkConsoleErrors
209
+ };
210
+ }
211
+
212
+ // src/cli-public.ts
213
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
214
+ function configurePublicCli(options) {
215
+ const builtInAgents = options.listBuiltInAgents(options.config.agents);
216
+ for (const agentName of builtInAgents) {
217
+ options.registerAgentCommand(options.program, agentName, options.config);
218
+ }
219
+ options.registerDefaultCommands(options.program, options.config);
220
+ const scan = options.detectAgentToken(options.argv);
221
+ if (!scan.hasAgentOverride && scan.token && !options.topLevelVerbs.has(scan.token) && !builtInAgents.includes(scan.token)) {
222
+ options.registerAgentCommand(options.program, scan.token, options.config);
223
+ }
224
+ options.program.argument("[prompt...]", "Prompt text").action(async function(promptParts) {
225
+ if (promptParts.length === 0 && process.stdin.isTTY) {
226
+ if (options.requestedJsonStrict) {
227
+ throw new InvalidArgumentError2(
228
+ "Prompt is required (pass as argument, --file, or pipe via stdin)"
229
+ );
230
+ }
231
+ this.outputHelp();
232
+ return;
233
+ }
234
+ await options.handlePromptAction(this, promptParts);
235
+ });
236
+ options.program.addHelpText(
237
+ "after",
238
+ `
239
+ Examples:
240
+ acpx codex sessions new
241
+ acpx codex "fix the tests"
242
+ acpx codex prompt "fix the tests"
243
+ acpx codex --no-wait "queue follow-up task"
244
+ acpx codex exec "what does this repo do"
245
+ acpx codex cancel
246
+ acpx codex set-mode plan
247
+ acpx codex set approval_policy conservative
248
+ acpx codex -s backend "fix the API"
249
+ acpx codex sessions
250
+ acpx codex sessions new --name backend
251
+ acpx codex sessions ensure --name backend
252
+ acpx codex sessions close backend
253
+ acpx codex status
254
+ acpx config show
255
+ acpx config init
256
+ acpx --ttl 30 codex "investigate flaky tests"
257
+ acpx claude "refactor auth"
258
+ acpx gemini "add logging"
259
+ acpx --agent ./my-custom-server "do something"`
260
+ );
261
+ }
262
+
46
263
  // src/config.ts
47
264
  import fs from "fs/promises";
48
265
  import os from "os";
@@ -92,7 +309,7 @@ function parseTimeoutMs(value, sourcePath) {
92
309
  }
93
310
  return Math.round(value * 1e3);
94
311
  }
95
- function parsePermissionMode(value, sourcePath) {
312
+ function parsePermissionMode2(value, sourcePath) {
96
313
  if (value == null) {
97
314
  return void 0;
98
315
  }
@@ -103,7 +320,7 @@ function parsePermissionMode(value, sourcePath) {
103
320
  }
104
321
  return value;
105
322
  }
106
- function parseNonInteractivePermissionPolicy(value, sourcePath) {
323
+ function parseNonInteractivePermissionPolicy2(value, sourcePath) {
107
324
  if (value == null) {
108
325
  return void 0;
109
326
  }
@@ -116,7 +333,7 @@ function parseNonInteractivePermissionPolicy(value, sourcePath) {
116
333
  }
117
334
  return value;
118
335
  }
119
- function parseAuthPolicy(value, sourcePath) {
336
+ function parseAuthPolicy2(value, sourcePath) {
120
337
  if (value == null) {
121
338
  return void 0;
122
339
  }
@@ -239,15 +456,15 @@ async function loadResolvedConfig(cwd) {
239
456
  const globalConfig = globalResult.config;
240
457
  const projectConfig = projectResult.config;
241
458
  const defaultAgent = parseDefaultAgent(projectConfig?.defaultAgent, projectPath) ?? parseDefaultAgent(globalConfig?.defaultAgent, globalPath) ?? DEFAULT_AGENT_NAME;
242
- const defaultPermissions = parsePermissionMode(projectConfig?.defaultPermissions, projectPath) ?? parsePermissionMode(globalConfig?.defaultPermissions, globalPath) ?? DEFAULT_PERMISSION_MODE;
243
- const nonInteractivePermissions = parseNonInteractivePermissionPolicy(
459
+ const defaultPermissions = parsePermissionMode2(projectConfig?.defaultPermissions, projectPath) ?? parsePermissionMode2(globalConfig?.defaultPermissions, globalPath) ?? DEFAULT_PERMISSION_MODE;
460
+ const nonInteractivePermissions = parseNonInteractivePermissionPolicy2(
244
461
  projectConfig?.nonInteractivePermissions,
245
462
  projectPath
246
- ) ?? parseNonInteractivePermissionPolicy(
463
+ ) ?? parseNonInteractivePermissionPolicy2(
247
464
  globalConfig?.nonInteractivePermissions,
248
465
  globalPath
249
466
  ) ?? DEFAULT_NON_INTERACTIVE_PERMISSION_POLICY;
250
- const authPolicy = parseAuthPolicy(projectConfig?.authPolicy, projectPath) ?? parseAuthPolicy(globalConfig?.authPolicy, globalPath) ?? DEFAULT_AUTH_POLICY;
467
+ const authPolicy = parseAuthPolicy2(projectConfig?.authPolicy, projectPath) ?? parseAuthPolicy2(globalConfig?.authPolicy, globalPath) ?? DEFAULT_AUTH_POLICY;
251
468
  const ttlMs = parseTtlMs(projectConfig?.ttl, projectPath) ?? parseTtlMs(globalConfig?.ttl, globalPath) ?? DEFAULT_TTL_MS;
252
469
  const timeoutConfiguredInProject = projectConfig != null && Object.prototype.hasOwnProperty.call(projectConfig, "timeout");
253
470
  const timeoutConfiguredInGlobal = globalConfig != null && Object.prototype.hasOwnProperty.call(globalConfig, "timeout");
@@ -499,29 +716,6 @@ function isAcpResourceNotFoundError(error) {
499
716
  return isSessionNotFoundText(formatUnknownErrorMessage(error));
500
717
  }
501
718
 
502
- // src/types.ts
503
- var EXIT_CODES = {
504
- SUCCESS: 0,
505
- ERROR: 1,
506
- USAGE: 2,
507
- TIMEOUT: 3,
508
- NO_SESSION: 4,
509
- PERMISSION_DENIED: 5,
510
- INTERRUPTED: 130
511
- };
512
- var OUTPUT_FORMATS = ["text", "json", "quiet"];
513
- var AUTH_POLICIES = ["skip", "fail"];
514
- var NON_INTERACTIVE_PERMISSION_POLICIES = ["deny", "fail"];
515
- var OUTPUT_ERROR_CODES = [
516
- "NO_SESSION",
517
- "TIMEOUT",
518
- "PERMISSION_DENIED",
519
- "PERMISSION_PROMPT_UNAVAILABLE",
520
- "RUNTIME",
521
- "USAGE"
522
- ];
523
- var OUTPUT_ERROR_ORIGINS = ["cli", "runtime", "queue", "acp"];
524
-
525
719
  // src/error-normalization.ts
526
720
  var AUTH_REQUIRED_ACP_CODES = /* @__PURE__ */ new Set([-32e3]);
527
721
  function asRecord2(value) {
@@ -859,12 +1053,12 @@ function formatLocations(locations) {
859
1053
  }
860
1054
  const unique = /* @__PURE__ */ new Set();
861
1055
  for (const location of locations) {
862
- const path8 = location.path?.trim();
863
- if (!path8) {
1056
+ const path9 = location.path?.trim();
1057
+ if (!path9) {
864
1058
  continue;
865
1059
  }
866
1060
  const line = typeof location.line === "number" && Number.isFinite(location.line) ? `:${Math.max(1, Math.trunc(location.line))}` : "";
867
- unique.add(`${path8}${line}`);
1061
+ unique.add(`${path9}${line}`);
868
1062
  }
869
1063
  const items = [...unique];
870
1064
  if (items.length === 0) {
@@ -877,15 +1071,15 @@ function formatLocations(locations) {
877
1071
  }
878
1072
  return `${visible.join(", ")}, +${hidden} more`;
879
1073
  }
880
- function summarizeDiff(path8, oldText, newText) {
1074
+ function summarizeDiff(path9, oldText, newText) {
881
1075
  const oldLines = oldText ? oldText.split("\n").length : 0;
882
1076
  const newLines = newText.split("\n").length;
883
1077
  const delta = newLines - oldLines;
884
1078
  if (delta === 0) {
885
- return `diff ${path8} (line count unchanged)`;
1079
+ return `diff ${path9} (line count unchanged)`;
886
1080
  }
887
1081
  const signedDelta = `${delta > 0 ? "+" : ""}${delta}`;
888
- return `diff ${path8} (${signedDelta} lines)`;
1082
+ return `diff ${path9} (${signedDelta} lines)`;
889
1083
  }
890
1084
  function textFromContentBlock(content) {
891
1085
  switch (content.type) {
@@ -1445,7 +1639,7 @@ function createOutputFormatter(format, options = {}) {
1445
1639
 
1446
1640
  // src/session-runtime.ts
1447
1641
  import fs5 from "fs/promises";
1448
- import path6 from "path";
1642
+ import path7 from "path";
1449
1643
 
1450
1644
  // src/client.ts
1451
1645
  import {
@@ -1454,7 +1648,7 @@ import {
1454
1648
  ndJsonStream
1455
1649
  } from "@agentclientprotocol/sdk";
1456
1650
  import { spawn as spawn2 } from "child_process";
1457
- import path3 from "path";
1651
+ import path4 from "path";
1458
1652
  import { Readable, Writable } from "stream";
1459
1653
 
1460
1654
  // src/filesystem.ts
@@ -1817,6 +2011,60 @@ function extractAgentSessionId(meta) {
1817
2011
  return void 0;
1818
2012
  }
1819
2013
 
2014
+ // src/version.ts
2015
+ import { readFileSync } from "fs";
2016
+ import path3 from "path";
2017
+ import { fileURLToPath } from "url";
2018
+ var UNKNOWN_VERSION = "0.0.0-unknown";
2019
+ var MODULE_DIR = path3.dirname(fileURLToPath(import.meta.url));
2020
+ var cachedVersion = null;
2021
+ function parseVersion(value) {
2022
+ if (typeof value !== "string") {
2023
+ return null;
2024
+ }
2025
+ const trimmed = value.trim();
2026
+ return trimmed.length > 0 ? trimmed : null;
2027
+ }
2028
+ function readPackageVersion(packageJsonPath) {
2029
+ try {
2030
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2031
+ return parseVersion(parsed.version);
2032
+ } catch {
2033
+ return null;
2034
+ }
2035
+ }
2036
+ function resolveVersionFromAncestors(startDir) {
2037
+ let current = startDir;
2038
+ while (true) {
2039
+ const packageVersion = readPackageVersion(path3.join(current, "package.json"));
2040
+ if (packageVersion) {
2041
+ return packageVersion;
2042
+ }
2043
+ const parent = path3.dirname(current);
2044
+ if (parent === current) {
2045
+ return null;
2046
+ }
2047
+ current = parent;
2048
+ }
2049
+ }
2050
+ function resolveAcpxVersion(params) {
2051
+ const envVersion = parseVersion((params?.env ?? process.env).npm_package_version);
2052
+ if (envVersion) {
2053
+ return envVersion;
2054
+ }
2055
+ if (params?.packageJsonPath) {
2056
+ return readPackageVersion(params.packageJsonPath) ?? UNKNOWN_VERSION;
2057
+ }
2058
+ return resolveVersionFromAncestors(MODULE_DIR) ?? UNKNOWN_VERSION;
2059
+ }
2060
+ function getAcpxVersion() {
2061
+ if (cachedVersion) {
2062
+ return cachedVersion;
2063
+ }
2064
+ cachedVersion = resolveAcpxVersion();
2065
+ return cachedVersion;
2066
+ }
2067
+
1820
2068
  // src/terminal.ts
1821
2069
  import { spawn } from "child_process";
1822
2070
  import { randomUUID } from "crypto";
@@ -2283,7 +2531,7 @@ function splitCommandLine(value) {
2283
2531
  };
2284
2532
  }
2285
2533
  function asAbsoluteCwd(cwd) {
2286
- return path3.resolve(cwd);
2534
+ return path4.resolve(cwd);
2287
2535
  }
2288
2536
  function toEnvToken(value) {
2289
2537
  return value.trim().replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
@@ -2491,7 +2739,7 @@ var AcpClient = class {
2491
2739
  },
2492
2740
  clientInfo: {
2493
2741
  name: "acpx",
2494
- version: "0.1.0"
2742
+ version: getAcpxVersion()
2495
2743
  }
2496
2744
  });
2497
2745
  await this.authenticateIfRequired(connection, initResult.authMethods ?? []);
@@ -2914,115 +3162,270 @@ var AcpClient = class {
2914
3162
  }
2915
3163
  };
2916
3164
 
2917
- // src/queue-owner-turn-controller.ts
2918
- var QueueOwnerTurnController = class {
2919
- options;
2920
- state = "idle";
2921
- pendingCancel = false;
2922
- activeController;
2923
- constructor(options) {
2924
- this.options = options;
3165
+ // src/queue-lease-store.ts
3166
+ import { createHash } from "crypto";
3167
+ import fs3 from "fs/promises";
3168
+ import os2 from "os";
3169
+ import path5 from "path";
3170
+ var PROCESS_EXIT_GRACE_MS = 1500;
3171
+ var PROCESS_POLL_MS = 50;
3172
+ var QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
3173
+ function queueBaseDir() {
3174
+ return path5.join(os2.homedir(), ".acpx", "queues");
3175
+ }
3176
+ function queueKeyForSession(sessionId) {
3177
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
3178
+ }
3179
+ function queueLockFilePath(sessionId) {
3180
+ return path5.join(queueBaseDir(), `${queueKeyForSession(sessionId)}.lock`);
3181
+ }
3182
+ function queueSocketPath(sessionId) {
3183
+ const key = queueKeyForSession(sessionId);
3184
+ if (process.platform === "win32") {
3185
+ return `\\\\.\\pipe\\acpx-${key}`;
2925
3186
  }
2926
- get lifecycleState() {
2927
- return this.state;
3187
+ return path5.join(queueBaseDir(), `${key}.sock`);
3188
+ }
3189
+ function parseQueueOwnerRecord(raw) {
3190
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3191
+ return null;
2928
3192
  }
2929
- get hasPendingCancel() {
2930
- return this.pendingCancel;
3193
+ const record = raw;
3194
+ if (!Number.isInteger(record.pid) || record.pid <= 0 || typeof record.sessionId !== "string" || typeof record.socketPath !== "string" || typeof record.createdAt !== "string" || typeof record.heartbeatAt !== "string" || !Number.isInteger(record.ownerGeneration) || record.ownerGeneration <= 0 || !Number.isInteger(record.queueDepth) || record.queueDepth < 0) {
3195
+ return null;
2931
3196
  }
2932
- beginTurn() {
2933
- this.state = "starting";
2934
- this.pendingCancel = false;
3197
+ return {
3198
+ pid: record.pid,
3199
+ sessionId: record.sessionId,
3200
+ socketPath: record.socketPath,
3201
+ createdAt: record.createdAt,
3202
+ heartbeatAt: record.heartbeatAt,
3203
+ ownerGeneration: record.ownerGeneration,
3204
+ queueDepth: record.queueDepth
3205
+ };
3206
+ }
3207
+ function createOwnerGeneration() {
3208
+ return Date.now() * 1e3 + Math.floor(Math.random() * 1e3);
3209
+ }
3210
+ function nowIso4() {
3211
+ return (/* @__PURE__ */ new Date()).toISOString();
3212
+ }
3213
+ function isQueueOwnerHeartbeatStale(owner) {
3214
+ const heartbeatMs = Date.parse(owner.heartbeatAt);
3215
+ if (!Number.isFinite(heartbeatMs)) {
3216
+ return true;
2935
3217
  }
2936
- markPromptActive() {
2937
- if (this.state === "starting" || this.state === "active") {
2938
- this.state = "active";
2939
- }
3218
+ return Date.now() - heartbeatMs > QUEUE_OWNER_STALE_HEARTBEAT_MS;
3219
+ }
3220
+ async function ensureQueueDir() {
3221
+ await fs3.mkdir(queueBaseDir(), { recursive: true });
3222
+ }
3223
+ async function removeSocketFile(socketPath) {
3224
+ if (process.platform === "win32") {
3225
+ return;
2940
3226
  }
2941
- endTurn() {
2942
- this.state = "idle";
2943
- this.pendingCancel = false;
3227
+ try {
3228
+ await fs3.unlink(socketPath);
3229
+ } catch (error) {
3230
+ if (error.code !== "ENOENT") {
3231
+ throw error;
3232
+ }
2944
3233
  }
2945
- beginClosing() {
2946
- this.state = "closing";
2947
- this.pendingCancel = false;
2948
- this.activeController = void 0;
3234
+ }
3235
+ async function waitForProcessExit(pid, timeoutMs) {
3236
+ const deadline = Date.now() + Math.max(0, timeoutMs);
3237
+ while (Date.now() <= deadline) {
3238
+ if (!isProcessAlive(pid)) {
3239
+ return true;
3240
+ }
3241
+ await waitMs2(PROCESS_POLL_MS);
2949
3242
  }
2950
- setActiveController(controller) {
2951
- this.activeController = controller;
3243
+ return !isProcessAlive(pid);
3244
+ }
3245
+ async function cleanupStaleQueueOwner(sessionId, owner) {
3246
+ const lockPath = queueLockFilePath(sessionId);
3247
+ const socketPath = owner?.socketPath ?? queueSocketPath(sessionId);
3248
+ await removeSocketFile(socketPath).catch(() => {
3249
+ });
3250
+ await fs3.unlink(lockPath).catch((error) => {
3251
+ if (error.code !== "ENOENT") {
3252
+ throw error;
3253
+ }
3254
+ });
3255
+ }
3256
+ async function readQueueOwnerRecord(sessionId) {
3257
+ const lockPath = queueLockFilePath(sessionId);
3258
+ try {
3259
+ const payload = await fs3.readFile(lockPath, "utf8");
3260
+ const parsed = parseQueueOwnerRecord(JSON.parse(payload));
3261
+ return parsed ?? void 0;
3262
+ } catch {
3263
+ return void 0;
2952
3264
  }
2953
- clearActiveController() {
2954
- this.activeController = void 0;
3265
+ }
3266
+ function isProcessAlive(pid) {
3267
+ if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) {
3268
+ return false;
2955
3269
  }
2956
- assertCanHandleControlRequest() {
2957
- if (this.state === "closing") {
2958
- throw new QueueConnectionError("Queue owner is closing", {
2959
- detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
2960
- origin: "queue",
2961
- retryable: true
2962
- });
2963
- }
3270
+ try {
3271
+ process.kill(pid, 0);
3272
+ return true;
3273
+ } catch {
3274
+ return false;
2964
3275
  }
2965
- async requestCancel() {
2966
- const activeController = this.activeController;
2967
- if (activeController?.hasActivePrompt()) {
2968
- const cancelled2 = await activeController.requestCancelActivePrompt();
2969
- if (cancelled2) {
2970
- this.pendingCancel = false;
2971
- }
2972
- return cancelled2;
2973
- }
2974
- if (this.state === "starting" || this.state === "active") {
2975
- this.pendingCancel = true;
2976
- return true;
2977
- }
3276
+ }
3277
+ async function terminateProcess(pid) {
3278
+ if (!isProcessAlive(pid)) {
2978
3279
  return false;
2979
3280
  }
2980
- async applyPendingCancel() {
2981
- const activeController = this.activeController;
2982
- if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) {
2983
- return false;
3281
+ try {
3282
+ process.kill(pid, "SIGTERM");
3283
+ } catch {
3284
+ return false;
3285
+ }
3286
+ if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) {
3287
+ return true;
3288
+ }
3289
+ try {
3290
+ process.kill(pid, "SIGKILL");
3291
+ } catch {
3292
+ return false;
3293
+ }
3294
+ await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
3295
+ return true;
3296
+ }
3297
+ async function ensureOwnerIsUsable(sessionId, owner) {
3298
+ const alive = isProcessAlive(owner.pid);
3299
+ const stale = isQueueOwnerHeartbeatStale(owner);
3300
+ if (alive && !stale) {
3301
+ return true;
3302
+ }
3303
+ if (alive) {
3304
+ await terminateProcess(owner.pid).catch(() => {
3305
+ });
3306
+ }
3307
+ await cleanupStaleQueueOwner(sessionId, owner);
3308
+ return false;
3309
+ }
3310
+ async function readQueueOwnerStatus(sessionId) {
3311
+ const owner = await readQueueOwnerRecord(sessionId);
3312
+ if (!owner) {
3313
+ return void 0;
3314
+ }
3315
+ const alive = await ensureOwnerIsUsable(sessionId, owner);
3316
+ if (!alive) {
3317
+ return void 0;
3318
+ }
3319
+ return {
3320
+ pid: owner.pid,
3321
+ socketPath: owner.socketPath,
3322
+ heartbeatAt: owner.heartbeatAt,
3323
+ ownerGeneration: owner.ownerGeneration,
3324
+ queueDepth: owner.queueDepth,
3325
+ alive,
3326
+ stale: isQueueOwnerHeartbeatStale(owner)
3327
+ };
3328
+ }
3329
+ async function tryAcquireQueueOwnerLease(sessionId, nowIsoFactory = nowIso4) {
3330
+ await ensureQueueDir();
3331
+ const lockPath = queueLockFilePath(sessionId);
3332
+ const socketPath = queueSocketPath(sessionId);
3333
+ const createdAt = nowIsoFactory();
3334
+ const ownerGeneration = createOwnerGeneration();
3335
+ const payload = JSON.stringify(
3336
+ {
3337
+ pid: process.pid,
3338
+ sessionId,
3339
+ socketPath,
3340
+ createdAt,
3341
+ heartbeatAt: createdAt,
3342
+ ownerGeneration,
3343
+ queueDepth: 0
3344
+ },
3345
+ null,
3346
+ 2
3347
+ );
3348
+ try {
3349
+ await fs3.writeFile(lockPath, `${payload}
3350
+ `, {
3351
+ encoding: "utf8",
3352
+ flag: "wx"
3353
+ });
3354
+ await removeSocketFile(socketPath).catch(() => {
3355
+ });
3356
+ return {
3357
+ sessionId,
3358
+ lockPath,
3359
+ socketPath,
3360
+ createdAt,
3361
+ ownerGeneration
3362
+ };
3363
+ } catch (error) {
3364
+ if (error.code !== "EEXIST") {
3365
+ throw error;
2984
3366
  }
2985
- const cancelled2 = await activeController.requestCancelActivePrompt();
2986
- if (cancelled2) {
2987
- this.pendingCancel = false;
3367
+ const owner = await readQueueOwnerRecord(sessionId);
3368
+ if (!owner) {
3369
+ await cleanupStaleQueueOwner(sessionId, owner);
3370
+ return void 0;
2988
3371
  }
2989
- return cancelled2;
2990
- }
2991
- async setSessionMode(modeId, timeoutMs) {
2992
- this.assertCanHandleControlRequest();
2993
- const activeController = this.activeController;
2994
- if (activeController) {
2995
- await this.options.withTimeout(
2996
- async () => await activeController.setSessionMode(modeId),
2997
- timeoutMs
2998
- );
2999
- return;
3372
+ if (!isProcessAlive(owner.pid) || isQueueOwnerHeartbeatStale(owner)) {
3373
+ if (isProcessAlive(owner.pid)) {
3374
+ await terminateProcess(owner.pid).catch(() => {
3375
+ });
3376
+ }
3377
+ await cleanupStaleQueueOwner(sessionId, owner);
3000
3378
  }
3001
- await this.options.setSessionModeFallback(modeId, timeoutMs);
3379
+ return void 0;
3002
3380
  }
3003
- async setSessionConfigOption(configId, value, timeoutMs) {
3004
- this.assertCanHandleControlRequest();
3005
- const activeController = this.activeController;
3006
- if (activeController) {
3007
- return await this.options.withTimeout(
3008
- async () => await activeController.setSessionConfigOption(configId, value),
3009
- timeoutMs
3010
- );
3381
+ }
3382
+ async function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso4) {
3383
+ const payload = JSON.stringify(
3384
+ {
3385
+ pid: process.pid,
3386
+ sessionId: lease.sessionId,
3387
+ socketPath: lease.socketPath,
3388
+ createdAt: lease.createdAt,
3389
+ heartbeatAt: nowIsoFactory(),
3390
+ ownerGeneration: lease.ownerGeneration,
3391
+ queueDepth: Math.max(0, Math.round(options.queueDepth))
3392
+ },
3393
+ null,
3394
+ 2
3395
+ );
3396
+ await fs3.writeFile(lease.lockPath, `${payload}
3397
+ `, {
3398
+ encoding: "utf8"
3399
+ });
3400
+ }
3401
+ async function releaseQueueOwnerLease(lease) {
3402
+ await removeSocketFile(lease.socketPath).catch(() => {
3403
+ });
3404
+ await fs3.unlink(lease.lockPath).catch((error) => {
3405
+ if (error.code !== "ENOENT") {
3406
+ throw error;
3011
3407
  }
3012
- return await this.options.setSessionConfigOptionFallback(
3013
- configId,
3014
- value,
3015
- timeoutMs
3016
- );
3408
+ });
3409
+ }
3410
+ async function terminateQueueOwnerForSession(sessionId) {
3411
+ const owner = await readQueueOwnerRecord(sessionId);
3412
+ if (!owner) {
3413
+ return;
3017
3414
  }
3018
- };
3415
+ if (isProcessAlive(owner.pid)) {
3416
+ await terminateProcess(owner.pid);
3417
+ }
3418
+ await cleanupStaleQueueOwner(sessionId, owner);
3419
+ }
3420
+ async function waitMs2(ms) {
3421
+ await new Promise((resolve) => {
3422
+ setTimeout(resolve, ms);
3423
+ });
3424
+ }
3019
3425
 
3020
- // src/queue-ipc.ts
3021
- import { createHash, randomUUID as randomUUID2 } from "crypto";
3022
- import fs3 from "fs/promises";
3426
+ // src/queue-ipc-client.ts
3427
+ import { randomUUID as randomUUID2 } from "crypto";
3023
3428
  import net from "net";
3024
- import os2 from "os";
3025
- import path4 from "path";
3026
3429
 
3027
3430
  // src/queue-messages.ts
3028
3431
  function asRecord4(value) {
@@ -3255,214 +3658,26 @@ function parseQueueOwnerMessage(raw) {
3255
3658
  return null;
3256
3659
  }
3257
3660
 
3258
- // src/queue-ipc.ts
3259
- var PROCESS_EXIT_GRACE_MS = 1500;
3260
- var PROCESS_POLL_MS = 50;
3661
+ // src/queue-ipc-client.ts
3261
3662
  var QUEUE_CONNECT_ATTEMPTS = 40;
3262
3663
  var QUEUE_CONNECT_RETRY_MS = 50;
3263
- function queueBaseDir() {
3264
- return path4.join(os2.homedir(), ".acpx", "queues");
3265
- }
3266
- function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
3267
- return {
3268
- type: "error",
3269
- requestId,
3270
- code: "RUNTIME",
3271
- detailCode,
3272
- origin: "queue",
3273
- retryable: options.retryable,
3274
- message
3275
- };
3664
+ function shouldRetryQueueConnect(error) {
3665
+ const code = error.code;
3666
+ return code === "ENOENT" || code === "ECONNREFUSED";
3276
3667
  }
3277
- function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options = {}) {
3278
- const normalized = normalizeOutputError(error, {
3279
- defaultCode: "RUNTIME",
3280
- origin: "queue",
3281
- detailCode,
3282
- retryable: options.retryable
3283
- });
3284
- return {
3285
- type: "error",
3286
- requestId,
3287
- code: normalized.code,
3288
- detailCode: normalized.detailCode,
3289
- origin: normalized.origin,
3290
- message: normalized.message,
3291
- retryable: normalized.retryable,
3292
- acp: normalized.acp
3293
- };
3294
- }
3295
- function isProcessAlive(pid) {
3296
- if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) {
3297
- return false;
3298
- }
3299
- try {
3300
- process.kill(pid, 0);
3301
- return true;
3302
- } catch {
3303
- return false;
3304
- }
3305
- }
3306
- async function waitForProcessExit(pid, timeoutMs) {
3307
- const deadline = Date.now() + Math.max(0, timeoutMs);
3308
- while (Date.now() <= deadline) {
3309
- if (!isProcessAlive(pid)) {
3310
- return true;
3311
- }
3312
- await new Promise((resolve) => {
3313
- setTimeout(resolve, PROCESS_POLL_MS);
3314
- });
3315
- }
3316
- return !isProcessAlive(pid);
3317
- }
3318
- async function terminateProcess(pid) {
3319
- if (!isProcessAlive(pid)) {
3320
- return false;
3321
- }
3322
- try {
3323
- process.kill(pid, "SIGTERM");
3324
- } catch {
3325
- return false;
3326
- }
3327
- if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) {
3328
- return true;
3329
- }
3330
- try {
3331
- process.kill(pid, "SIGKILL");
3332
- } catch {
3333
- return false;
3334
- }
3335
- await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
3336
- return true;
3337
- }
3338
- function parseQueueOwnerRecord(raw) {
3339
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3340
- return null;
3341
- }
3342
- const record = raw;
3343
- if (!Number.isInteger(record.pid) || record.pid <= 0 || typeof record.sessionId !== "string" || typeof record.socketPath !== "string") {
3344
- return null;
3345
- }
3346
- return {
3347
- pid: record.pid,
3348
- sessionId: record.sessionId,
3349
- socketPath: record.socketPath
3350
- };
3351
- }
3352
- function queueKeyForSession(sessionId) {
3353
- return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
3354
- }
3355
- function queueLockFilePath(sessionId) {
3356
- return path4.join(queueBaseDir(), `${queueKeyForSession(sessionId)}.lock`);
3357
- }
3358
- function queueSocketPath(sessionId) {
3359
- const key = queueKeyForSession(sessionId);
3360
- if (process.platform === "win32") {
3361
- return `\\\\.\\pipe\\acpx-${key}`;
3362
- }
3363
- return path4.join(queueBaseDir(), `${key}.sock`);
3364
- }
3365
- async function ensureQueueDir() {
3366
- await fs3.mkdir(queueBaseDir(), { recursive: true });
3367
- }
3368
- async function removeSocketFile(socketPath) {
3369
- if (process.platform === "win32") {
3370
- return;
3371
- }
3372
- try {
3373
- await fs3.unlink(socketPath);
3374
- } catch (error) {
3375
- if (error.code !== "ENOENT") {
3376
- throw error;
3377
- }
3378
- }
3379
- }
3380
- async function readQueueOwnerRecord(sessionId) {
3381
- const lockPath = queueLockFilePath(sessionId);
3382
- try {
3383
- const payload = await fs3.readFile(lockPath, "utf8");
3384
- const parsed = parseQueueOwnerRecord(JSON.parse(payload));
3385
- return parsed ?? void 0;
3386
- } catch {
3387
- return void 0;
3388
- }
3389
- }
3390
- async function cleanupStaleQueueOwner(sessionId, owner) {
3391
- const lockPath = queueLockFilePath(sessionId);
3392
- const socketPath = owner?.socketPath ?? queueSocketPath(sessionId);
3393
- await removeSocketFile(socketPath).catch(() => {
3394
- });
3395
- await fs3.unlink(lockPath).catch((error) => {
3396
- if (error.code !== "ENOENT") {
3397
- throw error;
3398
- }
3399
- });
3400
- }
3401
- async function tryAcquireQueueOwnerLease(sessionId, nowIso4 = () => (/* @__PURE__ */ new Date()).toISOString()) {
3402
- await ensureQueueDir();
3403
- const lockPath = queueLockFilePath(sessionId);
3404
- const socketPath = queueSocketPath(sessionId);
3405
- const payload = JSON.stringify(
3406
- {
3407
- pid: process.pid,
3408
- sessionId,
3409
- socketPath,
3410
- createdAt: nowIso4()
3411
- },
3412
- null,
3413
- 2
3414
- );
3415
- try {
3416
- await fs3.writeFile(lockPath, `${payload}
3417
- `, {
3418
- encoding: "utf8",
3419
- flag: "wx"
3420
- });
3421
- await removeSocketFile(socketPath).catch(() => {
3422
- });
3423
- return { lockPath, socketPath };
3424
- } catch (error) {
3425
- if (error.code !== "EEXIST") {
3426
- throw error;
3427
- }
3428
- const owner = await readQueueOwnerRecord(sessionId);
3429
- if (!owner || !isProcessAlive(owner.pid)) {
3430
- await cleanupStaleQueueOwner(sessionId, owner);
3431
- }
3432
- return void 0;
3433
- }
3434
- }
3435
- async function releaseQueueOwnerLease(lease) {
3436
- await removeSocketFile(lease.socketPath).catch(() => {
3437
- });
3438
- await fs3.unlink(lease.lockPath).catch((error) => {
3439
- if (error.code !== "ENOENT") {
3440
- throw error;
3441
- }
3442
- });
3443
- }
3444
- function shouldRetryQueueConnect(error) {
3445
- const code = error.code;
3446
- return code === "ENOENT" || code === "ECONNREFUSED";
3447
- }
3448
- async function waitMs2(ms) {
3449
- await new Promise((resolve) => {
3450
- setTimeout(resolve, ms);
3451
- });
3452
- }
3453
- async function connectToSocket(socketPath) {
3454
- return await new Promise((resolve, reject) => {
3455
- const socket = net.createConnection(socketPath);
3456
- const onConnect = () => {
3457
- socket.off("error", onError);
3458
- resolve(socket);
3459
- };
3460
- const onError = (error) => {
3461
- socket.off("connect", onConnect);
3462
- reject(error);
3463
- };
3464
- socket.once("connect", onConnect);
3465
- socket.once("error", onError);
3668
+ async function connectToSocket(socketPath) {
3669
+ return await new Promise((resolve, reject) => {
3670
+ const socket = net.createConnection(socketPath);
3671
+ const onConnect = () => {
3672
+ socket.off("error", onError);
3673
+ resolve(socket);
3674
+ };
3675
+ const onError = (error) => {
3676
+ socket.off("connect", onConnect);
3677
+ reject(error);
3678
+ };
3679
+ socket.once("connect", onConnect);
3680
+ socket.once("error", onError);
3466
3681
  });
3467
3682
  }
3468
3683
  async function connectToQueueOwner(owner) {
@@ -3486,320 +3701,36 @@ async function connectToQueueOwner(owner) {
3486
3701
  }
3487
3702
  return void 0;
3488
3703
  }
3489
- function writeQueueMessage(socket, message) {
3490
- if (socket.destroyed || !socket.writable) {
3491
- return;
3492
- }
3493
- socket.write(`${JSON.stringify(message)}
3494
- `);
3495
- }
3496
- var SessionQueueOwner = class _SessionQueueOwner {
3497
- server;
3498
- controlHandlers;
3499
- pending = [];
3500
- waiters = [];
3501
- closed = false;
3502
- constructor(server, controlHandlers) {
3503
- this.server = server;
3504
- this.controlHandlers = controlHandlers;
3505
- }
3506
- static async start(lease, controlHandlers) {
3507
- const ownerRef = { current: void 0 };
3508
- const server = net.createServer((socket) => {
3509
- ownerRef.current?.handleConnection(socket);
3510
- });
3511
- ownerRef.current = new _SessionQueueOwner(server, controlHandlers);
3512
- await new Promise((resolve, reject) => {
3513
- const onListening = () => {
3514
- server.off("error", onError);
3515
- resolve();
3516
- };
3517
- const onError = (error) => {
3518
- server.off("listening", onListening);
3519
- reject(error);
3520
- };
3521
- server.once("listening", onListening);
3522
- server.once("error", onError);
3523
- server.listen(lease.socketPath);
3524
- });
3525
- return ownerRef.current;
3704
+ async function submitToQueueOwner(owner, options) {
3705
+ const socket = await connectToQueueOwner(owner);
3706
+ if (!socket) {
3707
+ return void 0;
3526
3708
  }
3527
- async close() {
3528
- if (this.closed) {
3529
- return;
3530
- }
3531
- this.closed = true;
3532
- for (const waiter of this.waiters.splice(0)) {
3533
- waiter(void 0);
3534
- }
3535
- for (const task of this.pending.splice(0)) {
3536
- if (task.waitForCompletion) {
3537
- task.send(
3538
- makeQueueOwnerError(
3539
- task.requestId,
3540
- "Queue owner shutting down before prompt execution",
3541
- "QUEUE_OWNER_SHUTTING_DOWN",
3542
- {
3543
- retryable: true
3544
- }
3545
- )
3546
- );
3547
- }
3548
- task.close();
3549
- }
3550
- await new Promise((resolve) => {
3551
- this.server.close(() => resolve());
3552
- });
3553
- }
3554
- async nextTask(timeoutMs) {
3555
- if (this.pending.length > 0) {
3556
- return this.pending.shift();
3557
- }
3558
- if (this.closed) {
3559
- return void 0;
3560
- }
3561
- return await new Promise((resolve) => {
3562
- const shouldTimeout = timeoutMs != null;
3563
- const timer = shouldTimeout && setTimeout(
3564
- () => {
3565
- const index = this.waiters.indexOf(waiter);
3566
- if (index >= 0) {
3567
- this.waiters.splice(index, 1);
3568
- }
3569
- resolve(void 0);
3570
- },
3571
- Math.max(0, timeoutMs)
3572
- );
3573
- const waiter = (task) => {
3574
- if (timer) {
3575
- clearTimeout(timer);
3576
- }
3577
- resolve(task);
3578
- };
3579
- this.waiters.push(waiter);
3580
- });
3581
- }
3582
- enqueue(task) {
3583
- if (this.closed) {
3584
- if (task.waitForCompletion) {
3585
- task.send(
3586
- makeQueueOwnerError(
3587
- task.requestId,
3588
- "Queue owner is shutting down",
3589
- "QUEUE_OWNER_SHUTTING_DOWN",
3590
- {
3591
- retryable: true
3592
- }
3593
- )
3594
- );
3595
- }
3596
- task.close();
3597
- return;
3598
- }
3599
- const waiter = this.waiters.shift();
3600
- if (waiter) {
3601
- waiter(task);
3602
- return;
3603
- }
3604
- this.pending.push(task);
3605
- }
3606
- handleConnection(socket) {
3607
- socket.setEncoding("utf8");
3608
- if (this.closed) {
3609
- writeQueueMessage(
3610
- socket,
3611
- makeQueueOwnerError("unknown", "Queue owner is closed", "QUEUE_OWNER_CLOSED", {
3612
- retryable: true
3613
- })
3614
- );
3615
- socket.end();
3616
- return;
3617
- }
3618
- let buffer = "";
3619
- let handled = false;
3620
- const fail = (requestId, message, detailCode) => {
3621
- writeQueueMessage(
3622
- socket,
3623
- makeQueueOwnerError(requestId, message, detailCode, {
3624
- retryable: false
3625
- })
3626
- );
3627
- socket.end();
3628
- };
3629
- const processLine = (line) => {
3630
- if (handled) {
3631
- return;
3632
- }
3633
- handled = true;
3634
- let parsed;
3635
- try {
3636
- parsed = JSON.parse(line);
3637
- } catch {
3638
- fail(
3639
- "unknown",
3640
- "Invalid queue request payload",
3641
- "QUEUE_REQUEST_PAYLOAD_INVALID_JSON"
3642
- );
3643
- return;
3644
- }
3645
- const request = parseQueueRequest(parsed);
3646
- if (!request) {
3647
- fail("unknown", "Invalid queue request", "QUEUE_REQUEST_INVALID");
3648
- return;
3649
- }
3650
- if (request.type === "cancel_prompt") {
3651
- writeQueueMessage(socket, {
3652
- type: "accepted",
3653
- requestId: request.requestId
3654
- });
3655
- void this.controlHandlers.cancelPrompt().then((cancelled2) => {
3656
- writeQueueMessage(socket, {
3657
- type: "cancel_result",
3658
- requestId: request.requestId,
3659
- cancelled: cancelled2
3660
- });
3661
- }).catch((error) => {
3662
- writeQueueMessage(
3663
- socket,
3664
- makeQueueOwnerErrorFromUnknown(
3665
- request.requestId,
3666
- error,
3667
- "QUEUE_CONTROL_REQUEST_FAILED"
3668
- )
3669
- );
3670
- }).finally(() => {
3671
- if (!socket.destroyed) {
3672
- socket.end();
3673
- }
3674
- });
3675
- return;
3676
- }
3677
- if (request.type === "set_mode") {
3678
- writeQueueMessage(socket, {
3679
- type: "accepted",
3680
- requestId: request.requestId
3681
- });
3682
- void this.controlHandlers.setSessionMode(request.modeId, request.timeoutMs).then(() => {
3683
- writeQueueMessage(socket, {
3684
- type: "set_mode_result",
3685
- requestId: request.requestId,
3686
- modeId: request.modeId
3687
- });
3688
- }).catch((error) => {
3689
- writeQueueMessage(
3690
- socket,
3691
- makeQueueOwnerErrorFromUnknown(
3692
- request.requestId,
3693
- error,
3694
- "QUEUE_CONTROL_REQUEST_FAILED"
3695
- )
3696
- );
3697
- }).finally(() => {
3698
- if (!socket.destroyed) {
3699
- socket.end();
3700
- }
3701
- });
3702
- return;
3703
- }
3704
- if (request.type === "set_config_option") {
3705
- writeQueueMessage(socket, {
3706
- type: "accepted",
3707
- requestId: request.requestId
3708
- });
3709
- void this.controlHandlers.setSessionConfigOption(request.configId, request.value, request.timeoutMs).then((response) => {
3710
- writeQueueMessage(socket, {
3711
- type: "set_config_option_result",
3712
- requestId: request.requestId,
3713
- response
3714
- });
3715
- }).catch((error) => {
3716
- writeQueueMessage(
3717
- socket,
3718
- makeQueueOwnerErrorFromUnknown(
3719
- request.requestId,
3720
- error,
3721
- "QUEUE_CONTROL_REQUEST_FAILED"
3722
- )
3723
- );
3724
- }).finally(() => {
3725
- if (!socket.destroyed) {
3726
- socket.end();
3727
- }
3728
- });
3729
- return;
3730
- }
3731
- const task = {
3732
- requestId: request.requestId,
3733
- message: request.message,
3734
- permissionMode: request.permissionMode,
3735
- nonInteractivePermissions: request.nonInteractivePermissions,
3736
- timeoutMs: request.timeoutMs,
3737
- suppressSdkConsoleErrors: request.suppressSdkConsoleErrors,
3738
- waitForCompletion: request.waitForCompletion,
3739
- send: (message) => {
3740
- writeQueueMessage(socket, message);
3741
- },
3742
- close: () => {
3743
- if (!socket.destroyed) {
3744
- socket.end();
3745
- }
3746
- }
3747
- };
3748
- writeQueueMessage(socket, {
3749
- type: "accepted",
3750
- requestId: request.requestId
3751
- });
3752
- if (!request.waitForCompletion) {
3753
- task.close();
3754
- }
3755
- this.enqueue(task);
3756
- };
3757
- socket.on("data", (chunk) => {
3758
- buffer += chunk;
3759
- let index = buffer.indexOf("\n");
3760
- while (index >= 0) {
3761
- const line = buffer.slice(0, index).trim();
3762
- buffer = buffer.slice(index + 1);
3763
- if (line.length > 0) {
3764
- processLine(line);
3765
- }
3766
- index = buffer.indexOf("\n");
3767
- }
3768
- });
3769
- socket.on("error", () => {
3770
- });
3771
- }
3772
- };
3773
- async function submitToQueueOwner(owner, options) {
3774
- const socket = await connectToQueueOwner(owner);
3775
- if (!socket) {
3776
- return void 0;
3777
- }
3778
- socket.setEncoding("utf8");
3779
- const requestId = randomUUID2();
3780
- const request = {
3781
- type: "submit_prompt",
3782
- requestId,
3783
- message: options.message,
3784
- permissionMode: options.permissionMode,
3785
- nonInteractivePermissions: options.nonInteractivePermissions,
3786
- timeoutMs: options.timeoutMs,
3787
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3788
- waitForCompletion: options.waitForCompletion
3789
- };
3790
- options.outputFormatter.setContext({
3791
- sessionId: options.sessionId,
3792
- requestId,
3793
- stream: "prompt"
3794
- });
3795
- return await new Promise((resolve, reject) => {
3796
- let settled = false;
3797
- let acknowledged = false;
3798
- let buffer = "";
3799
- let sawDone = false;
3800
- const finishResolve = (result) => {
3801
- if (settled) {
3802
- return;
3709
+ socket.setEncoding("utf8");
3710
+ const requestId = randomUUID2();
3711
+ const request = {
3712
+ type: "submit_prompt",
3713
+ requestId,
3714
+ message: options.message,
3715
+ permissionMode: options.permissionMode,
3716
+ nonInteractivePermissions: options.nonInteractivePermissions,
3717
+ timeoutMs: options.timeoutMs,
3718
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3719
+ waitForCompletion: options.waitForCompletion
3720
+ };
3721
+ options.outputFormatter.setContext({
3722
+ sessionId: options.sessionId,
3723
+ requestId,
3724
+ stream: "prompt"
3725
+ });
3726
+ return await new Promise((resolve, reject) => {
3727
+ let settled = false;
3728
+ let acknowledged = false;
3729
+ let buffer = "";
3730
+ let sawDone = false;
3731
+ const finishResolve = (result) => {
3732
+ if (settled) {
3733
+ return;
3803
3734
  }
3804
3735
  settled = true;
3805
3736
  socket.removeAllListeners();
@@ -3976,105 +3907,651 @@ async function submitToQueueOwner(owner, options) {
3976
3907
  retryable: true
3977
3908
  })
3978
3909
  );
3979
- });
3980
- socket.write(`${JSON.stringify(request)}
3981
- `);
3982
- });
3983
- }
3984
- async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
3985
- const socket = await connectToQueueOwner(owner);
3986
- if (!socket) {
3987
- return void 0;
3988
- }
3989
- socket.setEncoding("utf8");
3990
- return await new Promise((resolve, reject) => {
3991
- let settled = false;
3992
- let acknowledged = false;
3910
+ });
3911
+ socket.write(`${JSON.stringify(request)}
3912
+ `);
3913
+ });
3914
+ }
3915
+ async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
3916
+ const socket = await connectToQueueOwner(owner);
3917
+ if (!socket) {
3918
+ return void 0;
3919
+ }
3920
+ socket.setEncoding("utf8");
3921
+ return await new Promise((resolve, reject) => {
3922
+ let settled = false;
3923
+ let acknowledged = false;
3924
+ let buffer = "";
3925
+ const finishResolve = (result) => {
3926
+ if (settled) {
3927
+ return;
3928
+ }
3929
+ settled = true;
3930
+ socket.removeAllListeners();
3931
+ if (!socket.destroyed) {
3932
+ socket.end();
3933
+ }
3934
+ resolve(result);
3935
+ };
3936
+ const finishReject = (error) => {
3937
+ if (settled) {
3938
+ return;
3939
+ }
3940
+ settled = true;
3941
+ socket.removeAllListeners();
3942
+ if (!socket.destroyed) {
3943
+ socket.destroy();
3944
+ }
3945
+ reject(error);
3946
+ };
3947
+ const processLine = (line) => {
3948
+ let parsed;
3949
+ try {
3950
+ parsed = JSON.parse(line);
3951
+ } catch {
3952
+ finishReject(
3953
+ new QueueProtocolError("Queue owner sent invalid JSON payload", {
3954
+ detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
3955
+ origin: "queue",
3956
+ retryable: true
3957
+ })
3958
+ );
3959
+ return;
3960
+ }
3961
+ const message = parseQueueOwnerMessage(parsed);
3962
+ if (!message || message.requestId !== request.requestId) {
3963
+ finishReject(
3964
+ new QueueProtocolError("Queue owner sent malformed message", {
3965
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
3966
+ origin: "queue",
3967
+ retryable: true
3968
+ })
3969
+ );
3970
+ return;
3971
+ }
3972
+ if (message.type === "accepted") {
3973
+ acknowledged = true;
3974
+ return;
3975
+ }
3976
+ if (message.type === "error") {
3977
+ finishReject(
3978
+ new QueueConnectionError(message.message, {
3979
+ outputCode: message.code,
3980
+ detailCode: message.detailCode,
3981
+ origin: message.origin ?? "queue",
3982
+ retryable: message.retryable,
3983
+ acp: message.acp
3984
+ })
3985
+ );
3986
+ return;
3987
+ }
3988
+ if (!acknowledged) {
3989
+ finishReject(
3990
+ new QueueConnectionError("Queue owner did not acknowledge request", {
3991
+ detailCode: "QUEUE_ACK_MISSING",
3992
+ origin: "queue",
3993
+ retryable: true
3994
+ })
3995
+ );
3996
+ return;
3997
+ }
3998
+ if (!isExpectedResponse(message)) {
3999
+ finishReject(
4000
+ new QueueProtocolError("Queue owner returned unexpected response", {
4001
+ detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
4002
+ origin: "queue",
4003
+ retryable: true
4004
+ })
4005
+ );
4006
+ return;
4007
+ }
4008
+ finishResolve(message);
4009
+ };
4010
+ socket.on("data", (chunk) => {
4011
+ buffer += chunk;
4012
+ let index = buffer.indexOf("\n");
4013
+ while (index >= 0) {
4014
+ const line = buffer.slice(0, index).trim();
4015
+ buffer = buffer.slice(index + 1);
4016
+ if (line.length > 0) {
4017
+ processLine(line);
4018
+ }
4019
+ index = buffer.indexOf("\n");
4020
+ }
4021
+ });
4022
+ socket.once("error", (error) => {
4023
+ finishReject(error);
4024
+ });
4025
+ socket.once("close", () => {
4026
+ if (settled) {
4027
+ return;
4028
+ }
4029
+ if (!acknowledged) {
4030
+ finishReject(
4031
+ new QueueConnectionError(
4032
+ "Queue owner disconnected before acknowledging request",
4033
+ {
4034
+ detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
4035
+ origin: "queue",
4036
+ retryable: true
4037
+ }
4038
+ )
4039
+ );
4040
+ return;
4041
+ }
4042
+ finishReject(
4043
+ new QueueConnectionError("Queue owner disconnected before responding", {
4044
+ detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
4045
+ origin: "queue",
4046
+ retryable: true
4047
+ })
4048
+ );
4049
+ });
4050
+ socket.write(`${JSON.stringify(request)}
4051
+ `);
4052
+ });
4053
+ }
4054
+ async function submitCancelToQueueOwner(owner) {
4055
+ const request = {
4056
+ type: "cancel_prompt",
4057
+ requestId: randomUUID2()
4058
+ };
4059
+ const response = await submitControlToQueueOwner(
4060
+ owner,
4061
+ request,
4062
+ (message) => message.type === "cancel_result"
4063
+ );
4064
+ if (!response) {
4065
+ return void 0;
4066
+ }
4067
+ if (response.requestId !== request.requestId) {
4068
+ throw new QueueProtocolError("Queue owner returned mismatched cancel response", {
4069
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4070
+ origin: "queue",
4071
+ retryable: true
4072
+ });
4073
+ }
4074
+ return response.cancelled;
4075
+ }
4076
+ async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
4077
+ const request = {
4078
+ type: "set_mode",
4079
+ requestId: randomUUID2(),
4080
+ modeId,
4081
+ timeoutMs
4082
+ };
4083
+ const response = await submitControlToQueueOwner(
4084
+ owner,
4085
+ request,
4086
+ (message) => message.type === "set_mode_result"
4087
+ );
4088
+ if (!response) {
4089
+ return void 0;
4090
+ }
4091
+ if (response.requestId !== request.requestId) {
4092
+ throw new QueueProtocolError("Queue owner returned mismatched set_mode response", {
4093
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4094
+ origin: "queue",
4095
+ retryable: true
4096
+ });
4097
+ }
4098
+ return true;
4099
+ }
4100
+ async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
4101
+ const request = {
4102
+ type: "set_config_option",
4103
+ requestId: randomUUID2(),
4104
+ configId,
4105
+ value,
4106
+ timeoutMs
4107
+ };
4108
+ const response = await submitControlToQueueOwner(
4109
+ owner,
4110
+ request,
4111
+ (message) => message.type === "set_config_option_result"
4112
+ );
4113
+ if (!response) {
4114
+ return void 0;
4115
+ }
4116
+ if (response.requestId !== request.requestId) {
4117
+ throw new QueueProtocolError(
4118
+ "Queue owner returned mismatched set_config_option response",
4119
+ {
4120
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4121
+ origin: "queue",
4122
+ retryable: true
4123
+ }
4124
+ );
4125
+ }
4126
+ return response.response;
4127
+ }
4128
+ async function trySubmitToRunningOwner(options) {
4129
+ const owner = await readQueueOwnerRecord(options.sessionId);
4130
+ if (!owner) {
4131
+ return void 0;
4132
+ }
4133
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4134
+ return void 0;
4135
+ }
4136
+ const submitted = await submitToQueueOwner(owner, options);
4137
+ if (submitted) {
4138
+ if (options.verbose) {
4139
+ process.stderr.write(
4140
+ `[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}
4141
+ `
4142
+ );
4143
+ }
4144
+ return submitted;
4145
+ }
4146
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4147
+ return void 0;
4148
+ }
4149
+ throw new QueueConnectionError(
4150
+ "Session queue owner is running but not accepting queue requests",
4151
+ {
4152
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4153
+ origin: "queue",
4154
+ retryable: true
4155
+ }
4156
+ );
4157
+ }
4158
+ async function tryCancelOnRunningOwner(options) {
4159
+ const owner = await readQueueOwnerRecord(options.sessionId);
4160
+ if (!owner) {
4161
+ return void 0;
4162
+ }
4163
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4164
+ return void 0;
4165
+ }
4166
+ const cancelled2 = await submitCancelToQueueOwner(owner);
4167
+ if (cancelled2 !== void 0) {
4168
+ if (options.verbose) {
4169
+ process.stderr.write(
4170
+ `[acpx] requested cancel on active owner pid ${owner.pid} for session ${options.sessionId}
4171
+ `
4172
+ );
4173
+ }
4174
+ return cancelled2;
4175
+ }
4176
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4177
+ return void 0;
4178
+ }
4179
+ throw new QueueConnectionError(
4180
+ "Session queue owner is running but not accepting cancel requests",
4181
+ {
4182
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4183
+ origin: "queue",
4184
+ retryable: true
4185
+ }
4186
+ );
4187
+ }
4188
+ async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
4189
+ const owner = await readQueueOwnerRecord(sessionId);
4190
+ if (!owner) {
4191
+ return void 0;
4192
+ }
4193
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
4194
+ return void 0;
4195
+ }
4196
+ const submitted = await submitSetModeToQueueOwner(owner, modeId, timeoutMs);
4197
+ if (submitted) {
4198
+ if (verbose) {
4199
+ process.stderr.write(
4200
+ `[acpx] requested session/set_mode on owner pid ${owner.pid} for session ${sessionId}
4201
+ `
4202
+ );
4203
+ }
4204
+ return true;
4205
+ }
4206
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
4207
+ return void 0;
4208
+ }
4209
+ throw new QueueConnectionError(
4210
+ "Session queue owner is running but not accepting set_mode requests",
4211
+ {
4212
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4213
+ origin: "queue",
4214
+ retryable: true
4215
+ }
4216
+ );
4217
+ }
4218
+ async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
4219
+ const owner = await readQueueOwnerRecord(sessionId);
4220
+ if (!owner) {
4221
+ return void 0;
4222
+ }
4223
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
4224
+ return void 0;
4225
+ }
4226
+ const response = await submitSetConfigOptionToQueueOwner(
4227
+ owner,
4228
+ configId,
4229
+ value,
4230
+ timeoutMs
4231
+ );
4232
+ if (response) {
4233
+ if (verbose) {
4234
+ process.stderr.write(
4235
+ `[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}
4236
+ `
4237
+ );
4238
+ }
4239
+ return response;
4240
+ }
4241
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
4242
+ return void 0;
4243
+ }
4244
+ throw new QueueConnectionError(
4245
+ "Session queue owner is running but not accepting set_config_option requests",
4246
+ {
4247
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4248
+ origin: "queue",
4249
+ retryable: true
4250
+ }
4251
+ );
4252
+ }
4253
+
4254
+ // src/queue-ipc-server.ts
4255
+ import net2 from "net";
4256
+ function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
4257
+ return {
4258
+ type: "error",
4259
+ requestId,
4260
+ code: "RUNTIME",
4261
+ detailCode,
4262
+ origin: "queue",
4263
+ retryable: options.retryable,
4264
+ message
4265
+ };
4266
+ }
4267
+ function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options = {}) {
4268
+ const normalized = normalizeOutputError(error, {
4269
+ defaultCode: "RUNTIME",
4270
+ origin: "queue",
4271
+ detailCode,
4272
+ retryable: options.retryable
4273
+ });
4274
+ return {
4275
+ type: "error",
4276
+ requestId,
4277
+ code: normalized.code,
4278
+ detailCode: normalized.detailCode,
4279
+ origin: normalized.origin,
4280
+ message: normalized.message,
4281
+ retryable: normalized.retryable,
4282
+ acp: normalized.acp
4283
+ };
4284
+ }
4285
+ function writeQueueMessage(socket, message) {
4286
+ if (socket.destroyed || !socket.writable) {
4287
+ return;
4288
+ }
4289
+ socket.write(`${JSON.stringify(message)}
4290
+ `);
4291
+ }
4292
+ var SessionQueueOwner = class _SessionQueueOwner {
4293
+ server;
4294
+ controlHandlers;
4295
+ pending = [];
4296
+ waiters = [];
4297
+ closed = false;
4298
+ constructor(server, controlHandlers) {
4299
+ this.server = server;
4300
+ this.controlHandlers = controlHandlers;
4301
+ }
4302
+ static async start(lease, controlHandlers) {
4303
+ const ownerRef = { current: void 0 };
4304
+ const server = net2.createServer((socket) => {
4305
+ ownerRef.current?.handleConnection(socket);
4306
+ });
4307
+ ownerRef.current = new _SessionQueueOwner(server, controlHandlers);
4308
+ await new Promise((resolve, reject) => {
4309
+ const onListening = () => {
4310
+ server.off("error", onError);
4311
+ resolve();
4312
+ };
4313
+ const onError = (error) => {
4314
+ server.off("listening", onListening);
4315
+ reject(error);
4316
+ };
4317
+ server.once("listening", onListening);
4318
+ server.once("error", onError);
4319
+ server.listen(lease.socketPath);
4320
+ });
4321
+ return ownerRef.current;
4322
+ }
4323
+ async close() {
4324
+ if (this.closed) {
4325
+ return;
4326
+ }
4327
+ this.closed = true;
4328
+ for (const waiter of this.waiters.splice(0)) {
4329
+ waiter(void 0);
4330
+ }
4331
+ for (const task of this.pending.splice(0)) {
4332
+ if (task.waitForCompletion) {
4333
+ task.send(
4334
+ makeQueueOwnerError(
4335
+ task.requestId,
4336
+ "Queue owner shutting down before prompt execution",
4337
+ "QUEUE_OWNER_SHUTTING_DOWN",
4338
+ {
4339
+ retryable: true
4340
+ }
4341
+ )
4342
+ );
4343
+ }
4344
+ task.close();
4345
+ }
4346
+ await new Promise((resolve) => {
4347
+ this.server.close(() => resolve());
4348
+ });
4349
+ }
4350
+ async nextTask(timeoutMs) {
4351
+ if (this.pending.length > 0) {
4352
+ return this.pending.shift();
4353
+ }
4354
+ if (this.closed) {
4355
+ return void 0;
4356
+ }
4357
+ return await new Promise((resolve) => {
4358
+ const shouldTimeout = timeoutMs != null;
4359
+ const timer = shouldTimeout && setTimeout(
4360
+ () => {
4361
+ const index = this.waiters.indexOf(waiter);
4362
+ if (index >= 0) {
4363
+ this.waiters.splice(index, 1);
4364
+ }
4365
+ resolve(void 0);
4366
+ },
4367
+ Math.max(0, timeoutMs)
4368
+ );
4369
+ const waiter = (task) => {
4370
+ if (timer) {
4371
+ clearTimeout(timer);
4372
+ }
4373
+ resolve(task);
4374
+ };
4375
+ this.waiters.push(waiter);
4376
+ });
4377
+ }
4378
+ queueDepth() {
4379
+ return this.pending.length;
4380
+ }
4381
+ enqueue(task) {
4382
+ if (this.closed) {
4383
+ if (task.waitForCompletion) {
4384
+ task.send(
4385
+ makeQueueOwnerError(
4386
+ task.requestId,
4387
+ "Queue owner is shutting down",
4388
+ "QUEUE_OWNER_SHUTTING_DOWN",
4389
+ {
4390
+ retryable: true
4391
+ }
4392
+ )
4393
+ );
4394
+ }
4395
+ task.close();
4396
+ return;
4397
+ }
4398
+ const waiter = this.waiters.shift();
4399
+ if (waiter) {
4400
+ waiter(task);
4401
+ return;
4402
+ }
4403
+ this.pending.push(task);
4404
+ }
4405
+ handleConnection(socket) {
4406
+ socket.setEncoding("utf8");
4407
+ if (this.closed) {
4408
+ writeQueueMessage(
4409
+ socket,
4410
+ makeQueueOwnerError("unknown", "Queue owner is closed", "QUEUE_OWNER_CLOSED", {
4411
+ retryable: true
4412
+ })
4413
+ );
4414
+ socket.end();
4415
+ return;
4416
+ }
3993
4417
  let buffer = "";
3994
- const finishResolve = (result) => {
3995
- if (settled) {
3996
- return;
3997
- }
3998
- settled = true;
3999
- socket.removeAllListeners();
4000
- if (!socket.destroyed) {
4001
- socket.end();
4002
- }
4003
- resolve(result);
4418
+ let handled = false;
4419
+ const fail = (requestId, message, detailCode) => {
4420
+ writeQueueMessage(
4421
+ socket,
4422
+ makeQueueOwnerError(requestId, message, detailCode, {
4423
+ retryable: false
4424
+ })
4425
+ );
4426
+ socket.end();
4004
4427
  };
4005
- const finishReject = (error) => {
4006
- if (settled) {
4428
+ const processLine = (line) => {
4429
+ if (handled) {
4007
4430
  return;
4008
4431
  }
4009
- settled = true;
4010
- socket.removeAllListeners();
4011
- if (!socket.destroyed) {
4012
- socket.destroy();
4013
- }
4014
- reject(error);
4015
- };
4016
- const processLine = (line) => {
4432
+ handled = true;
4017
4433
  let parsed;
4018
4434
  try {
4019
4435
  parsed = JSON.parse(line);
4020
4436
  } catch {
4021
- finishReject(
4022
- new QueueProtocolError("Queue owner sent invalid JSON payload", {
4023
- detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
4024
- origin: "queue",
4025
- retryable: true
4026
- })
4437
+ fail(
4438
+ "unknown",
4439
+ "Invalid queue request payload",
4440
+ "QUEUE_REQUEST_PAYLOAD_INVALID_JSON"
4027
4441
  );
4028
4442
  return;
4029
4443
  }
4030
- const message = parseQueueOwnerMessage(parsed);
4031
- if (!message || message.requestId !== request.requestId) {
4032
- finishReject(
4033
- new QueueProtocolError("Queue owner sent malformed message", {
4034
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4035
- origin: "queue",
4036
- retryable: true
4037
- })
4038
- );
4444
+ const request = parseQueueRequest(parsed);
4445
+ if (!request) {
4446
+ fail("unknown", "Invalid queue request", "QUEUE_REQUEST_INVALID");
4039
4447
  return;
4040
4448
  }
4041
- if (message.type === "accepted") {
4042
- acknowledged = true;
4449
+ if (request.type === "cancel_prompt") {
4450
+ writeQueueMessage(socket, {
4451
+ type: "accepted",
4452
+ requestId: request.requestId
4453
+ });
4454
+ void this.controlHandlers.cancelPrompt().then((cancelled2) => {
4455
+ writeQueueMessage(socket, {
4456
+ type: "cancel_result",
4457
+ requestId: request.requestId,
4458
+ cancelled: cancelled2
4459
+ });
4460
+ }).catch((error) => {
4461
+ writeQueueMessage(
4462
+ socket,
4463
+ makeQueueOwnerErrorFromUnknown(
4464
+ request.requestId,
4465
+ error,
4466
+ "QUEUE_CONTROL_REQUEST_FAILED"
4467
+ )
4468
+ );
4469
+ }).finally(() => {
4470
+ if (!socket.destroyed) {
4471
+ socket.end();
4472
+ }
4473
+ });
4043
4474
  return;
4044
4475
  }
4045
- if (message.type === "error") {
4046
- finishReject(
4047
- new QueueConnectionError(message.message, {
4048
- outputCode: message.code,
4049
- detailCode: message.detailCode,
4050
- origin: message.origin ?? "queue",
4051
- retryable: message.retryable,
4052
- acp: message.acp
4053
- })
4054
- );
4476
+ if (request.type === "set_mode") {
4477
+ writeQueueMessage(socket, {
4478
+ type: "accepted",
4479
+ requestId: request.requestId
4480
+ });
4481
+ void this.controlHandlers.setSessionMode(request.modeId, request.timeoutMs).then(() => {
4482
+ writeQueueMessage(socket, {
4483
+ type: "set_mode_result",
4484
+ requestId: request.requestId,
4485
+ modeId: request.modeId
4486
+ });
4487
+ }).catch((error) => {
4488
+ writeQueueMessage(
4489
+ socket,
4490
+ makeQueueOwnerErrorFromUnknown(
4491
+ request.requestId,
4492
+ error,
4493
+ "QUEUE_CONTROL_REQUEST_FAILED"
4494
+ )
4495
+ );
4496
+ }).finally(() => {
4497
+ if (!socket.destroyed) {
4498
+ socket.end();
4499
+ }
4500
+ });
4055
4501
  return;
4056
4502
  }
4057
- if (!acknowledged) {
4058
- finishReject(
4059
- new QueueConnectionError("Queue owner did not acknowledge request", {
4060
- detailCode: "QUEUE_ACK_MISSING",
4061
- origin: "queue",
4062
- retryable: true
4063
- })
4064
- );
4503
+ if (request.type === "set_config_option") {
4504
+ writeQueueMessage(socket, {
4505
+ type: "accepted",
4506
+ requestId: request.requestId
4507
+ });
4508
+ void this.controlHandlers.setSessionConfigOption(request.configId, request.value, request.timeoutMs).then((response) => {
4509
+ writeQueueMessage(socket, {
4510
+ type: "set_config_option_result",
4511
+ requestId: request.requestId,
4512
+ response
4513
+ });
4514
+ }).catch((error) => {
4515
+ writeQueueMessage(
4516
+ socket,
4517
+ makeQueueOwnerErrorFromUnknown(
4518
+ request.requestId,
4519
+ error,
4520
+ "QUEUE_CONTROL_REQUEST_FAILED"
4521
+ )
4522
+ );
4523
+ }).finally(() => {
4524
+ if (!socket.destroyed) {
4525
+ socket.end();
4526
+ }
4527
+ });
4065
4528
  return;
4066
4529
  }
4067
- if (!isExpectedResponse(message)) {
4068
- finishReject(
4069
- new QueueProtocolError("Queue owner returned unexpected response", {
4070
- detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
4071
- origin: "queue",
4072
- retryable: true
4073
- })
4074
- );
4075
- return;
4530
+ const task = {
4531
+ requestId: request.requestId,
4532
+ message: request.message,
4533
+ permissionMode: request.permissionMode,
4534
+ nonInteractivePermissions: request.nonInteractivePermissions,
4535
+ timeoutMs: request.timeoutMs,
4536
+ suppressSdkConsoleErrors: request.suppressSdkConsoleErrors,
4537
+ waitForCompletion: request.waitForCompletion,
4538
+ send: (message) => {
4539
+ writeQueueMessage(socket, message);
4540
+ },
4541
+ close: () => {
4542
+ if (!socket.destroyed) {
4543
+ socket.end();
4544
+ }
4545
+ }
4546
+ };
4547
+ writeQueueMessage(socket, {
4548
+ type: "accepted",
4549
+ requestId: request.requestId
4550
+ });
4551
+ if (!request.waitForCompletion) {
4552
+ task.close();
4076
4553
  }
4077
- finishResolve(message);
4554
+ this.enqueue(task);
4078
4555
  };
4079
4556
  socket.on("data", (chunk) => {
4080
4557
  buffer += chunk;
@@ -4088,268 +4565,276 @@ async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
4088
4565
  index = buffer.indexOf("\n");
4089
4566
  }
4090
4567
  });
4091
- socket.once("error", (error) => {
4092
- finishReject(error);
4093
- });
4094
- socket.once("close", () => {
4095
- if (settled) {
4096
- return;
4097
- }
4098
- if (!acknowledged) {
4099
- finishReject(
4100
- new QueueConnectionError(
4101
- "Queue owner disconnected before acknowledging request",
4102
- {
4103
- detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
4104
- origin: "queue",
4105
- retryable: true
4106
- }
4107
- )
4108
- );
4109
- return;
4110
- }
4111
- finishReject(
4112
- new QueueConnectionError("Queue owner disconnected before responding", {
4113
- detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
4114
- origin: "queue",
4115
- retryable: true
4116
- })
4117
- );
4118
- });
4119
- socket.write(`${JSON.stringify(request)}
4120
- `);
4121
- });
4122
- }
4123
- async function submitCancelToQueueOwner(owner) {
4124
- const request = {
4125
- type: "cancel_prompt",
4126
- requestId: randomUUID2()
4127
- };
4128
- const response = await submitControlToQueueOwner(
4129
- owner,
4130
- request,
4131
- (message) => message.type === "cancel_result"
4132
- );
4133
- if (!response) {
4134
- return void 0;
4135
- }
4136
- if (response.requestId !== request.requestId) {
4137
- throw new QueueProtocolError("Queue owner returned mismatched cancel response", {
4138
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4139
- origin: "queue",
4140
- retryable: true
4568
+ socket.on("error", () => {
4141
4569
  });
4142
4570
  }
4143
- return response.cancelled;
4144
- }
4145
- async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
4146
- const request = {
4147
- type: "set_mode",
4148
- requestId: randomUUID2(),
4149
- modeId,
4150
- timeoutMs
4151
- };
4152
- const response = await submitControlToQueueOwner(
4153
- owner,
4154
- request,
4155
- (message) => message.type === "set_mode_result"
4156
- );
4157
- if (!response) {
4158
- return void 0;
4571
+ };
4572
+
4573
+ // src/queue-owner-turn-controller.ts
4574
+ var QueueOwnerTurnController = class {
4575
+ options;
4576
+ state = "idle";
4577
+ pendingCancel = false;
4578
+ activeController;
4579
+ constructor(options) {
4580
+ this.options = options;
4159
4581
  }
4160
- if (response.requestId !== request.requestId) {
4161
- throw new QueueProtocolError("Queue owner returned mismatched set_mode response", {
4162
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4163
- origin: "queue",
4164
- retryable: true
4165
- });
4582
+ get lifecycleState() {
4583
+ return this.state;
4166
4584
  }
4167
- return true;
4168
- }
4169
- async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
4170
- const request = {
4171
- type: "set_config_option",
4172
- requestId: randomUUID2(),
4173
- configId,
4174
- value,
4175
- timeoutMs
4176
- };
4177
- const response = await submitControlToQueueOwner(
4178
- owner,
4179
- request,
4180
- (message) => message.type === "set_config_option_result"
4181
- );
4182
- if (!response) {
4183
- return void 0;
4585
+ get hasPendingCancel() {
4586
+ return this.pendingCancel;
4184
4587
  }
4185
- if (response.requestId !== request.requestId) {
4186
- throw new QueueProtocolError(
4187
- "Queue owner returned mismatched set_config_option response",
4188
- {
4189
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4190
- origin: "queue",
4191
- retryable: true
4192
- }
4193
- );
4588
+ beginTurn() {
4589
+ this.state = "starting";
4590
+ this.pendingCancel = false;
4194
4591
  }
4195
- return response.response;
4196
- }
4197
- async function trySubmitToRunningOwner(options) {
4198
- const owner = await readQueueOwnerRecord(options.sessionId);
4199
- if (!owner) {
4200
- return void 0;
4592
+ markPromptActive() {
4593
+ if (this.state === "starting" || this.state === "active") {
4594
+ this.state = "active";
4595
+ }
4201
4596
  }
4202
- if (!isProcessAlive(owner.pid)) {
4203
- await cleanupStaleQueueOwner(options.sessionId, owner);
4204
- return void 0;
4597
+ endTurn() {
4598
+ this.state = "idle";
4599
+ this.pendingCancel = false;
4205
4600
  }
4206
- const submitted = await submitToQueueOwner(owner, options);
4207
- if (submitted) {
4208
- if (options.verbose) {
4209
- process.stderr.write(
4210
- `[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}
4211
- `
4212
- );
4213
- }
4214
- return submitted;
4601
+ beginClosing() {
4602
+ this.state = "closing";
4603
+ this.pendingCancel = false;
4604
+ this.activeController = void 0;
4215
4605
  }
4216
- if (!isProcessAlive(owner.pid)) {
4217
- await cleanupStaleQueueOwner(options.sessionId, owner);
4218
- return void 0;
4606
+ setActiveController(controller) {
4607
+ this.activeController = controller;
4219
4608
  }
4220
- throw new QueueConnectionError(
4221
- "Session queue owner is running but not accepting queue requests",
4222
- {
4223
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4224
- origin: "queue",
4225
- retryable: true
4609
+ clearActiveController() {
4610
+ this.activeController = void 0;
4611
+ }
4612
+ assertCanHandleControlRequest() {
4613
+ if (this.state === "closing") {
4614
+ throw new QueueConnectionError("Queue owner is closing", {
4615
+ detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
4616
+ origin: "queue",
4617
+ retryable: true
4618
+ });
4226
4619
  }
4227
- );
4228
- }
4229
- async function tryCancelOnRunningOwner(options) {
4230
- const owner = await readQueueOwnerRecord(options.sessionId);
4231
- if (!owner) {
4232
- return void 0;
4233
4620
  }
4234
- if (!isProcessAlive(owner.pid)) {
4235
- await cleanupStaleQueueOwner(options.sessionId, owner);
4236
- return void 0;
4621
+ async requestCancel() {
4622
+ const activeController = this.activeController;
4623
+ if (activeController?.hasActivePrompt()) {
4624
+ const cancelled2 = await activeController.requestCancelActivePrompt();
4625
+ if (cancelled2) {
4626
+ this.pendingCancel = false;
4627
+ }
4628
+ return cancelled2;
4629
+ }
4630
+ if (this.state === "starting" || this.state === "active") {
4631
+ this.pendingCancel = true;
4632
+ return true;
4633
+ }
4634
+ return false;
4237
4635
  }
4238
- const cancelled2 = await submitCancelToQueueOwner(owner);
4239
- if (cancelled2 !== void 0) {
4240
- if (options.verbose) {
4241
- process.stderr.write(
4242
- `[acpx] requested cancel on active owner pid ${owner.pid} for session ${options.sessionId}
4243
- `
4244
- );
4636
+ async applyPendingCancel() {
4637
+ const activeController = this.activeController;
4638
+ if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) {
4639
+ return false;
4640
+ }
4641
+ const cancelled2 = await activeController.requestCancelActivePrompt();
4642
+ if (cancelled2) {
4643
+ this.pendingCancel = false;
4245
4644
  }
4246
4645
  return cancelled2;
4247
4646
  }
4248
- if (!isProcessAlive(owner.pid)) {
4249
- await cleanupStaleQueueOwner(options.sessionId, owner);
4250
- return void 0;
4251
- }
4252
- throw new QueueConnectionError(
4253
- "Session queue owner is running but not accepting cancel requests",
4254
- {
4255
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4256
- origin: "queue",
4257
- retryable: true
4647
+ async setSessionMode(modeId, timeoutMs) {
4648
+ this.assertCanHandleControlRequest();
4649
+ const activeController = this.activeController;
4650
+ if (activeController) {
4651
+ await this.options.withTimeout(
4652
+ async () => await activeController.setSessionMode(modeId),
4653
+ timeoutMs
4654
+ );
4655
+ return;
4258
4656
  }
4259
- );
4260
- }
4261
- async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
4262
- const owner = await readQueueOwnerRecord(sessionId);
4263
- if (!owner) {
4264
- return void 0;
4265
- }
4266
- if (!isProcessAlive(owner.pid)) {
4267
- await cleanupStaleQueueOwner(sessionId, owner);
4268
- return void 0;
4657
+ await this.options.setSessionModeFallback(modeId, timeoutMs);
4269
4658
  }
4270
- const submitted = await submitSetModeToQueueOwner(owner, modeId, timeoutMs);
4271
- if (submitted) {
4272
- if (verbose) {
4273
- process.stderr.write(
4274
- `[acpx] requested session/set_mode on owner pid ${owner.pid} for session ${sessionId}
4275
- `
4659
+ async setSessionConfigOption(configId, value, timeoutMs) {
4660
+ this.assertCanHandleControlRequest();
4661
+ const activeController = this.activeController;
4662
+ if (activeController) {
4663
+ return await this.options.withTimeout(
4664
+ async () => await activeController.setSessionConfigOption(configId, value),
4665
+ timeoutMs
4276
4666
  );
4277
4667
  }
4278
- return true;
4668
+ return await this.options.setSessionConfigOptionFallback(
4669
+ configId,
4670
+ value,
4671
+ timeoutMs
4672
+ );
4279
4673
  }
4280
- if (!isProcessAlive(owner.pid)) {
4281
- await cleanupStaleQueueOwner(sessionId, owner);
4282
- return void 0;
4674
+ };
4675
+
4676
+ // src/session-owner-runtime.ts
4677
+ var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
4678
+ var QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 2e3;
4679
+ function normalizeQueueOwnerTtlMs(ttlMs) {
4680
+ if (ttlMs == null) {
4681
+ return DEFAULT_QUEUE_OWNER_TTL_MS;
4283
4682
  }
4284
- throw new QueueConnectionError(
4285
- "Session queue owner is running but not accepting set_mode requests",
4286
- {
4287
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4288
- origin: "queue",
4289
- retryable: true
4683
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) {
4684
+ return DEFAULT_QUEUE_OWNER_TTL_MS;
4685
+ }
4686
+ return Math.round(ttlMs);
4687
+ }
4688
+ function createQueueOwnerTurnRuntime(options, deps) {
4689
+ const turnController = new QueueOwnerTurnController({
4690
+ withTimeout: async (run, timeoutMs) => await deps.withTimeout(run, timeoutMs),
4691
+ setSessionModeFallback: deps.setSessionModeFallback,
4692
+ setSessionConfigOptionFallback: deps.setSessionConfigOptionFallback
4693
+ });
4694
+ const applyPendingCancel = async () => {
4695
+ return await turnController.applyPendingCancel();
4696
+ };
4697
+ const scheduleApplyPendingCancel = () => {
4698
+ void applyPendingCancel().catch((error) => {
4699
+ if (options.verbose) {
4700
+ process.stderr.write(
4701
+ `[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}
4702
+ `
4703
+ );
4704
+ }
4705
+ });
4706
+ };
4707
+ return {
4708
+ beginClosing: () => {
4709
+ turnController.beginClosing();
4710
+ },
4711
+ onClientAvailable: (controller) => {
4712
+ turnController.setActiveController(controller);
4713
+ scheduleApplyPendingCancel();
4714
+ },
4715
+ onClientClosed: () => {
4716
+ turnController.clearActiveController();
4717
+ },
4718
+ onPromptActive: async () => {
4719
+ turnController.markPromptActive();
4720
+ await applyPendingCancel();
4721
+ },
4722
+ runPromptTurn: async (run) => {
4723
+ turnController.beginTurn();
4724
+ try {
4725
+ return await run();
4726
+ } finally {
4727
+ turnController.endTurn();
4728
+ }
4729
+ },
4730
+ controlHandlers: {
4731
+ cancelPrompt: async () => {
4732
+ const accepted = await turnController.requestCancel();
4733
+ if (!accepted) {
4734
+ return false;
4735
+ }
4736
+ await applyPendingCancel();
4737
+ return true;
4738
+ },
4739
+ setSessionMode: async (modeId, timeoutMs) => {
4740
+ await turnController.setSessionMode(modeId, timeoutMs);
4741
+ },
4742
+ setSessionConfigOption: async (configId, value, timeoutMs) => {
4743
+ return await turnController.setSessionConfigOption(configId, value, timeoutMs);
4744
+ }
4290
4745
  }
4291
- );
4746
+ };
4292
4747
  }
4293
- async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
4294
- const owner = await readQueueOwnerRecord(sessionId);
4295
- if (!owner) {
4296
- return void 0;
4297
- }
4298
- if (!isProcessAlive(owner.pid)) {
4299
- await cleanupStaleQueueOwner(sessionId, owner);
4300
- return void 0;
4301
- }
4302
- const response = await submitSetConfigOptionToQueueOwner(
4303
- owner,
4304
- configId,
4305
- value,
4306
- timeoutMs
4307
- );
4308
- if (response) {
4309
- if (verbose) {
4748
+ async function runQueueOwnerProcess(options, deps) {
4749
+ const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
4750
+ const lease = await tryAcquireQueueOwnerLease(options.sessionId);
4751
+ if (!lease) {
4752
+ if (options.verbose) {
4310
4753
  process.stderr.write(
4311
- `[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}
4754
+ `[acpx] queue owner already active for session ${options.sessionId}; skipping spawn
4312
4755
  `
4313
4756
  );
4314
4757
  }
4315
- return response;
4316
- }
4317
- if (!isProcessAlive(owner.pid)) {
4318
- await cleanupStaleQueueOwner(sessionId, owner);
4319
- return void 0;
4320
- }
4321
- throw new QueueConnectionError(
4322
- "Session queue owner is running but not accepting set_config_option requests",
4323
- {
4324
- detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4325
- origin: "queue",
4326
- retryable: true
4327
- }
4328
- );
4329
- }
4330
- async function terminateQueueOwnerForSession(sessionId) {
4331
- const owner = await readQueueOwnerRecord(sessionId);
4332
- if (!owner) {
4333
4758
  return;
4334
4759
  }
4335
- if (isProcessAlive(owner.pid)) {
4336
- await terminateProcess(owner.pid);
4760
+ const runtime = createQueueOwnerTurnRuntime(options, deps);
4761
+ let owner;
4762
+ let heartbeatTimer;
4763
+ const refreshHeartbeat = async () => {
4764
+ if (!owner) {
4765
+ return;
4766
+ }
4767
+ await refreshQueueOwnerLease(lease, {
4768
+ queueDepth: owner.queueDepth()
4769
+ }).catch((error) => {
4770
+ if (options.verbose) {
4771
+ process.stderr.write(
4772
+ `[acpx] queue owner heartbeat update failed: ${formatErrorMessage(error)}
4773
+ `
4774
+ );
4775
+ }
4776
+ });
4777
+ };
4778
+ try {
4779
+ owner = await SessionQueueOwner.start(lease, runtime.controlHandlers);
4780
+ await refreshHeartbeat();
4781
+ heartbeatTimer = setInterval(() => {
4782
+ void refreshHeartbeat();
4783
+ }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
4784
+ heartbeatTimer.unref();
4785
+ const idleWaitMs = queueOwnerTtlMs === 0 ? void 0 : Math.max(0, queueOwnerTtlMs);
4786
+ while (true) {
4787
+ const task = await owner.nextTask(idleWaitMs);
4788
+ if (!task) {
4789
+ if (queueOwnerTtlMs > 0 && options.verbose) {
4790
+ process.stderr.write(
4791
+ `[acpx] queue owner TTL expired after ${Math.round(queueOwnerTtlMs / 1e3)}s for session ${options.sessionId}; shutting down
4792
+ `
4793
+ );
4794
+ }
4795
+ break;
4796
+ }
4797
+ await runtime.runPromptTurn(async () => {
4798
+ await deps.runQueuedTask(options.sessionId, task, {
4799
+ verbose: options.verbose,
4800
+ nonInteractivePermissions: options.nonInteractivePermissions,
4801
+ authCredentials: options.authCredentials,
4802
+ authPolicy: options.authPolicy,
4803
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
4804
+ onClientAvailable: runtime.onClientAvailable,
4805
+ onClientClosed: runtime.onClientClosed,
4806
+ onPromptActive: runtime.onPromptActive
4807
+ });
4808
+ });
4809
+ await refreshHeartbeat();
4810
+ }
4811
+ } finally {
4812
+ if (heartbeatTimer) {
4813
+ clearInterval(heartbeatTimer);
4814
+ }
4815
+ runtime.beginClosing();
4816
+ if (owner) {
4817
+ await owner.close();
4818
+ }
4819
+ await releaseQueueOwnerLease(lease);
4337
4820
  }
4338
- await cleanupStaleQueueOwner(sessionId, owner);
4339
4821
  }
4340
4822
 
4823
+ // src/session-owner-spawn.ts
4824
+ import { spawn as spawn3 } from "child_process";
4825
+
4341
4826
  // src/session-persistence.ts
4342
4827
  import { statSync } from "fs";
4343
4828
  import fs4 from "fs/promises";
4344
4829
  import os3 from "os";
4345
- import path5 from "path";
4830
+ import path6 from "path";
4346
4831
  var DEFAULT_HISTORY_LIMIT = 20;
4347
4832
  function sessionFilePath(id) {
4348
4833
  const safeId = encodeURIComponent(id);
4349
- return path5.join(sessionBaseDir(), `${safeId}.json`);
4834
+ return path6.join(sessionBaseDir(), `${safeId}.json`);
4350
4835
  }
4351
4836
  function sessionBaseDir() {
4352
- return path5.join(os3.homedir(), ".acpx", "sessions");
4837
+ return path6.join(os3.homedir(), ".acpx", "sessions");
4353
4838
  }
4354
4839
  async function ensureSessionDir() {
4355
4840
  await fs4.mkdir(sessionBaseDir(), { recursive: true });
@@ -4468,7 +4953,7 @@ async function resolveSessionRecord(sessionId) {
4468
4953
  throw new SessionNotFoundError(sessionId);
4469
4954
  }
4470
4955
  function hasGitDirectory(dir) {
4471
- const gitPath = path5.join(dir, ".git");
4956
+ const gitPath = path6.join(dir, ".git");
4472
4957
  try {
4473
4958
  return statSync(gitPath).isDirectory();
4474
4959
  } catch {
@@ -4476,15 +4961,15 @@ function hasGitDirectory(dir) {
4476
4961
  }
4477
4962
  }
4478
4963
  function isWithinBoundary(boundary, target) {
4479
- const relative = path5.relative(boundary, target);
4480
- return relative.length === 0 || !relative.startsWith("..") && !path5.isAbsolute(relative);
4964
+ const relative = path6.relative(boundary, target);
4965
+ return relative.length === 0 || !relative.startsWith("..") && !path6.isAbsolute(relative);
4481
4966
  }
4482
4967
  function absolutePath(value) {
4483
- return path5.resolve(value);
4968
+ return path6.resolve(value);
4484
4969
  }
4485
4970
  function findGitRepositoryRoot(startDir) {
4486
4971
  let current = absolutePath(startDir);
4487
- const root = path5.parse(current).root;
4972
+ const root = path6.parse(current).root;
4488
4973
  for (; ; ) {
4489
4974
  if (hasGitDirectory(current)) {
4490
4975
  return current;
@@ -4492,7 +4977,7 @@ function findGitRepositoryRoot(startDir) {
4492
4977
  if (current === root) {
4493
4978
  return void 0;
4494
4979
  }
4495
- const parent = path5.dirname(current);
4980
+ const parent = path6.dirname(current);
4496
4981
  if (parent === current) {
4497
4982
  return void 0;
4498
4983
  }
@@ -4517,7 +5002,7 @@ async function listSessions() {
4517
5002
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
4518
5003
  continue;
4519
5004
  }
4520
- const fullPath = path5.join(sessionBaseDir(), entry.name);
5005
+ const fullPath = path6.join(sessionBaseDir(), entry.name);
4521
5006
  try {
4522
5007
  const payload = await fs4.readFile(fullPath, "utf8");
4523
5008
  const parsed = parseSessionRecord(JSON.parse(payload));
@@ -4578,7 +5063,7 @@ async function findSessionByDirectoryWalk(options) {
4578
5063
  if (dir === walkBoundary) {
4579
5064
  return void 0;
4580
5065
  }
4581
- const parent = path5.dirname(dir);
5066
+ const parent = path6.dirname(dir);
4582
5067
  if (parent === dir) {
4583
5068
  return void 0;
4584
5069
  }
@@ -4586,6 +5071,124 @@ async function findSessionByDirectoryWalk(options) {
4586
5071
  }
4587
5072
  }
4588
5073
 
5074
+ // src/session-owner-spawn.ts
5075
+ var QUEUE_OWNER_STARTUP_TIMEOUT_MS = 1e4;
5076
+ var QUEUE_OWNER_RESPAWN_BACKOFF_MS = 250;
5077
+ function isQueueNotAcceptingError(error) {
5078
+ return error instanceof QueueConnectionError && error.detailCode === "QUEUE_NOT_ACCEPTING_REQUESTS";
5079
+ }
5080
+ function spawnDetachedQueueOwner(ownerSpawn) {
5081
+ const child = spawn3(ownerSpawn.command, ownerSpawn.args, {
5082
+ cwd: ownerSpawn.cwd,
5083
+ env: ownerSpawn.env,
5084
+ stdio: "ignore",
5085
+ detached: true
5086
+ });
5087
+ child.unref();
5088
+ }
5089
+ async function buildDefaultQueueOwnerSpawn(options, queueOwnerTtlMs) {
5090
+ const entrypoint = process.argv[1];
5091
+ if (!entrypoint) {
5092
+ throw new Error("Cannot spawn queue owner process: CLI entrypoint is missing");
5093
+ }
5094
+ const record = await resolveSessionRecord(options.sessionId);
5095
+ const args = [
5096
+ entrypoint,
5097
+ "__queue-owner",
5098
+ "--session-id",
5099
+ options.sessionId,
5100
+ "--ttl-ms",
5101
+ String(queueOwnerTtlMs),
5102
+ "--permission-mode",
5103
+ options.permissionMode
5104
+ ];
5105
+ if (options.nonInteractivePermissions) {
5106
+ args.push("--non-interactive-permissions", options.nonInteractivePermissions);
5107
+ }
5108
+ if (options.authPolicy) {
5109
+ args.push("--auth-policy", options.authPolicy);
5110
+ }
5111
+ if (options.timeoutMs != null && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
5112
+ args.push("--timeout-ms", String(Math.round(options.timeoutMs)));
5113
+ }
5114
+ if (options.verbose) {
5115
+ args.push("--verbose");
5116
+ }
5117
+ if (options.suppressSdkConsoleErrors) {
5118
+ args.push("--suppress-sdk-console-errors");
5119
+ }
5120
+ return {
5121
+ command: process.execPath,
5122
+ args,
5123
+ cwd: absolutePath(record.cwd)
5124
+ };
5125
+ }
5126
+ async function sendViaDetachedQueueOwner(options) {
5127
+ const waitForCompletion = options.waitForCompletion !== false;
5128
+ const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
5129
+ const ownerSpawn = options.queueOwnerSpawn ?? await buildDefaultQueueOwnerSpawn(options, queueOwnerTtlMs);
5130
+ const startupDeadline = Date.now() + QUEUE_OWNER_STARTUP_TIMEOUT_MS;
5131
+ let lastSpawnAttemptAt = 0;
5132
+ for (; ; ) {
5133
+ try {
5134
+ const queuedToOwner = await trySubmitToRunningOwner({
5135
+ sessionId: options.sessionId,
5136
+ message: options.message,
5137
+ permissionMode: options.permissionMode,
5138
+ nonInteractivePermissions: options.nonInteractivePermissions,
5139
+ outputFormatter: options.outputFormatter,
5140
+ errorEmissionPolicy: options.errorEmissionPolicy,
5141
+ timeoutMs: options.timeoutMs,
5142
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5143
+ waitForCompletion,
5144
+ verbose: options.verbose
5145
+ });
5146
+ if (queuedToOwner) {
5147
+ return queuedToOwner;
5148
+ }
5149
+ } catch (error) {
5150
+ if (!isQueueNotAcceptingError(error)) {
5151
+ throw error;
5152
+ }
5153
+ if (Date.now() >= startupDeadline) {
5154
+ throw new QueueConnectionError(
5155
+ "Timed out waiting for detached queue owner to accept prompt requests",
5156
+ {
5157
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
5158
+ origin: "queue",
5159
+ retryable: true,
5160
+ cause: error instanceof Error ? error : void 0
5161
+ }
5162
+ );
5163
+ }
5164
+ await waitMs2(QUEUE_CONNECT_RETRY_MS);
5165
+ continue;
5166
+ }
5167
+ const now = Date.now();
5168
+ if (now >= startupDeadline) {
5169
+ throw new QueueConnectionError(
5170
+ "Timed out waiting for detached queue owner to start",
5171
+ {
5172
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
5173
+ origin: "queue",
5174
+ retryable: true
5175
+ }
5176
+ );
5177
+ }
5178
+ if (now - lastSpawnAttemptAt >= QUEUE_OWNER_RESPAWN_BACKOFF_MS) {
5179
+ spawnDetachedQueueOwner(ownerSpawn);
5180
+ lastSpawnAttemptAt = now;
5181
+ if (options.verbose) {
5182
+ process.stderr.write(
5183
+ `[acpx] starting detached queue owner for session ${options.sessionId}
5184
+ `
5185
+ );
5186
+ }
5187
+ }
5188
+ await waitMs2(QUEUE_CONNECT_RETRY_MS);
5189
+ }
5190
+ }
5191
+
4589
5192
  // src/session-runtime-history.ts
4590
5193
  var SESSION_HISTORY_MAX_ENTRIES = 500;
4591
5194
  var SESSION_HISTORY_PREVIEW_CHARS = 220;
@@ -4774,7 +5377,6 @@ async function connectAndLoadSession(options) {
4774
5377
  }
4775
5378
 
4776
5379
  // src/session-runtime.ts
4777
- var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
4778
5380
  var INTERRUPT_CANCEL_WAIT_MS = 2500;
4779
5381
  var TimeoutError = class extends Error {
4780
5382
  constructor(timeoutMs) {
@@ -4902,15 +5504,6 @@ var DISCARD_OUTPUT_FORMATTER = {
4902
5504
  flush() {
4903
5505
  }
4904
5506
  };
4905
- function normalizeQueueOwnerTtlMs(ttlMs) {
4906
- if (ttlMs == null) {
4907
- return DEFAULT_QUEUE_OWNER_TTL_MS;
4908
- }
4909
- if (!Number.isFinite(ttlMs) || ttlMs < 0) {
4910
- return DEFAULT_QUEUE_OWNER_TTL_MS;
4911
- }
4912
- return Math.round(ttlMs);
4913
- }
4914
5507
  function shouldFallbackToNewSession(error) {
4915
5508
  if (error instanceof TimeoutError || error instanceof InterruptedError) {
4916
5509
  return false;
@@ -5376,179 +5969,41 @@ async function ensureSession(options) {
5376
5969
  created: true
5377
5970
  };
5378
5971
  }
5379
- async function sendSession(options) {
5380
- const waitForCompletion = options.waitForCompletion !== false;
5381
- const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
5382
- const queuedToOwner = await trySubmitToRunningOwner({
5383
- sessionId: options.sessionId,
5384
- message: options.message,
5385
- permissionMode: options.permissionMode,
5386
- nonInteractivePermissions: options.nonInteractivePermissions,
5387
- outputFormatter: options.outputFormatter,
5388
- errorEmissionPolicy: options.errorEmissionPolicy,
5389
- timeoutMs: options.timeoutMs,
5390
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5391
- waitForCompletion,
5392
- verbose: options.verbose
5393
- });
5394
- if (queuedToOwner) {
5395
- return queuedToOwner;
5396
- }
5397
- for (; ; ) {
5398
- const lease = await tryAcquireQueueOwnerLease(options.sessionId);
5399
- if (!lease) {
5400
- const retryQueued = await trySubmitToRunningOwner({
5401
- sessionId: options.sessionId,
5402
- message: options.message,
5403
- permissionMode: options.permissionMode,
5972
+ async function runQueueOwnerProcess2(options) {
5973
+ await runQueueOwnerProcess(options, {
5974
+ runQueuedTask,
5975
+ withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
5976
+ setSessionModeFallback: async (modeId, timeoutMs) => {
5977
+ await runSessionSetModeDirect({
5978
+ sessionRecordId: options.sessionId,
5979
+ modeId,
5404
5980
  nonInteractivePermissions: options.nonInteractivePermissions,
5405
- outputFormatter: options.outputFormatter,
5406
- errorEmissionPolicy: options.errorEmissionPolicy,
5407
- timeoutMs: options.timeoutMs,
5408
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5409
- waitForCompletion,
5981
+ authCredentials: options.authCredentials,
5982
+ authPolicy: options.authPolicy,
5983
+ timeoutMs,
5410
5984
  verbose: options.verbose
5411
5985
  });
5412
- if (retryQueued) {
5413
- return retryQueued;
5414
- }
5415
- await waitMs2(QUEUE_CONNECT_RETRY_MS);
5416
- continue;
5417
- }
5418
- let owner;
5419
- const turnController = new QueueOwnerTurnController({
5420
- withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
5421
- setSessionModeFallback: async (modeId, timeoutMs) => {
5422
- await runSessionSetModeDirect({
5423
- sessionRecordId: options.sessionId,
5424
- modeId,
5425
- nonInteractivePermissions: options.nonInteractivePermissions,
5426
- authCredentials: options.authCredentials,
5427
- authPolicy: options.authPolicy,
5428
- timeoutMs,
5429
- verbose: options.verbose
5430
- });
5431
- },
5432
- setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
5433
- const result = await runSessionSetConfigOptionDirect({
5434
- sessionRecordId: options.sessionId,
5435
- configId,
5436
- value,
5437
- nonInteractivePermissions: options.nonInteractivePermissions,
5438
- authCredentials: options.authCredentials,
5439
- authPolicy: options.authPolicy,
5440
- timeoutMs,
5441
- verbose: options.verbose
5442
- });
5443
- return result.response;
5444
- }
5445
- });
5446
- const applyPendingCancel = async () => {
5447
- return await turnController.applyPendingCancel();
5448
- };
5449
- const scheduleApplyPendingCancel = () => {
5450
- void applyPendingCancel().catch((error) => {
5451
- if (options.verbose) {
5452
- process.stderr.write(
5453
- `[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}
5454
- `
5455
- );
5456
- }
5457
- });
5458
- };
5459
- const setActiveController = (controller) => {
5460
- turnController.setActiveController(controller);
5461
- scheduleApplyPendingCancel();
5462
- };
5463
- const clearActiveController = () => {
5464
- turnController.clearActiveController();
5465
- };
5466
- const runPromptTurn = async (run) => {
5467
- turnController.beginTurn();
5468
- try {
5469
- return await run();
5470
- } finally {
5471
- turnController.endTurn();
5472
- }
5473
- };
5474
- try {
5475
- owner = await SessionQueueOwner.start(lease, {
5476
- cancelPrompt: async () => {
5477
- const accepted = await turnController.requestCancel();
5478
- if (!accepted) {
5479
- return false;
5480
- }
5481
- await applyPendingCancel();
5482
- return true;
5483
- },
5484
- setSessionMode: async (modeId, timeoutMs) => {
5485
- await turnController.setSessionMode(modeId, timeoutMs);
5486
- },
5487
- setSessionConfigOption: async (configId, value, timeoutMs) => {
5488
- return await turnController.setSessionConfigOption(
5489
- configId,
5490
- value,
5491
- timeoutMs
5492
- );
5493
- }
5494
- });
5495
- const localResult = await runPromptTurn(async () => {
5496
- return await runSessionPrompt({
5497
- sessionRecordId: options.sessionId,
5498
- message: options.message,
5499
- permissionMode: options.permissionMode,
5500
- nonInteractivePermissions: options.nonInteractivePermissions,
5501
- authCredentials: options.authCredentials,
5502
- authPolicy: options.authPolicy,
5503
- outputFormatter: options.outputFormatter,
5504
- timeoutMs: options.timeoutMs,
5505
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5506
- verbose: options.verbose,
5507
- onClientAvailable: setActiveController,
5508
- onClientClosed: clearActiveController,
5509
- onPromptActive: async () => {
5510
- turnController.markPromptActive();
5511
- await applyPendingCancel();
5512
- }
5513
- });
5986
+ },
5987
+ setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
5988
+ const result = await runSessionSetConfigOptionDirect({
5989
+ sessionRecordId: options.sessionId,
5990
+ configId,
5991
+ value,
5992
+ nonInteractivePermissions: options.nonInteractivePermissions,
5993
+ authCredentials: options.authCredentials,
5994
+ authPolicy: options.authPolicy,
5995
+ timeoutMs,
5996
+ verbose: options.verbose
5514
5997
  });
5515
- const idleWaitMs = queueOwnerTtlMs === 0 ? void 0 : Math.max(0, queueOwnerTtlMs);
5516
- while (true) {
5517
- const task = await owner.nextTask(idleWaitMs);
5518
- if (!task) {
5519
- if (queueOwnerTtlMs > 0 && options.verbose) {
5520
- process.stderr.write(
5521
- `[acpx] queue owner TTL expired after ${Math.round(queueOwnerTtlMs / 1e3)}s for session ${options.sessionId}; shutting down
5522
- `
5523
- );
5524
- }
5525
- break;
5526
- }
5527
- await runPromptTurn(async () => {
5528
- await runQueuedTask(options.sessionId, task, {
5529
- verbose: options.verbose,
5530
- nonInteractivePermissions: options.nonInteractivePermissions,
5531
- authCredentials: options.authCredentials,
5532
- authPolicy: options.authPolicy,
5533
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5534
- onClientAvailable: setActiveController,
5535
- onClientClosed: clearActiveController,
5536
- onPromptActive: async () => {
5537
- turnController.markPromptActive();
5538
- await applyPendingCancel();
5539
- }
5540
- });
5541
- });
5542
- }
5543
- return localResult;
5544
- } finally {
5545
- turnController.beginClosing();
5546
- if (owner) {
5547
- await owner.close();
5548
- }
5549
- await releaseQueueOwnerLease(lease);
5998
+ return result.response;
5550
5999
  }
5551
- }
6000
+ });
6001
+ }
6002
+ async function sendSession(options) {
6003
+ return await sendViaDetachedQueueOwner(options);
6004
+ }
6005
+ async function readSessionQueueOwnerStatus(sessionId) {
6006
+ return await readQueueOwnerStatus(sessionId);
5552
6007
  }
5553
6008
  async function cancelSessionPrompt(options) {
5554
6009
  const cancelled2 = await tryCancelOnRunningOwner(options);
@@ -5626,9 +6081,9 @@ async function isLikelyMatchingProcess(pid, agentCommand) {
5626
6081
  if (argv.length === 0) {
5627
6082
  return false;
5628
6083
  }
5629
- const executableBase = path6.basename(argv[0]);
5630
- const expectedBase = path6.basename(expectedToken);
5631
- return executableBase === expectedBase || argv.some((entry) => path6.basename(entry) === expectedBase);
6084
+ const executableBase = path7.basename(argv[0]);
6085
+ const expectedBase = path7.basename(expectedToken);
6086
+ return executableBase === expectedBase || argv.some((entry) => path7.basename(entry) === expectedBase);
5632
6087
  } catch {
5633
6088
  return true;
5634
6089
  }
@@ -5654,6 +6109,7 @@ var NoSessionError = class extends Error {
5654
6109
  }
5655
6110
  };
5656
6111
  var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set([
6112
+ "__queue-owner",
5657
6113
  "prompt",
5658
6114
  "exec",
5659
6115
  "cancel",
@@ -5666,25 +6122,25 @@ var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set([
5666
6122
  ]);
5667
6123
  function parseOutputFormat2(value) {
5668
6124
  if (!OUTPUT_FORMATS.includes(value)) {
5669
- throw new InvalidArgumentError(
6125
+ throw new InvalidArgumentError3(
5670
6126
  `Invalid format "${value}". Expected one of: ${OUTPUT_FORMATS.join(", ")}`
5671
6127
  );
5672
6128
  }
5673
6129
  return value;
5674
6130
  }
5675
- function parseAuthPolicy2(value) {
6131
+ function parseAuthPolicy3(value) {
5676
6132
  if (!AUTH_POLICIES.includes(value)) {
5677
- throw new InvalidArgumentError(
6133
+ throw new InvalidArgumentError3(
5678
6134
  `Invalid auth policy "${value}". Expected one of: ${AUTH_POLICIES.join(", ")}`
5679
6135
  );
5680
6136
  }
5681
6137
  return value;
5682
6138
  }
5683
- function parseNonInteractivePermissionPolicy2(value) {
6139
+ function parseNonInteractivePermissionPolicy3(value) {
5684
6140
  if (!NON_INTERACTIVE_PERMISSION_POLICIES.includes(
5685
6141
  value
5686
6142
  )) {
5687
- throw new InvalidArgumentError(
6143
+ throw new InvalidArgumentError3(
5688
6144
  `Invalid non-interactive permission policy "${value}". Expected one of: ${NON_INTERACTIVE_PERMISSION_POLICIES.join(", ")}`
5689
6145
  );
5690
6146
  }
@@ -5693,35 +6149,35 @@ function parseNonInteractivePermissionPolicy2(value) {
5693
6149
  function parseTimeoutSeconds(value) {
5694
6150
  const parsed = Number(value);
5695
6151
  if (!Number.isFinite(parsed) || parsed <= 0) {
5696
- throw new InvalidArgumentError("Timeout must be a positive number of seconds");
6152
+ throw new InvalidArgumentError3("Timeout must be a positive number of seconds");
5697
6153
  }
5698
6154
  return Math.round(parsed * 1e3);
5699
6155
  }
5700
6156
  function parseTtlSeconds(value) {
5701
6157
  const parsed = Number(value);
5702
6158
  if (!Number.isFinite(parsed) || parsed < 0) {
5703
- throw new InvalidArgumentError("TTL must be a non-negative number of seconds");
6159
+ throw new InvalidArgumentError3("TTL must be a non-negative number of seconds");
5704
6160
  }
5705
6161
  return Math.round(parsed * 1e3);
5706
6162
  }
5707
6163
  function parseSessionName(value) {
5708
6164
  const trimmed = value.trim();
5709
6165
  if (trimmed.length === 0) {
5710
- throw new InvalidArgumentError("Session name must not be empty");
6166
+ throw new InvalidArgumentError3("Session name must not be empty");
5711
6167
  }
5712
6168
  return trimmed;
5713
6169
  }
5714
- function parseNonEmptyValue(label, value) {
6170
+ function parseNonEmptyValue2(label, value) {
5715
6171
  const trimmed = value.trim();
5716
6172
  if (trimmed.length === 0) {
5717
- throw new InvalidArgumentError(`${label} must not be empty`);
6173
+ throw new InvalidArgumentError3(`${label} must not be empty`);
5718
6174
  }
5719
6175
  return trimmed;
5720
6176
  }
5721
6177
  function parseHistoryLimit(value) {
5722
6178
  const parsed = Number(value);
5723
6179
  if (!Number.isInteger(parsed) || parsed <= 0) {
5724
- throw new InvalidArgumentError("Limit must be a positive integer");
6180
+ throw new InvalidArgumentError3("Limit must be a positive integer");
5725
6181
  }
5726
6182
  return parsed;
5727
6183
  }
@@ -5730,7 +6186,7 @@ function resolvePermissionMode(flags, defaultMode) {
5730
6186
  Boolean
5731
6187
  ).length;
5732
6188
  if (selected2 > 1) {
5733
- throw new InvalidArgumentError(
6189
+ throw new InvalidArgumentError3(
5734
6190
  "Use only one permission mode: --approve-all, --approve-reads, or --deny-all"
5735
6191
  );
5736
6192
  }
@@ -5751,13 +6207,13 @@ async function readPromptInputFromStdin() {
5751
6207
  }
5752
6208
  async function readPrompt(promptParts, filePath, cwd) {
5753
6209
  if (filePath) {
5754
- const source = filePath === "-" ? await readPromptInputFromStdin() : await fs6.readFile(path7.resolve(cwd, filePath), "utf8");
6210
+ const source = filePath === "-" ? await readPromptInputFromStdin() : await fs6.readFile(path8.resolve(cwd, filePath), "utf8");
5755
6211
  const pieces = [source.trim(), promptParts.join(" ").trim()].filter(
5756
6212
  (value) => value.length > 0
5757
6213
  );
5758
6214
  const prompt2 = pieces.join("\n\n").trim();
5759
6215
  if (!prompt2) {
5760
- throw new InvalidArgumentError("Prompt from --file is empty");
6216
+ throw new InvalidArgumentError3("Prompt from --file is empty");
5761
6217
  }
5762
6218
  return prompt2;
5763
6219
  }
@@ -5766,13 +6222,13 @@ async function readPrompt(promptParts, filePath, cwd) {
5766
6222
  return joined;
5767
6223
  }
5768
6224
  if (process.stdin.isTTY) {
5769
- throw new InvalidArgumentError(
6225
+ throw new InvalidArgumentError3(
5770
6226
  "Prompt is required (pass as argument, --file, or pipe via stdin)"
5771
6227
  );
5772
6228
  }
5773
6229
  const prompt = (await readPromptInputFromStdin()).trim();
5774
6230
  if (!prompt) {
5775
- throw new InvalidArgumentError("Prompt from stdin is empty");
6231
+ throw new InvalidArgumentError3("Prompt from stdin is empty");
5776
6232
  }
5777
6233
  return prompt;
5778
6234
  }
@@ -5787,14 +6243,14 @@ function addGlobalFlags(command) {
5787
6243
  return command.option("--agent <command>", "Raw ACP agent command (escape hatch)").option("--cwd <dir>", "Working directory", process.cwd()).option(
5788
6244
  "--auth-policy <policy>",
5789
6245
  "Authentication policy: skip or fail when auth is required",
5790
- parseAuthPolicy2
6246
+ parseAuthPolicy3
5791
6247
  ).option("--approve-all", "Auto-approve all permission requests").option(
5792
6248
  "--approve-reads",
5793
6249
  "Auto-approve read/search requests and prompt for writes"
5794
6250
  ).option("--deny-all", "Deny all permission requests").option(
5795
6251
  "--non-interactive-permissions <policy>",
5796
6252
  "When prompting is unavailable: deny or fail",
5797
- parseNonInteractivePermissionPolicy2
6253
+ parseNonInteractivePermissionPolicy3
5798
6254
  ).option("--format <fmt>", "Output format: text, json, quiet", parseOutputFormat2).option(
5799
6255
  "--json-strict",
5800
6256
  "Strict JSON mode: requires --format json and suppresses non-JSON stderr output"
@@ -5851,10 +6307,10 @@ function resolveGlobalFlags(command, config) {
5851
6307
  const jsonStrict = opts.jsonStrict === true;
5852
6308
  const verbose = opts.verbose === true;
5853
6309
  if (jsonStrict && format !== "json") {
5854
- throw new InvalidArgumentError("--json-strict requires --format json");
6310
+ throw new InvalidArgumentError3("--json-strict requires --format json");
5855
6311
  }
5856
6312
  if (jsonStrict && verbose) {
5857
- throw new InvalidArgumentError("--json-strict cannot be combined with --verbose");
6313
+ throw new InvalidArgumentError3("--json-strict cannot be combined with --verbose");
5858
6314
  }
5859
6315
  return {
5860
6316
  agent: opts.agent,
@@ -5883,7 +6339,7 @@ function resolveOutputPolicy(format, jsonStrict) {
5883
6339
  function resolveAgentInvocation(explicitAgentName, globalFlags, config) {
5884
6340
  const override = globalFlags.agent?.trim();
5885
6341
  if (override && explicitAgentName) {
5886
- throw new InvalidArgumentError(
6342
+ throw new InvalidArgumentError3(
5887
6343
  "Do not combine positional agent with --agent override"
5888
6344
  );
5889
6345
  }
@@ -5892,7 +6348,7 @@ function resolveAgentInvocation(explicitAgentName, globalFlags, config) {
5892
6348
  return {
5893
6349
  agentName,
5894
6350
  agentCommand,
5895
- cwd: path7.resolve(globalFlags.cwd)
6351
+ cwd: path8.resolve(globalFlags.cwd)
5896
6352
  };
5897
6353
  }
5898
6354
  function printSessionsByFormat(sessions, format) {
@@ -6031,19 +6487,19 @@ function formatSessionLabel(record) {
6031
6487
  return record.name ?? "cwd";
6032
6488
  }
6033
6489
  function formatRoutedFrom(sessionCwd, currentCwd) {
6034
- const relative = path7.relative(sessionCwd, currentCwd);
6490
+ const relative = path8.relative(sessionCwd, currentCwd);
6035
6491
  if (!relative || relative === ".") {
6036
6492
  return void 0;
6037
6493
  }
6038
- return relative.startsWith(".") ? relative : `.${path7.sep}${relative}`;
6494
+ return relative.startsWith(".") ? relative : `.${path8.sep}${relative}`;
6039
6495
  }
6040
6496
  function sessionConnectionStatus(record) {
6041
6497
  return isProcessAlive(record.pid) ? "connected" : "needs reconnect";
6042
6498
  }
6043
6499
  function formatPromptSessionBannerLine(record, currentCwd) {
6044
6500
  const label = formatSessionLabel(record);
6045
- const normalizedSessionCwd = path7.resolve(record.cwd);
6046
- const normalizedCurrentCwd = path7.resolve(currentCwd);
6501
+ const normalizedSessionCwd = path8.resolve(record.cwd);
6502
+ const normalizedCurrentCwd = path8.resolve(currentCwd);
6047
6503
  const routedFrom = normalizedSessionCwd === normalizedCurrentCwd ? void 0 : formatRoutedFrom(normalizedSessionCwd, normalizedCurrentCwd);
6048
6504
  const status = sessionConnectionStatus(record);
6049
6505
  if (routedFrom) {
@@ -6561,7 +7017,12 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6561
7017
  uptime: null,
6562
7018
  lastPromptTime: null,
6563
7019
  exitCode: null,
6564
- signal: null
7020
+ signal: null,
7021
+ ownerPid: null,
7022
+ ownerStatus: null,
7023
+ ownerGeneration: null,
7024
+ ownerHeartbeatAt: null,
7025
+ ownerQueueDepth: null
6565
7026
  };
6566
7027
  if (globalFlags.format === "json") {
6567
7028
  process.stdout.write(`${JSON.stringify(payload2)}
@@ -6583,10 +7044,14 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6583
7044
  process.stdout.write(`uptime: -
6584
7045
  `);
6585
7046
  process.stdout.write(`lastPromptTime: -
7047
+ `);
7048
+ process.stdout.write(`ownerStatus: -
6586
7049
  `);
6587
7050
  return;
6588
7051
  }
6589
7052
  const running = isProcessAlive(record.pid);
7053
+ const owner = await readSessionQueueOwnerStatus(record.id);
7054
+ const ownerStatus = owner ? owner.stale ? "stale" : "active" : "inactive";
6590
7055
  const payload = {
6591
7056
  ...canonicalSessionIdentity(record),
6592
7057
  agentCommand: record.agentCommand,
@@ -6595,7 +7060,12 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6595
7060
  uptime: running ? formatUptime(record.agentStartedAt) ?? null : null,
6596
7061
  lastPromptTime: record.lastPromptAt ?? null,
6597
7062
  exitCode: running ? null : record.lastAgentExitCode ?? null,
6598
- signal: running ? null : record.lastAgentExitSignal ?? null
7063
+ signal: running ? null : record.lastAgentExitSignal ?? null,
7064
+ ownerPid: owner?.pid ?? null,
7065
+ ownerStatus,
7066
+ ownerGeneration: owner?.ownerGeneration ?? null,
7067
+ ownerHeartbeatAt: owner?.heartbeatAt ?? null,
7068
+ ownerQueueDepth: owner?.queueDepth ?? null
6599
7069
  };
6600
7070
  if (globalFlags.format === "json") {
6601
7071
  process.stdout.write(`${JSON.stringify(payload)}
@@ -6622,6 +7092,16 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6622
7092
  process.stdout.write(`uptime: ${payload.uptime ?? "-"}
6623
7093
  `);
6624
7094
  process.stdout.write(`lastPromptTime: ${payload.lastPromptTime ?? "-"}
7095
+ `);
7096
+ process.stdout.write(`ownerStatus: ${payload.ownerStatus}
7097
+ `);
7098
+ process.stdout.write(`ownerPid: ${payload.ownerPid ?? "-"}
7099
+ `);
7100
+ process.stdout.write(`ownerGeneration: ${payload.ownerGeneration ?? "-"}
7101
+ `);
7102
+ process.stdout.write(`ownerHeartbeatAt: ${payload.ownerHeartbeatAt ?? "-"}
7103
+ `);
7104
+ process.stdout.write(`ownerQueueDepth: ${payload.ownerQueueDepth ?? "-"}
6625
7105
  `);
6626
7106
  if (payload.status === "dead") {
6627
7107
  process.stdout.write(`exitCode: ${payload.exitCode ?? "-"}
@@ -6726,7 +7206,7 @@ function registerSharedAgentSubcommands(parent, explicitAgentName, config, descr
6726
7206
  const setModeCommand = parent.command("set-mode").description(descriptions.setMode).argument(
6727
7207
  "<mode>",
6728
7208
  "Mode id",
6729
- (value) => parseNonEmptyValue("Mode", value)
7209
+ (value) => parseNonEmptyValue2("Mode", value)
6730
7210
  );
6731
7211
  addSessionNameOption(setModeCommand);
6732
7212
  setModeCommand.action(async function(modeId, flags) {
@@ -6735,11 +7215,11 @@ function registerSharedAgentSubcommands(parent, explicitAgentName, config, descr
6735
7215
  const setConfigCommand = parent.command("set").description(descriptions.setConfig).argument(
6736
7216
  "<key>",
6737
7217
  "Config option id",
6738
- (value) => parseNonEmptyValue("Config option key", value)
7218
+ (value) => parseNonEmptyValue2("Config option key", value)
6739
7219
  ).argument(
6740
7220
  "<value>",
6741
7221
  "Config option value",
6742
- (value) => parseNonEmptyValue("Config option value", value)
7222
+ (value) => parseNonEmptyValue2("Config option value", value)
6743
7223
  );
6744
7224
  addSessionNameOption(setConfigCommand);
6745
7225
  setConfigCommand.action(async function(key, value, flags) {
@@ -6831,14 +7311,14 @@ function detectInitialCwd(argv) {
6831
7311
  if (token === "--cwd") {
6832
7312
  const next = argv[index + 1];
6833
7313
  if (next && next !== "--") {
6834
- return path7.resolve(next);
7314
+ return path8.resolve(next);
6835
7315
  }
6836
7316
  break;
6837
7317
  }
6838
7318
  if (token.startsWith("--cwd=")) {
6839
7319
  const value = token.slice("--cwd=".length).trim();
6840
7320
  if (value.length > 0) {
6841
- return path7.resolve(value);
7321
+ return path8.resolve(value);
6842
7322
  }
6843
7323
  break;
6844
7324
  }
@@ -6934,9 +7414,12 @@ async function main(argv = process.argv) {
6934
7414
  requestedOutputFormat,
6935
7415
  requestedJsonStrict
6936
7416
  );
6937
- const builtInAgents = listBuiltInAgents(config.agents);
7417
+ const internalQueueOwnerFlags = parseQueueOwnerFlags(
7418
+ argv.slice(2),
7419
+ DEFAULT_QUEUE_OWNER_TTL_MS
7420
+ );
6938
7421
  const program = new Command();
6939
- program.name("acpx").description("Headless CLI client for the Agent Client Protocol").enablePositionalOptions().showHelpAfterError();
7422
+ program.name("acpx").description("Headless CLI client for the Agent Client Protocol").version(getAcpxVersion()).enablePositionalOptions().showHelpAfterError();
6940
7423
  if (requestedJsonStrict) {
6941
7424
  program.configureOutput({
6942
7425
  writeOut: () => {
@@ -6946,56 +7429,39 @@ async function main(argv = process.argv) {
6946
7429
  });
6947
7430
  }
6948
7431
  addGlobalFlags(program);
6949
- for (const agentName of builtInAgents) {
6950
- registerAgentCommand(program, agentName, config);
6951
- }
6952
- registerDefaultCommands(program, config);
6953
- const scan = detectAgentToken(argv.slice(2));
6954
- if (!scan.hasAgentOverride && scan.token && !TOP_LEVEL_VERBS.has(scan.token) && !builtInAgents.includes(scan.token)) {
6955
- registerAgentCommand(program, scan.token, config);
6956
- }
6957
- program.argument("[prompt...]", "Prompt text").action(async function(promptParts) {
6958
- if (promptParts.length === 0 && process.stdin.isTTY) {
6959
- if (requestedJsonStrict) {
6960
- throw new InvalidArgumentError(
6961
- "Prompt is required (pass as argument, --file, or pipe via stdin)"
6962
- );
6963
- }
6964
- this.outputHelp();
6965
- return;
7432
+ configurePublicCli({
7433
+ program,
7434
+ argv: argv.slice(2),
7435
+ config,
7436
+ requestedJsonStrict,
7437
+ topLevelVerbs: TOP_LEVEL_VERBS,
7438
+ listBuiltInAgents,
7439
+ detectAgentToken,
7440
+ registerAgentCommand,
7441
+ registerDefaultCommands,
7442
+ handlePromptAction: async (command, promptParts) => {
7443
+ await handlePrompt(void 0, promptParts, {}, command, config);
6966
7444
  }
6967
- await handlePrompt(void 0, promptParts, {}, this, config);
6968
7445
  });
6969
- program.addHelpText(
6970
- "after",
6971
- `
6972
- Examples:
6973
- acpx codex sessions new
6974
- acpx codex "fix the tests"
6975
- acpx codex prompt "fix the tests"
6976
- acpx codex --no-wait "queue follow-up task"
6977
- acpx codex exec "what does this repo do"
6978
- acpx codex cancel
6979
- acpx codex set-mode plan
6980
- acpx codex set approval_policy conservative
6981
- acpx codex -s backend "fix the API"
6982
- acpx codex sessions
6983
- acpx codex sessions new --name backend
6984
- acpx codex sessions ensure --name backend
6985
- acpx codex sessions close backend
6986
- acpx codex status
6987
- acpx config show
6988
- acpx config init
6989
- acpx --ttl 30 codex "investigate flaky tests"
6990
- acpx claude "refactor auth"
6991
- acpx gemini "add logging"
6992
- acpx --agent ./my-custom-server "do something"`
6993
- );
6994
7446
  program.exitOverride((error) => {
6995
7447
  throw error;
6996
7448
  });
6997
7449
  await runWithOutputPolicy(requestedOutputPolicy, async () => {
6998
7450
  try {
7451
+ if (internalQueueOwnerFlags) {
7452
+ await runQueueOwnerProcess2({
7453
+ sessionId: internalQueueOwnerFlags.sessionId,
7454
+ ttlMs: internalQueueOwnerFlags.ttlMs,
7455
+ permissionMode: internalQueueOwnerFlags.permissionMode,
7456
+ nonInteractivePermissions: internalQueueOwnerFlags.nonInteractivePermissions,
7457
+ authCredentials: config.auth,
7458
+ authPolicy: internalQueueOwnerFlags.authPolicy,
7459
+ timeoutMs: internalQueueOwnerFlags.timeoutMs,
7460
+ suppressSdkConsoleErrors: internalQueueOwnerFlags.suppressSdkConsoleErrors,
7461
+ verbose: internalQueueOwnerFlags.verbose
7462
+ });
7463
+ return;
7464
+ }
6999
7465
  await program.parseAsync(argv);
7000
7466
  } catch (error) {
7001
7467
  if (error instanceof CommanderError) {