acpx 0.1.10 → 0.1.11

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 +1712 -1300
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
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
7
  import path7 from "path";
@@ -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) {
@@ -2914,115 +3108,270 @@ var AcpClient = class {
2914
3108
  }
2915
3109
  };
2916
3110
 
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;
2925
- }
2926
- get lifecycleState() {
2927
- return this.state;
2928
- }
2929
- get hasPendingCancel() {
2930
- return this.pendingCancel;
2931
- }
2932
- beginTurn() {
2933
- this.state = "starting";
2934
- this.pendingCancel = false;
3111
+ // src/queue-lease-store.ts
3112
+ import { createHash } from "crypto";
3113
+ import fs3 from "fs/promises";
3114
+ import os2 from "os";
3115
+ import path4 from "path";
3116
+ var PROCESS_EXIT_GRACE_MS = 1500;
3117
+ var PROCESS_POLL_MS = 50;
3118
+ var QUEUE_OWNER_STALE_HEARTBEAT_MS = 15e3;
3119
+ function queueBaseDir() {
3120
+ return path4.join(os2.homedir(), ".acpx", "queues");
3121
+ }
3122
+ function queueKeyForSession(sessionId) {
3123
+ return createHash("sha256").update(sessionId).digest("hex").slice(0, 24);
3124
+ }
3125
+ function queueLockFilePath(sessionId) {
3126
+ return path4.join(queueBaseDir(), `${queueKeyForSession(sessionId)}.lock`);
3127
+ }
3128
+ function queueSocketPath(sessionId) {
3129
+ const key = queueKeyForSession(sessionId);
3130
+ if (process.platform === "win32") {
3131
+ return `\\\\.\\pipe\\acpx-${key}`;
2935
3132
  }
2936
- markPromptActive() {
2937
- if (this.state === "starting" || this.state === "active") {
2938
- this.state = "active";
2939
- }
3133
+ return path4.join(queueBaseDir(), `${key}.sock`);
3134
+ }
3135
+ function parseQueueOwnerRecord(raw) {
3136
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3137
+ return null;
2940
3138
  }
2941
- endTurn() {
2942
- this.state = "idle";
2943
- this.pendingCancel = false;
3139
+ const record = raw;
3140
+ 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) {
3141
+ return null;
2944
3142
  }
2945
- beginClosing() {
2946
- this.state = "closing";
2947
- this.pendingCancel = false;
2948
- this.activeController = void 0;
3143
+ return {
3144
+ pid: record.pid,
3145
+ sessionId: record.sessionId,
3146
+ socketPath: record.socketPath,
3147
+ createdAt: record.createdAt,
3148
+ heartbeatAt: record.heartbeatAt,
3149
+ ownerGeneration: record.ownerGeneration,
3150
+ queueDepth: record.queueDepth
3151
+ };
3152
+ }
3153
+ function createOwnerGeneration() {
3154
+ return Date.now() * 1e3 + Math.floor(Math.random() * 1e3);
3155
+ }
3156
+ function nowIso4() {
3157
+ return (/* @__PURE__ */ new Date()).toISOString();
3158
+ }
3159
+ function isQueueOwnerHeartbeatStale(owner) {
3160
+ const heartbeatMs = Date.parse(owner.heartbeatAt);
3161
+ if (!Number.isFinite(heartbeatMs)) {
3162
+ return true;
2949
3163
  }
2950
- setActiveController(controller) {
2951
- this.activeController = controller;
3164
+ return Date.now() - heartbeatMs > QUEUE_OWNER_STALE_HEARTBEAT_MS;
3165
+ }
3166
+ async function ensureQueueDir() {
3167
+ await fs3.mkdir(queueBaseDir(), { recursive: true });
3168
+ }
3169
+ async function removeSocketFile(socketPath) {
3170
+ if (process.platform === "win32") {
3171
+ return;
2952
3172
  }
2953
- clearActiveController() {
2954
- this.activeController = void 0;
3173
+ try {
3174
+ await fs3.unlink(socketPath);
3175
+ } catch (error) {
3176
+ if (error.code !== "ENOENT") {
3177
+ throw error;
3178
+ }
2955
3179
  }
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
- });
3180
+ }
3181
+ async function waitForProcessExit(pid, timeoutMs) {
3182
+ const deadline = Date.now() + Math.max(0, timeoutMs);
3183
+ while (Date.now() <= deadline) {
3184
+ if (!isProcessAlive(pid)) {
3185
+ return true;
2963
3186
  }
3187
+ await waitMs2(PROCESS_POLL_MS);
2964
3188
  }
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;
3189
+ return !isProcessAlive(pid);
3190
+ }
3191
+ async function cleanupStaleQueueOwner(sessionId, owner) {
3192
+ const lockPath = queueLockFilePath(sessionId);
3193
+ const socketPath = owner?.socketPath ?? queueSocketPath(sessionId);
3194
+ await removeSocketFile(socketPath).catch(() => {
3195
+ });
3196
+ await fs3.unlink(lockPath).catch((error) => {
3197
+ if (error.code !== "ENOENT") {
3198
+ throw error;
2977
3199
  }
3200
+ });
3201
+ }
3202
+ async function readQueueOwnerRecord(sessionId) {
3203
+ const lockPath = queueLockFilePath(sessionId);
3204
+ try {
3205
+ const payload = await fs3.readFile(lockPath, "utf8");
3206
+ const parsed = parseQueueOwnerRecord(JSON.parse(payload));
3207
+ return parsed ?? void 0;
3208
+ } catch {
3209
+ return void 0;
3210
+ }
3211
+ }
3212
+ function isProcessAlive(pid) {
3213
+ if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) {
2978
3214
  return false;
2979
3215
  }
2980
- async applyPendingCancel() {
2981
- const activeController = this.activeController;
2982
- if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) {
2983
- return false;
3216
+ try {
3217
+ process.kill(pid, 0);
3218
+ return true;
3219
+ } catch {
3220
+ return false;
3221
+ }
3222
+ }
3223
+ async function terminateProcess(pid) {
3224
+ if (!isProcessAlive(pid)) {
3225
+ return false;
3226
+ }
3227
+ try {
3228
+ process.kill(pid, "SIGTERM");
3229
+ } catch {
3230
+ return false;
3231
+ }
3232
+ if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) {
3233
+ return true;
3234
+ }
3235
+ try {
3236
+ process.kill(pid, "SIGKILL");
3237
+ } catch {
3238
+ return false;
3239
+ }
3240
+ await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
3241
+ return true;
3242
+ }
3243
+ async function ensureOwnerIsUsable(sessionId, owner) {
3244
+ const alive = isProcessAlive(owner.pid);
3245
+ const stale = isQueueOwnerHeartbeatStale(owner);
3246
+ if (alive && !stale) {
3247
+ return true;
3248
+ }
3249
+ if (alive) {
3250
+ await terminateProcess(owner.pid).catch(() => {
3251
+ });
3252
+ }
3253
+ await cleanupStaleQueueOwner(sessionId, owner);
3254
+ return false;
3255
+ }
3256
+ async function readQueueOwnerStatus(sessionId) {
3257
+ const owner = await readQueueOwnerRecord(sessionId);
3258
+ if (!owner) {
3259
+ return void 0;
3260
+ }
3261
+ const alive = await ensureOwnerIsUsable(sessionId, owner);
3262
+ if (!alive) {
3263
+ return void 0;
3264
+ }
3265
+ return {
3266
+ pid: owner.pid,
3267
+ socketPath: owner.socketPath,
3268
+ heartbeatAt: owner.heartbeatAt,
3269
+ ownerGeneration: owner.ownerGeneration,
3270
+ queueDepth: owner.queueDepth,
3271
+ alive,
3272
+ stale: isQueueOwnerHeartbeatStale(owner)
3273
+ };
3274
+ }
3275
+ async function tryAcquireQueueOwnerLease(sessionId, nowIsoFactory = nowIso4) {
3276
+ await ensureQueueDir();
3277
+ const lockPath = queueLockFilePath(sessionId);
3278
+ const socketPath = queueSocketPath(sessionId);
3279
+ const createdAt = nowIsoFactory();
3280
+ const ownerGeneration = createOwnerGeneration();
3281
+ const payload = JSON.stringify(
3282
+ {
3283
+ pid: process.pid,
3284
+ sessionId,
3285
+ socketPath,
3286
+ createdAt,
3287
+ heartbeatAt: createdAt,
3288
+ ownerGeneration,
3289
+ queueDepth: 0
3290
+ },
3291
+ null,
3292
+ 2
3293
+ );
3294
+ try {
3295
+ await fs3.writeFile(lockPath, `${payload}
3296
+ `, {
3297
+ encoding: "utf8",
3298
+ flag: "wx"
3299
+ });
3300
+ await removeSocketFile(socketPath).catch(() => {
3301
+ });
3302
+ return {
3303
+ sessionId,
3304
+ lockPath,
3305
+ socketPath,
3306
+ createdAt,
3307
+ ownerGeneration
3308
+ };
3309
+ } catch (error) {
3310
+ if (error.code !== "EEXIST") {
3311
+ throw error;
2984
3312
  }
2985
- const cancelled2 = await activeController.requestCancelActivePrompt();
2986
- if (cancelled2) {
2987
- this.pendingCancel = false;
3313
+ const owner = await readQueueOwnerRecord(sessionId);
3314
+ if (!owner) {
3315
+ await cleanupStaleQueueOwner(sessionId, owner);
3316
+ return void 0;
2988
3317
  }
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;
3318
+ if (!isProcessAlive(owner.pid) || isQueueOwnerHeartbeatStale(owner)) {
3319
+ if (isProcessAlive(owner.pid)) {
3320
+ await terminateProcess(owner.pid).catch(() => {
3321
+ });
3322
+ }
3323
+ await cleanupStaleQueueOwner(sessionId, owner);
3000
3324
  }
3001
- await this.options.setSessionModeFallback(modeId, timeoutMs);
3325
+ return void 0;
3002
3326
  }
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
- );
3327
+ }
3328
+ async function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso4) {
3329
+ const payload = JSON.stringify(
3330
+ {
3331
+ pid: process.pid,
3332
+ sessionId: lease.sessionId,
3333
+ socketPath: lease.socketPath,
3334
+ createdAt: lease.createdAt,
3335
+ heartbeatAt: nowIsoFactory(),
3336
+ ownerGeneration: lease.ownerGeneration,
3337
+ queueDepth: Math.max(0, Math.round(options.queueDepth))
3338
+ },
3339
+ null,
3340
+ 2
3341
+ );
3342
+ await fs3.writeFile(lease.lockPath, `${payload}
3343
+ `, {
3344
+ encoding: "utf8"
3345
+ });
3346
+ }
3347
+ async function releaseQueueOwnerLease(lease) {
3348
+ await removeSocketFile(lease.socketPath).catch(() => {
3349
+ });
3350
+ await fs3.unlink(lease.lockPath).catch((error) => {
3351
+ if (error.code !== "ENOENT") {
3352
+ throw error;
3011
3353
  }
3012
- return await this.options.setSessionConfigOptionFallback(
3013
- configId,
3014
- value,
3015
- timeoutMs
3016
- );
3354
+ });
3355
+ }
3356
+ async function terminateQueueOwnerForSession(sessionId) {
3357
+ const owner = await readQueueOwnerRecord(sessionId);
3358
+ if (!owner) {
3359
+ return;
3017
3360
  }
3018
- };
3361
+ if (isProcessAlive(owner.pid)) {
3362
+ await terminateProcess(owner.pid);
3363
+ }
3364
+ await cleanupStaleQueueOwner(sessionId, owner);
3365
+ }
3366
+ async function waitMs2(ms) {
3367
+ await new Promise((resolve) => {
3368
+ setTimeout(resolve, ms);
3369
+ });
3370
+ }
3019
3371
 
3020
- // src/queue-ipc.ts
3021
- import { createHash, randomUUID as randomUUID2 } from "crypto";
3022
- import fs3 from "fs/promises";
3372
+ // src/queue-ipc-client.ts
3373
+ import { randomUUID as randomUUID2 } from "crypto";
3023
3374
  import net from "net";
3024
- import os2 from "os";
3025
- import path4 from "path";
3026
3375
 
3027
3376
  // src/queue-messages.ts
3028
3377
  function asRecord4(value) {
@@ -3255,236 +3604,629 @@ function parseQueueOwnerMessage(raw) {
3255
3604
  return null;
3256
3605
  }
3257
3606
 
3258
- // src/queue-ipc.ts
3259
- var PROCESS_EXIT_GRACE_MS = 1500;
3260
- var PROCESS_POLL_MS = 50;
3607
+ // src/queue-ipc-client.ts
3261
3608
  var QUEUE_CONNECT_ATTEMPTS = 40;
3262
3609
  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
- };
3610
+ function shouldRetryQueueConnect(error) {
3611
+ const code = error.code;
3612
+ return code === "ENOENT" || code === "ECONNREFUSED";
3276
3613
  }
3277
- function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options = {}) {
3278
- const normalized = normalizeOutputError(error, {
3279
- defaultCode: "RUNTIME",
3280
- origin: "queue",
3281
- detailCode,
3282
- retryable: options.retryable
3614
+ async function connectToSocket(socketPath) {
3615
+ return await new Promise((resolve, reject) => {
3616
+ const socket = net.createConnection(socketPath);
3617
+ const onConnect = () => {
3618
+ socket.off("error", onError);
3619
+ resolve(socket);
3620
+ };
3621
+ const onError = (error) => {
3622
+ socket.off("connect", onConnect);
3623
+ reject(error);
3624
+ };
3625
+ socket.once("connect", onConnect);
3626
+ socket.once("error", onError);
3283
3627
  });
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
3628
  }
3295
- function isProcessAlive(pid) {
3296
- if (!pid || !Number.isInteger(pid) || pid <= 0 || pid === process.pid) {
3297
- return false;
3629
+ async function connectToQueueOwner(owner) {
3630
+ let lastError;
3631
+ for (let attempt = 0; attempt < QUEUE_CONNECT_ATTEMPTS; attempt += 1) {
3632
+ try {
3633
+ return await connectToSocket(owner.socketPath);
3634
+ } catch (error) {
3635
+ lastError = error;
3636
+ if (!shouldRetryQueueConnect(error)) {
3637
+ throw error;
3638
+ }
3639
+ if (!isProcessAlive(owner.pid)) {
3640
+ return void 0;
3641
+ }
3642
+ await waitMs2(QUEUE_CONNECT_RETRY_MS);
3643
+ }
3298
3644
  }
3299
- try {
3300
- process.kill(pid, 0);
3301
- return true;
3302
- } catch {
3303
- return false;
3645
+ if (lastError && !shouldRetryQueueConnect(lastError)) {
3646
+ throw lastError;
3304
3647
  }
3648
+ return void 0;
3305
3649
  }
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);
3650
+ async function submitToQueueOwner(owner, options) {
3651
+ const socket = await connectToQueueOwner(owner);
3652
+ if (!socket) {
3653
+ return void 0;
3654
+ }
3655
+ socket.setEncoding("utf8");
3656
+ const requestId = randomUUID2();
3657
+ const request = {
3658
+ type: "submit_prompt",
3659
+ requestId,
3660
+ message: options.message,
3661
+ permissionMode: options.permissionMode,
3662
+ nonInteractivePermissions: options.nonInteractivePermissions,
3663
+ timeoutMs: options.timeoutMs,
3664
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
3665
+ waitForCompletion: options.waitForCompletion
3666
+ };
3667
+ options.outputFormatter.setContext({
3668
+ sessionId: options.sessionId,
3669
+ requestId,
3670
+ stream: "prompt"
3671
+ });
3672
+ return await new Promise((resolve, reject) => {
3673
+ let settled = false;
3674
+ let acknowledged = false;
3675
+ let buffer = "";
3676
+ let sawDone = false;
3677
+ const finishResolve = (result) => {
3678
+ if (settled) {
3679
+ return;
3680
+ }
3681
+ settled = true;
3682
+ socket.removeAllListeners();
3683
+ if (!socket.destroyed) {
3684
+ socket.end();
3685
+ }
3686
+ resolve(result);
3687
+ };
3688
+ const finishReject = (error) => {
3689
+ if (settled) {
3690
+ return;
3691
+ }
3692
+ settled = true;
3693
+ socket.removeAllListeners();
3694
+ if (!socket.destroyed) {
3695
+ socket.destroy();
3696
+ }
3697
+ reject(error);
3698
+ };
3699
+ const processLine = (line) => {
3700
+ let parsed;
3701
+ try {
3702
+ parsed = JSON.parse(line);
3703
+ } catch {
3704
+ finishReject(
3705
+ new QueueProtocolError("Queue owner sent invalid JSON payload", {
3706
+ detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
3707
+ origin: "queue",
3708
+ retryable: true
3709
+ })
3710
+ );
3711
+ return;
3712
+ }
3713
+ const message = parseQueueOwnerMessage(parsed);
3714
+ if (!message || message.requestId !== requestId) {
3715
+ finishReject(
3716
+ new QueueProtocolError("Queue owner sent malformed message", {
3717
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
3718
+ origin: "queue",
3719
+ retryable: true
3720
+ })
3721
+ );
3722
+ return;
3723
+ }
3724
+ if (message.type === "accepted") {
3725
+ acknowledged = true;
3726
+ options.outputFormatter.setContext({
3727
+ sessionId: options.sessionId,
3728
+ requestId: message.requestId,
3729
+ stream: "prompt"
3730
+ });
3731
+ if (!options.waitForCompletion) {
3732
+ const queued = {
3733
+ queued: true,
3734
+ sessionId: options.sessionId,
3735
+ requestId
3736
+ };
3737
+ finishResolve(queued);
3738
+ }
3739
+ return;
3740
+ }
3741
+ if (message.type === "error") {
3742
+ options.outputFormatter.setContext({
3743
+ sessionId: options.sessionId,
3744
+ requestId: message.requestId,
3745
+ stream: "prompt"
3746
+ });
3747
+ options.outputFormatter.onError({
3748
+ code: message.code ?? "RUNTIME",
3749
+ detailCode: message.detailCode,
3750
+ origin: message.origin ?? "queue",
3751
+ message: message.message,
3752
+ retryable: message.retryable,
3753
+ acp: message.acp
3754
+ });
3755
+ options.outputFormatter.flush();
3756
+ const queueErrorAlreadyEmitted = options.errorEmissionPolicy?.queueErrorAlreadyEmitted ?? true;
3757
+ finishReject(
3758
+ new QueueConnectionError(message.message, {
3759
+ outputCode: message.code,
3760
+ detailCode: message.detailCode,
3761
+ origin: message.origin ?? "queue",
3762
+ retryable: message.retryable,
3763
+ acp: message.acp,
3764
+ ...queueErrorAlreadyEmitted ? { outputAlreadyEmitted: true } : {}
3765
+ })
3766
+ );
3767
+ return;
3768
+ }
3769
+ if (!acknowledged) {
3770
+ finishReject(
3771
+ new QueueConnectionError("Queue owner did not acknowledge request", {
3772
+ detailCode: "QUEUE_ACK_MISSING",
3773
+ origin: "queue",
3774
+ retryable: true
3775
+ })
3776
+ );
3777
+ return;
3778
+ }
3779
+ if (message.type === "session_update") {
3780
+ options.outputFormatter.onSessionUpdate(message.notification);
3781
+ return;
3782
+ }
3783
+ if (message.type === "client_operation") {
3784
+ options.outputFormatter.onClientOperation(message.operation);
3785
+ return;
3786
+ }
3787
+ if (message.type === "done") {
3788
+ options.outputFormatter.onDone(message.stopReason);
3789
+ sawDone = true;
3790
+ return;
3791
+ }
3792
+ if (message.type === "result") {
3793
+ if (!sawDone) {
3794
+ options.outputFormatter.onDone(message.result.stopReason);
3795
+ }
3796
+ options.outputFormatter.flush();
3797
+ finishResolve(message.result);
3798
+ return;
3799
+ }
3800
+ finishReject(
3801
+ new QueueProtocolError("Queue owner returned unexpected response", {
3802
+ detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
3803
+ origin: "queue",
3804
+ retryable: true
3805
+ })
3806
+ );
3807
+ };
3808
+ socket.on("data", (chunk) => {
3809
+ buffer += chunk;
3810
+ let index = buffer.indexOf("\n");
3811
+ while (index >= 0) {
3812
+ const line = buffer.slice(0, index).trim();
3813
+ buffer = buffer.slice(index + 1);
3814
+ if (line.length > 0) {
3815
+ processLine(line);
3816
+ }
3817
+ index = buffer.indexOf("\n");
3818
+ }
3314
3819
  });
3820
+ socket.once("error", (error) => {
3821
+ finishReject(error);
3822
+ });
3823
+ socket.once("close", () => {
3824
+ if (settled) {
3825
+ return;
3826
+ }
3827
+ if (!acknowledged) {
3828
+ finishReject(
3829
+ new QueueConnectionError(
3830
+ "Queue owner disconnected before acknowledging request",
3831
+ {
3832
+ detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
3833
+ origin: "queue",
3834
+ retryable: true
3835
+ }
3836
+ )
3837
+ );
3838
+ return;
3839
+ }
3840
+ if (!options.waitForCompletion) {
3841
+ const queued = {
3842
+ queued: true,
3843
+ sessionId: options.sessionId,
3844
+ requestId
3845
+ };
3846
+ finishResolve(queued);
3847
+ return;
3848
+ }
3849
+ finishReject(
3850
+ new QueueConnectionError("Queue owner disconnected before prompt completion", {
3851
+ detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
3852
+ origin: "queue",
3853
+ retryable: true
3854
+ })
3855
+ );
3856
+ });
3857
+ socket.write(`${JSON.stringify(request)}
3858
+ `);
3859
+ });
3860
+ }
3861
+ async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
3862
+ const socket = await connectToQueueOwner(owner);
3863
+ if (!socket) {
3864
+ return void 0;
3315
3865
  }
3316
- return !isProcessAlive(pid);
3866
+ socket.setEncoding("utf8");
3867
+ return await new Promise((resolve, reject) => {
3868
+ let settled = false;
3869
+ let acknowledged = false;
3870
+ let buffer = "";
3871
+ const finishResolve = (result) => {
3872
+ if (settled) {
3873
+ return;
3874
+ }
3875
+ settled = true;
3876
+ socket.removeAllListeners();
3877
+ if (!socket.destroyed) {
3878
+ socket.end();
3879
+ }
3880
+ resolve(result);
3881
+ };
3882
+ const finishReject = (error) => {
3883
+ if (settled) {
3884
+ return;
3885
+ }
3886
+ settled = true;
3887
+ socket.removeAllListeners();
3888
+ if (!socket.destroyed) {
3889
+ socket.destroy();
3890
+ }
3891
+ reject(error);
3892
+ };
3893
+ const processLine = (line) => {
3894
+ let parsed;
3895
+ try {
3896
+ parsed = JSON.parse(line);
3897
+ } catch {
3898
+ finishReject(
3899
+ new QueueProtocolError("Queue owner sent invalid JSON payload", {
3900
+ detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
3901
+ origin: "queue",
3902
+ retryable: true
3903
+ })
3904
+ );
3905
+ return;
3906
+ }
3907
+ const message = parseQueueOwnerMessage(parsed);
3908
+ if (!message || message.requestId !== request.requestId) {
3909
+ finishReject(
3910
+ new QueueProtocolError("Queue owner sent malformed message", {
3911
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
3912
+ origin: "queue",
3913
+ retryable: true
3914
+ })
3915
+ );
3916
+ return;
3917
+ }
3918
+ if (message.type === "accepted") {
3919
+ acknowledged = true;
3920
+ return;
3921
+ }
3922
+ if (message.type === "error") {
3923
+ finishReject(
3924
+ new QueueConnectionError(message.message, {
3925
+ outputCode: message.code,
3926
+ detailCode: message.detailCode,
3927
+ origin: message.origin ?? "queue",
3928
+ retryable: message.retryable,
3929
+ acp: message.acp
3930
+ })
3931
+ );
3932
+ return;
3933
+ }
3934
+ if (!acknowledged) {
3935
+ finishReject(
3936
+ new QueueConnectionError("Queue owner did not acknowledge request", {
3937
+ detailCode: "QUEUE_ACK_MISSING",
3938
+ origin: "queue",
3939
+ retryable: true
3940
+ })
3941
+ );
3942
+ return;
3943
+ }
3944
+ if (!isExpectedResponse(message)) {
3945
+ finishReject(
3946
+ new QueueProtocolError("Queue owner returned unexpected response", {
3947
+ detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
3948
+ origin: "queue",
3949
+ retryable: true
3950
+ })
3951
+ );
3952
+ return;
3953
+ }
3954
+ finishResolve(message);
3955
+ };
3956
+ socket.on("data", (chunk) => {
3957
+ buffer += chunk;
3958
+ let index = buffer.indexOf("\n");
3959
+ while (index >= 0) {
3960
+ const line = buffer.slice(0, index).trim();
3961
+ buffer = buffer.slice(index + 1);
3962
+ if (line.length > 0) {
3963
+ processLine(line);
3964
+ }
3965
+ index = buffer.indexOf("\n");
3966
+ }
3967
+ });
3968
+ socket.once("error", (error) => {
3969
+ finishReject(error);
3970
+ });
3971
+ socket.once("close", () => {
3972
+ if (settled) {
3973
+ return;
3974
+ }
3975
+ if (!acknowledged) {
3976
+ finishReject(
3977
+ new QueueConnectionError(
3978
+ "Queue owner disconnected before acknowledging request",
3979
+ {
3980
+ detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
3981
+ origin: "queue",
3982
+ retryable: true
3983
+ }
3984
+ )
3985
+ );
3986
+ return;
3987
+ }
3988
+ finishReject(
3989
+ new QueueConnectionError("Queue owner disconnected before responding", {
3990
+ detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
3991
+ origin: "queue",
3992
+ retryable: true
3993
+ })
3994
+ );
3995
+ });
3996
+ socket.write(`${JSON.stringify(request)}
3997
+ `);
3998
+ });
3317
3999
  }
3318
- async function terminateProcess(pid) {
3319
- if (!isProcessAlive(pid)) {
3320
- return false;
4000
+ async function submitCancelToQueueOwner(owner) {
4001
+ const request = {
4002
+ type: "cancel_prompt",
4003
+ requestId: randomUUID2()
4004
+ };
4005
+ const response = await submitControlToQueueOwner(
4006
+ owner,
4007
+ request,
4008
+ (message) => message.type === "cancel_result"
4009
+ );
4010
+ if (!response) {
4011
+ return void 0;
4012
+ }
4013
+ if (response.requestId !== request.requestId) {
4014
+ throw new QueueProtocolError("Queue owner returned mismatched cancel response", {
4015
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4016
+ origin: "queue",
4017
+ retryable: true
4018
+ });
4019
+ }
4020
+ return response.cancelled;
4021
+ }
4022
+ async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
4023
+ const request = {
4024
+ type: "set_mode",
4025
+ requestId: randomUUID2(),
4026
+ modeId,
4027
+ timeoutMs
4028
+ };
4029
+ const response = await submitControlToQueueOwner(
4030
+ owner,
4031
+ request,
4032
+ (message) => message.type === "set_mode_result"
4033
+ );
4034
+ if (!response) {
4035
+ return void 0;
4036
+ }
4037
+ if (response.requestId !== request.requestId) {
4038
+ throw new QueueProtocolError("Queue owner returned mismatched set_mode response", {
4039
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4040
+ origin: "queue",
4041
+ retryable: true
4042
+ });
4043
+ }
4044
+ return true;
4045
+ }
4046
+ async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
4047
+ const request = {
4048
+ type: "set_config_option",
4049
+ requestId: randomUUID2(),
4050
+ configId,
4051
+ value,
4052
+ timeoutMs
4053
+ };
4054
+ const response = await submitControlToQueueOwner(
4055
+ owner,
4056
+ request,
4057
+ (message) => message.type === "set_config_option_result"
4058
+ );
4059
+ if (!response) {
4060
+ return void 0;
4061
+ }
4062
+ if (response.requestId !== request.requestId) {
4063
+ throw new QueueProtocolError(
4064
+ "Queue owner returned mismatched set_config_option response",
4065
+ {
4066
+ detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
4067
+ origin: "queue",
4068
+ retryable: true
4069
+ }
4070
+ );
4071
+ }
4072
+ return response.response;
4073
+ }
4074
+ async function trySubmitToRunningOwner(options) {
4075
+ const owner = await readQueueOwnerRecord(options.sessionId);
4076
+ if (!owner) {
4077
+ return void 0;
3321
4078
  }
3322
- try {
3323
- process.kill(pid, "SIGTERM");
3324
- } catch {
3325
- return false;
4079
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4080
+ return void 0;
3326
4081
  }
3327
- if (await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS)) {
3328
- return true;
4082
+ const submitted = await submitToQueueOwner(owner, options);
4083
+ if (submitted) {
4084
+ if (options.verbose) {
4085
+ process.stderr.write(
4086
+ `[acpx] queued prompt on active owner pid ${owner.pid} for session ${options.sessionId}
4087
+ `
4088
+ );
4089
+ }
4090
+ return submitted;
3329
4091
  }
3330
- try {
3331
- process.kill(pid, "SIGKILL");
3332
- } catch {
3333
- return false;
4092
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4093
+ return void 0;
3334
4094
  }
3335
- await waitForProcessExit(pid, PROCESS_EXIT_GRACE_MS);
3336
- return true;
4095
+ throw new QueueConnectionError(
4096
+ "Session queue owner is running but not accepting queue requests",
4097
+ {
4098
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4099
+ origin: "queue",
4100
+ retryable: true
4101
+ }
4102
+ );
3337
4103
  }
3338
- function parseQueueOwnerRecord(raw) {
3339
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3340
- return null;
4104
+ async function tryCancelOnRunningOwner(options) {
4105
+ const owner = await readQueueOwnerRecord(options.sessionId);
4106
+ if (!owner) {
4107
+ return void 0;
3341
4108
  }
3342
- const record = raw;
3343
- if (!Number.isInteger(record.pid) || record.pid <= 0 || typeof record.sessionId !== "string" || typeof record.socketPath !== "string") {
3344
- return null;
4109
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4110
+ return void 0;
3345
4111
  }
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}`;
4112
+ const cancelled2 = await submitCancelToQueueOwner(owner);
4113
+ if (cancelled2 !== void 0) {
4114
+ if (options.verbose) {
4115
+ process.stderr.write(
4116
+ `[acpx] requested cancel on active owner pid ${owner.pid} for session ${options.sessionId}
4117
+ `
4118
+ );
4119
+ }
4120
+ return cancelled2;
3362
4121
  }
3363
- return path4.join(queueBaseDir(), `${key}.sock`);
3364
- }
3365
- async function ensureQueueDir() {
3366
- await fs3.mkdir(queueBaseDir(), { recursive: true });
4122
+ if (!await ensureOwnerIsUsable(options.sessionId, owner)) {
4123
+ return void 0;
4124
+ }
4125
+ throw new QueueConnectionError(
4126
+ "Session queue owner is running but not accepting cancel requests",
4127
+ {
4128
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4129
+ origin: "queue",
4130
+ retryable: true
4131
+ }
4132
+ );
3367
4133
  }
3368
- async function removeSocketFile(socketPath) {
3369
- if (process.platform === "win32") {
3370
- return;
4134
+ async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
4135
+ const owner = await readQueueOwnerRecord(sessionId);
4136
+ if (!owner) {
4137
+ return void 0;
3371
4138
  }
3372
- try {
3373
- await fs3.unlink(socketPath);
3374
- } catch (error) {
3375
- if (error.code !== "ENOENT") {
3376
- throw error;
4139
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
4140
+ return void 0;
4141
+ }
4142
+ const submitted = await submitSetModeToQueueOwner(owner, modeId, timeoutMs);
4143
+ if (submitted) {
4144
+ if (verbose) {
4145
+ process.stderr.write(
4146
+ `[acpx] requested session/set_mode on owner pid ${owner.pid} for session ${sessionId}
4147
+ `
4148
+ );
3377
4149
  }
4150
+ return true;
3378
4151
  }
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 {
4152
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
3387
4153
  return void 0;
3388
4154
  }
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;
4155
+ throw new QueueConnectionError(
4156
+ "Session queue owner is running but not accepting set_mode requests",
4157
+ {
4158
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4159
+ origin: "queue",
4160
+ retryable: true
3398
4161
  }
3399
- });
4162
+ );
3400
4163
  }
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
4164
+ async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
4165
+ const owner = await readQueueOwnerRecord(sessionId);
4166
+ if (!owner) {
4167
+ return void 0;
4168
+ }
4169
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
4170
+ return void 0;
4171
+ }
4172
+ const response = await submitSetConfigOptionToQueueOwner(
4173
+ owner,
4174
+ configId,
4175
+ value,
4176
+ timeoutMs
3414
4177
  );
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);
4178
+ if (response) {
4179
+ if (verbose) {
4180
+ process.stderr.write(
4181
+ `[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}
4182
+ `
4183
+ );
3431
4184
  }
4185
+ return response;
4186
+ }
4187
+ if (!await ensureOwnerIsUsable(sessionId, owner)) {
3432
4188
  return void 0;
3433
4189
  }
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;
4190
+ throw new QueueConnectionError(
4191
+ "Session queue owner is running but not accepting set_config_option requests",
4192
+ {
4193
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
4194
+ origin: "queue",
4195
+ retryable: true
3441
4196
  }
3442
- });
3443
- }
3444
- function shouldRetryQueueConnect(error) {
3445
- const code = error.code;
3446
- return code === "ENOENT" || code === "ECONNREFUSED";
4197
+ );
3447
4198
  }
3448
- async function waitMs2(ms) {
3449
- await new Promise((resolve) => {
3450
- setTimeout(resolve, ms);
3451
- });
4199
+
4200
+ // src/queue-ipc-server.ts
4201
+ import net2 from "net";
4202
+ function makeQueueOwnerError(requestId, message, detailCode, options = {}) {
4203
+ return {
4204
+ type: "error",
4205
+ requestId,
4206
+ code: "RUNTIME",
4207
+ detailCode,
4208
+ origin: "queue",
4209
+ retryable: options.retryable,
4210
+ message
4211
+ };
3452
4212
  }
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);
4213
+ function makeQueueOwnerErrorFromUnknown(requestId, error, detailCode, options = {}) {
4214
+ const normalized = normalizeOutputError(error, {
4215
+ defaultCode: "RUNTIME",
4216
+ origin: "queue",
4217
+ detailCode,
4218
+ retryable: options.retryable
3466
4219
  });
3467
- }
3468
- async function connectToQueueOwner(owner) {
3469
- let lastError;
3470
- for (let attempt = 0; attempt < QUEUE_CONNECT_ATTEMPTS; attempt += 1) {
3471
- try {
3472
- return await connectToSocket(owner.socketPath);
3473
- } catch (error) {
3474
- lastError = error;
3475
- if (!shouldRetryQueueConnect(error)) {
3476
- throw error;
3477
- }
3478
- if (!isProcessAlive(owner.pid)) {
3479
- return void 0;
3480
- }
3481
- await waitMs2(QUEUE_CONNECT_RETRY_MS);
3482
- }
3483
- }
3484
- if (lastError && !shouldRetryQueueConnect(lastError)) {
3485
- throw lastError;
3486
- }
3487
- return void 0;
4220
+ return {
4221
+ type: "error",
4222
+ requestId,
4223
+ code: normalized.code,
4224
+ detailCode: normalized.detailCode,
4225
+ origin: normalized.origin,
4226
+ message: normalized.message,
4227
+ retryable: normalized.retryable,
4228
+ acp: normalized.acp
4229
+ };
3488
4230
  }
3489
4231
  function writeQueueMessage(socket, message) {
3490
4232
  if (socket.destroyed || !socket.writable) {
@@ -3505,7 +4247,7 @@ var SessionQueueOwner = class _SessionQueueOwner {
3505
4247
  }
3506
4248
  static async start(lease, controlHandlers) {
3507
4249
  const ownerRef = { current: void 0 };
3508
- const server = net.createServer((socket) => {
4250
+ const server = net2.createServer((socket) => {
3509
4251
  ownerRef.current?.handleConnection(socket);
3510
4252
  });
3511
4253
  ownerRef.current = new _SessionQueueOwner(server, controlHandlers);
@@ -3524,557 +4266,238 @@ var SessionQueueOwner = class _SessionQueueOwner {
3524
4266
  });
3525
4267
  return ownerRef.current;
3526
4268
  }
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;
3803
- }
3804
- settled = true;
3805
- socket.removeAllListeners();
3806
- if (!socket.destroyed) {
3807
- socket.end();
3808
- }
3809
- resolve(result);
3810
- };
3811
- const finishReject = (error) => {
3812
- if (settled) {
3813
- return;
3814
- }
3815
- settled = true;
3816
- socket.removeAllListeners();
3817
- if (!socket.destroyed) {
3818
- socket.destroy();
3819
- }
3820
- reject(error);
3821
- };
3822
- const processLine = (line) => {
3823
- let parsed;
3824
- try {
3825
- parsed = JSON.parse(line);
3826
- } catch {
3827
- finishReject(
3828
- new QueueProtocolError("Queue owner sent invalid JSON payload", {
3829
- detailCode: "QUEUE_PROTOCOL_INVALID_JSON",
3830
- origin: "queue",
3831
- retryable: true
3832
- })
3833
- );
3834
- return;
3835
- }
3836
- const message = parseQueueOwnerMessage(parsed);
3837
- if (!message || message.requestId !== requestId) {
3838
- finishReject(
3839
- new QueueProtocolError("Queue owner sent malformed message", {
3840
- detailCode: "QUEUE_PROTOCOL_MALFORMED_MESSAGE",
3841
- origin: "queue",
3842
- retryable: true
3843
- })
3844
- );
3845
- return;
3846
- }
3847
- if (message.type === "accepted") {
3848
- acknowledged = true;
3849
- options.outputFormatter.setContext({
3850
- sessionId: options.sessionId,
3851
- requestId: message.requestId,
3852
- stream: "prompt"
3853
- });
3854
- if (!options.waitForCompletion) {
3855
- const queued = {
3856
- queued: true,
3857
- sessionId: options.sessionId,
3858
- requestId
3859
- };
3860
- finishResolve(queued);
3861
- }
3862
- return;
3863
- }
3864
- if (message.type === "error") {
3865
- options.outputFormatter.setContext({
3866
- sessionId: options.sessionId,
3867
- requestId: message.requestId,
3868
- stream: "prompt"
3869
- });
3870
- options.outputFormatter.onError({
3871
- code: message.code ?? "RUNTIME",
3872
- detailCode: message.detailCode,
3873
- origin: message.origin ?? "queue",
3874
- message: message.message,
3875
- retryable: message.retryable,
3876
- acp: message.acp
3877
- });
3878
- options.outputFormatter.flush();
3879
- const queueErrorAlreadyEmitted = options.errorEmissionPolicy?.queueErrorAlreadyEmitted ?? true;
3880
- finishReject(
3881
- new QueueConnectionError(message.message, {
3882
- outputCode: message.code,
3883
- detailCode: message.detailCode,
3884
- origin: message.origin ?? "queue",
3885
- retryable: message.retryable,
3886
- acp: message.acp,
3887
- ...queueErrorAlreadyEmitted ? { outputAlreadyEmitted: true } : {}
3888
- })
3889
- );
3890
- return;
3891
- }
3892
- if (!acknowledged) {
3893
- finishReject(
3894
- new QueueConnectionError("Queue owner did not acknowledge request", {
3895
- detailCode: "QUEUE_ACK_MISSING",
3896
- origin: "queue",
3897
- retryable: true
3898
- })
4269
+ async close() {
4270
+ if (this.closed) {
4271
+ return;
4272
+ }
4273
+ this.closed = true;
4274
+ for (const waiter of this.waiters.splice(0)) {
4275
+ waiter(void 0);
4276
+ }
4277
+ for (const task of this.pending.splice(0)) {
4278
+ if (task.waitForCompletion) {
4279
+ task.send(
4280
+ makeQueueOwnerError(
4281
+ task.requestId,
4282
+ "Queue owner shutting down before prompt execution",
4283
+ "QUEUE_OWNER_SHUTTING_DOWN",
4284
+ {
4285
+ retryable: true
4286
+ }
4287
+ )
3899
4288
  );
3900
- return;
3901
- }
3902
- if (message.type === "session_update") {
3903
- options.outputFormatter.onSessionUpdate(message.notification);
3904
- return;
3905
- }
3906
- if (message.type === "client_operation") {
3907
- options.outputFormatter.onClientOperation(message.operation);
3908
- return;
3909
- }
3910
- if (message.type === "done") {
3911
- options.outputFormatter.onDone(message.stopReason);
3912
- sawDone = true;
3913
- return;
3914
- }
3915
- if (message.type === "result") {
3916
- if (!sawDone) {
3917
- options.outputFormatter.onDone(message.result.stopReason);
3918
- }
3919
- options.outputFormatter.flush();
3920
- finishResolve(message.result);
3921
- return;
3922
4289
  }
3923
- finishReject(
3924
- new QueueProtocolError("Queue owner returned unexpected response", {
3925
- detailCode: "QUEUE_PROTOCOL_UNEXPECTED_RESPONSE",
3926
- origin: "queue",
3927
- retryable: true
3928
- })
4290
+ task.close();
4291
+ }
4292
+ await new Promise((resolve) => {
4293
+ this.server.close(() => resolve());
4294
+ });
4295
+ }
4296
+ async nextTask(timeoutMs) {
4297
+ if (this.pending.length > 0) {
4298
+ return this.pending.shift();
4299
+ }
4300
+ if (this.closed) {
4301
+ return void 0;
4302
+ }
4303
+ return await new Promise((resolve) => {
4304
+ const shouldTimeout = timeoutMs != null;
4305
+ const timer = shouldTimeout && setTimeout(
4306
+ () => {
4307
+ const index = this.waiters.indexOf(waiter);
4308
+ if (index >= 0) {
4309
+ this.waiters.splice(index, 1);
4310
+ }
4311
+ resolve(void 0);
4312
+ },
4313
+ Math.max(0, timeoutMs)
3929
4314
  );
3930
- };
3931
- socket.on("data", (chunk) => {
3932
- buffer += chunk;
3933
- let index = buffer.indexOf("\n");
3934
- while (index >= 0) {
3935
- const line = buffer.slice(0, index).trim();
3936
- buffer = buffer.slice(index + 1);
3937
- if (line.length > 0) {
3938
- processLine(line);
4315
+ const waiter = (task) => {
4316
+ if (timer) {
4317
+ clearTimeout(timer);
3939
4318
  }
3940
- index = buffer.indexOf("\n");
3941
- }
3942
- });
3943
- socket.once("error", (error) => {
3944
- finishReject(error);
4319
+ resolve(task);
4320
+ };
4321
+ this.waiters.push(waiter);
3945
4322
  });
3946
- socket.once("close", () => {
3947
- if (settled) {
3948
- return;
3949
- }
3950
- if (!acknowledged) {
3951
- finishReject(
3952
- new QueueConnectionError(
3953
- "Queue owner disconnected before acknowledging request",
4323
+ }
4324
+ queueDepth() {
4325
+ return this.pending.length;
4326
+ }
4327
+ enqueue(task) {
4328
+ if (this.closed) {
4329
+ if (task.waitForCompletion) {
4330
+ task.send(
4331
+ makeQueueOwnerError(
4332
+ task.requestId,
4333
+ "Queue owner is shutting down",
4334
+ "QUEUE_OWNER_SHUTTING_DOWN",
3954
4335
  {
3955
- detailCode: "QUEUE_DISCONNECTED_BEFORE_ACK",
3956
- origin: "queue",
3957
4336
  retryable: true
3958
4337
  }
3959
4338
  )
3960
4339
  );
3961
- return;
3962
- }
3963
- if (!options.waitForCompletion) {
3964
- const queued = {
3965
- queued: true,
3966
- sessionId: options.sessionId,
3967
- requestId
3968
- };
3969
- finishResolve(queued);
3970
- return;
3971
4340
  }
3972
- finishReject(
3973
- new QueueConnectionError("Queue owner disconnected before prompt completion", {
3974
- detailCode: "QUEUE_DISCONNECTED_BEFORE_COMPLETION",
3975
- origin: "queue",
4341
+ task.close();
4342
+ return;
4343
+ }
4344
+ const waiter = this.waiters.shift();
4345
+ if (waiter) {
4346
+ waiter(task);
4347
+ return;
4348
+ }
4349
+ this.pending.push(task);
4350
+ }
4351
+ handleConnection(socket) {
4352
+ socket.setEncoding("utf8");
4353
+ if (this.closed) {
4354
+ writeQueueMessage(
4355
+ socket,
4356
+ makeQueueOwnerError("unknown", "Queue owner is closed", "QUEUE_OWNER_CLOSED", {
3976
4357
  retryable: true
3977
4358
  })
3978
4359
  );
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;
4360
+ socket.end();
4361
+ return;
4362
+ }
3993
4363
  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);
4364
+ let handled = false;
4365
+ const fail = (requestId, message, detailCode) => {
4366
+ writeQueueMessage(
4367
+ socket,
4368
+ makeQueueOwnerError(requestId, message, detailCode, {
4369
+ retryable: false
4370
+ })
4371
+ );
4372
+ socket.end();
4004
4373
  };
4005
- const finishReject = (error) => {
4006
- if (settled) {
4374
+ const processLine = (line) => {
4375
+ if (handled) {
4007
4376
  return;
4008
4377
  }
4009
- settled = true;
4010
- socket.removeAllListeners();
4011
- if (!socket.destroyed) {
4012
- socket.destroy();
4013
- }
4014
- reject(error);
4015
- };
4016
- const processLine = (line) => {
4378
+ handled = true;
4017
4379
  let parsed;
4018
4380
  try {
4019
4381
  parsed = JSON.parse(line);
4020
4382
  } 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
- })
4383
+ fail(
4384
+ "unknown",
4385
+ "Invalid queue request payload",
4386
+ "QUEUE_REQUEST_PAYLOAD_INVALID_JSON"
4027
4387
  );
4028
4388
  return;
4029
4389
  }
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
- );
4390
+ const request = parseQueueRequest(parsed);
4391
+ if (!request) {
4392
+ fail("unknown", "Invalid queue request", "QUEUE_REQUEST_INVALID");
4039
4393
  return;
4040
4394
  }
4041
- if (message.type === "accepted") {
4042
- acknowledged = true;
4395
+ if (request.type === "cancel_prompt") {
4396
+ writeQueueMessage(socket, {
4397
+ type: "accepted",
4398
+ requestId: request.requestId
4399
+ });
4400
+ void this.controlHandlers.cancelPrompt().then((cancelled2) => {
4401
+ writeQueueMessage(socket, {
4402
+ type: "cancel_result",
4403
+ requestId: request.requestId,
4404
+ cancelled: cancelled2
4405
+ });
4406
+ }).catch((error) => {
4407
+ writeQueueMessage(
4408
+ socket,
4409
+ makeQueueOwnerErrorFromUnknown(
4410
+ request.requestId,
4411
+ error,
4412
+ "QUEUE_CONTROL_REQUEST_FAILED"
4413
+ )
4414
+ );
4415
+ }).finally(() => {
4416
+ if (!socket.destroyed) {
4417
+ socket.end();
4418
+ }
4419
+ });
4043
4420
  return;
4044
4421
  }
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
- );
4422
+ if (request.type === "set_mode") {
4423
+ writeQueueMessage(socket, {
4424
+ type: "accepted",
4425
+ requestId: request.requestId
4426
+ });
4427
+ void this.controlHandlers.setSessionMode(request.modeId, request.timeoutMs).then(() => {
4428
+ writeQueueMessage(socket, {
4429
+ type: "set_mode_result",
4430
+ requestId: request.requestId,
4431
+ modeId: request.modeId
4432
+ });
4433
+ }).catch((error) => {
4434
+ writeQueueMessage(
4435
+ socket,
4436
+ makeQueueOwnerErrorFromUnknown(
4437
+ request.requestId,
4438
+ error,
4439
+ "QUEUE_CONTROL_REQUEST_FAILED"
4440
+ )
4441
+ );
4442
+ }).finally(() => {
4443
+ if (!socket.destroyed) {
4444
+ socket.end();
4445
+ }
4446
+ });
4055
4447
  return;
4056
4448
  }
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
- );
4449
+ if (request.type === "set_config_option") {
4450
+ writeQueueMessage(socket, {
4451
+ type: "accepted",
4452
+ requestId: request.requestId
4453
+ });
4454
+ void this.controlHandlers.setSessionConfigOption(request.configId, request.value, request.timeoutMs).then((response) => {
4455
+ writeQueueMessage(socket, {
4456
+ type: "set_config_option_result",
4457
+ requestId: request.requestId,
4458
+ response
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
+ });
4065
4474
  return;
4066
4475
  }
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;
4476
+ const task = {
4477
+ requestId: request.requestId,
4478
+ message: request.message,
4479
+ permissionMode: request.permissionMode,
4480
+ nonInteractivePermissions: request.nonInteractivePermissions,
4481
+ timeoutMs: request.timeoutMs,
4482
+ suppressSdkConsoleErrors: request.suppressSdkConsoleErrors,
4483
+ waitForCompletion: request.waitForCompletion,
4484
+ send: (message) => {
4485
+ writeQueueMessage(socket, message);
4486
+ },
4487
+ close: () => {
4488
+ if (!socket.destroyed) {
4489
+ socket.end();
4490
+ }
4491
+ }
4492
+ };
4493
+ writeQueueMessage(socket, {
4494
+ type: "accepted",
4495
+ requestId: request.requestId
4496
+ });
4497
+ if (!request.waitForCompletion) {
4498
+ task.close();
4076
4499
  }
4077
- finishResolve(message);
4500
+ this.enqueue(task);
4078
4501
  };
4079
4502
  socket.on("data", (chunk) => {
4080
4503
  buffer += chunk;
@@ -4088,256 +4511,264 @@ async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
4088
4511
  index = buffer.indexOf("\n");
4089
4512
  }
4090
4513
  });
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
4514
+ socket.on("error", () => {
4141
4515
  });
4142
4516
  }
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;
4517
+ };
4518
+
4519
+ // src/queue-owner-turn-controller.ts
4520
+ var QueueOwnerTurnController = class {
4521
+ options;
4522
+ state = "idle";
4523
+ pendingCancel = false;
4524
+ activeController;
4525
+ constructor(options) {
4526
+ this.options = options;
4159
4527
  }
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
- });
4528
+ get lifecycleState() {
4529
+ return this.state;
4166
4530
  }
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;
4531
+ get hasPendingCancel() {
4532
+ return this.pendingCancel;
4184
4533
  }
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
- );
4534
+ beginTurn() {
4535
+ this.state = "starting";
4536
+ this.pendingCancel = false;
4194
4537
  }
4195
- return response.response;
4196
- }
4197
- async function trySubmitToRunningOwner(options) {
4198
- const owner = await readQueueOwnerRecord(options.sessionId);
4199
- if (!owner) {
4200
- return void 0;
4538
+ markPromptActive() {
4539
+ if (this.state === "starting" || this.state === "active") {
4540
+ this.state = "active";
4541
+ }
4201
4542
  }
4202
- if (!isProcessAlive(owner.pid)) {
4203
- await cleanupStaleQueueOwner(options.sessionId, owner);
4204
- return void 0;
4543
+ endTurn() {
4544
+ this.state = "idle";
4545
+ this.pendingCancel = false;
4205
4546
  }
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;
4547
+ beginClosing() {
4548
+ this.state = "closing";
4549
+ this.pendingCancel = false;
4550
+ this.activeController = void 0;
4215
4551
  }
4216
- if (!isProcessAlive(owner.pid)) {
4217
- await cleanupStaleQueueOwner(options.sessionId, owner);
4218
- return void 0;
4552
+ setActiveController(controller) {
4553
+ this.activeController = controller;
4219
4554
  }
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
4555
+ clearActiveController() {
4556
+ this.activeController = void 0;
4557
+ }
4558
+ assertCanHandleControlRequest() {
4559
+ if (this.state === "closing") {
4560
+ throw new QueueConnectionError("Queue owner is closing", {
4561
+ detailCode: "QUEUE_OWNER_SHUTTING_DOWN",
4562
+ origin: "queue",
4563
+ retryable: true
4564
+ });
4226
4565
  }
4227
- );
4228
- }
4229
- async function tryCancelOnRunningOwner(options) {
4230
- const owner = await readQueueOwnerRecord(options.sessionId);
4231
- if (!owner) {
4232
- return void 0;
4233
4566
  }
4234
- if (!isProcessAlive(owner.pid)) {
4235
- await cleanupStaleQueueOwner(options.sessionId, owner);
4236
- return void 0;
4567
+ async requestCancel() {
4568
+ const activeController = this.activeController;
4569
+ if (activeController?.hasActivePrompt()) {
4570
+ const cancelled2 = await activeController.requestCancelActivePrompt();
4571
+ if (cancelled2) {
4572
+ this.pendingCancel = false;
4573
+ }
4574
+ return cancelled2;
4575
+ }
4576
+ if (this.state === "starting" || this.state === "active") {
4577
+ this.pendingCancel = true;
4578
+ return true;
4579
+ }
4580
+ return false;
4237
4581
  }
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
- );
4582
+ async applyPendingCancel() {
4583
+ const activeController = this.activeController;
4584
+ if (!this.pendingCancel || !activeController || !activeController.hasActivePrompt()) {
4585
+ return false;
4586
+ }
4587
+ const cancelled2 = await activeController.requestCancelActivePrompt();
4588
+ if (cancelled2) {
4589
+ this.pendingCancel = false;
4245
4590
  }
4246
4591
  return cancelled2;
4247
4592
  }
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
4593
+ async setSessionMode(modeId, timeoutMs) {
4594
+ this.assertCanHandleControlRequest();
4595
+ const activeController = this.activeController;
4596
+ if (activeController) {
4597
+ await this.options.withTimeout(
4598
+ async () => await activeController.setSessionMode(modeId),
4599
+ timeoutMs
4600
+ );
4601
+ return;
4258
4602
  }
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;
4603
+ await this.options.setSessionModeFallback(modeId, timeoutMs);
4269
4604
  }
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
- `
4605
+ async setSessionConfigOption(configId, value, timeoutMs) {
4606
+ this.assertCanHandleControlRequest();
4607
+ const activeController = this.activeController;
4608
+ if (activeController) {
4609
+ return await this.options.withTimeout(
4610
+ async () => await activeController.setSessionConfigOption(configId, value),
4611
+ timeoutMs
4276
4612
  );
4277
4613
  }
4278
- return true;
4279
- }
4280
- if (!isProcessAlive(owner.pid)) {
4281
- await cleanupStaleQueueOwner(sessionId, owner);
4282
- return void 0;
4614
+ return await this.options.setSessionConfigOptionFallback(
4615
+ configId,
4616
+ value,
4617
+ timeoutMs
4618
+ );
4283
4619
  }
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
4290
- }
4291
- );
4292
- }
4293
- async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
4294
- const owner = await readQueueOwnerRecord(sessionId);
4295
- if (!owner) {
4296
- return void 0;
4620
+ };
4621
+
4622
+ // src/session-owner-runtime.ts
4623
+ var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
4624
+ var QUEUE_OWNER_HEARTBEAT_INTERVAL_MS = 2e3;
4625
+ function normalizeQueueOwnerTtlMs(ttlMs) {
4626
+ if (ttlMs == null) {
4627
+ return DEFAULT_QUEUE_OWNER_TTL_MS;
4297
4628
  }
4298
- if (!isProcessAlive(owner.pid)) {
4299
- await cleanupStaleQueueOwner(sessionId, owner);
4300
- return void 0;
4629
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) {
4630
+ return DEFAULT_QUEUE_OWNER_TTL_MS;
4301
4631
  }
4302
- const response = await submitSetConfigOptionToQueueOwner(
4303
- owner,
4304
- configId,
4305
- value,
4306
- timeoutMs
4307
- );
4308
- if (response) {
4309
- if (verbose) {
4632
+ return Math.round(ttlMs);
4633
+ }
4634
+ function createQueueOwnerTurnRuntime(options, deps) {
4635
+ const turnController = new QueueOwnerTurnController({
4636
+ withTimeout: async (run, timeoutMs) => await deps.withTimeout(run, timeoutMs),
4637
+ setSessionModeFallback: deps.setSessionModeFallback,
4638
+ setSessionConfigOptionFallback: deps.setSessionConfigOptionFallback
4639
+ });
4640
+ const applyPendingCancel = async () => {
4641
+ return await turnController.applyPendingCancel();
4642
+ };
4643
+ const scheduleApplyPendingCancel = () => {
4644
+ void applyPendingCancel().catch((error) => {
4645
+ if (options.verbose) {
4646
+ process.stderr.write(
4647
+ `[acpx] failed to apply deferred cancel: ${formatErrorMessage(error)}
4648
+ `
4649
+ );
4650
+ }
4651
+ });
4652
+ };
4653
+ return {
4654
+ beginClosing: () => {
4655
+ turnController.beginClosing();
4656
+ },
4657
+ onClientAvailable: (controller) => {
4658
+ turnController.setActiveController(controller);
4659
+ scheduleApplyPendingCancel();
4660
+ },
4661
+ onClientClosed: () => {
4662
+ turnController.clearActiveController();
4663
+ },
4664
+ onPromptActive: async () => {
4665
+ turnController.markPromptActive();
4666
+ await applyPendingCancel();
4667
+ },
4668
+ runPromptTurn: async (run) => {
4669
+ turnController.beginTurn();
4670
+ try {
4671
+ return await run();
4672
+ } finally {
4673
+ turnController.endTurn();
4674
+ }
4675
+ },
4676
+ controlHandlers: {
4677
+ cancelPrompt: async () => {
4678
+ const accepted = await turnController.requestCancel();
4679
+ if (!accepted) {
4680
+ return false;
4681
+ }
4682
+ await applyPendingCancel();
4683
+ return true;
4684
+ },
4685
+ setSessionMode: async (modeId, timeoutMs) => {
4686
+ await turnController.setSessionMode(modeId, timeoutMs);
4687
+ },
4688
+ setSessionConfigOption: async (configId, value, timeoutMs) => {
4689
+ return await turnController.setSessionConfigOption(configId, value, timeoutMs);
4690
+ }
4691
+ }
4692
+ };
4693
+ }
4694
+ async function runQueueOwnerProcess(options, deps) {
4695
+ const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
4696
+ const lease = await tryAcquireQueueOwnerLease(options.sessionId);
4697
+ if (!lease) {
4698
+ if (options.verbose) {
4310
4699
  process.stderr.write(
4311
- `[acpx] requested session/set_config_option on owner pid ${owner.pid} for session ${sessionId}
4700
+ `[acpx] queue owner already active for session ${options.sessionId}; skipping spawn
4312
4701
  `
4313
4702
  );
4314
4703
  }
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
4704
  return;
4334
4705
  }
4335
- if (isProcessAlive(owner.pid)) {
4336
- await terminateProcess(owner.pid);
4706
+ const runtime = createQueueOwnerTurnRuntime(options, deps);
4707
+ let owner;
4708
+ let heartbeatTimer;
4709
+ const refreshHeartbeat = async () => {
4710
+ if (!owner) {
4711
+ return;
4712
+ }
4713
+ await refreshQueueOwnerLease(lease, {
4714
+ queueDepth: owner.queueDepth()
4715
+ }).catch((error) => {
4716
+ if (options.verbose) {
4717
+ process.stderr.write(
4718
+ `[acpx] queue owner heartbeat update failed: ${formatErrorMessage(error)}
4719
+ `
4720
+ );
4721
+ }
4722
+ });
4723
+ };
4724
+ try {
4725
+ owner = await SessionQueueOwner.start(lease, runtime.controlHandlers);
4726
+ await refreshHeartbeat();
4727
+ heartbeatTimer = setInterval(() => {
4728
+ void refreshHeartbeat();
4729
+ }, QUEUE_OWNER_HEARTBEAT_INTERVAL_MS);
4730
+ heartbeatTimer.unref();
4731
+ const idleWaitMs = queueOwnerTtlMs === 0 ? void 0 : Math.max(0, queueOwnerTtlMs);
4732
+ while (true) {
4733
+ const task = await owner.nextTask(idleWaitMs);
4734
+ if (!task) {
4735
+ if (queueOwnerTtlMs > 0 && options.verbose) {
4736
+ process.stderr.write(
4737
+ `[acpx] queue owner TTL expired after ${Math.round(queueOwnerTtlMs / 1e3)}s for session ${options.sessionId}; shutting down
4738
+ `
4739
+ );
4740
+ }
4741
+ break;
4742
+ }
4743
+ await runtime.runPromptTurn(async () => {
4744
+ await deps.runQueuedTask(options.sessionId, task, {
4745
+ verbose: options.verbose,
4746
+ nonInteractivePermissions: options.nonInteractivePermissions,
4747
+ authCredentials: options.authCredentials,
4748
+ authPolicy: options.authPolicy,
4749
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
4750
+ onClientAvailable: runtime.onClientAvailable,
4751
+ onClientClosed: runtime.onClientClosed,
4752
+ onPromptActive: runtime.onPromptActive
4753
+ });
4754
+ });
4755
+ await refreshHeartbeat();
4756
+ }
4757
+ } finally {
4758
+ if (heartbeatTimer) {
4759
+ clearInterval(heartbeatTimer);
4760
+ }
4761
+ runtime.beginClosing();
4762
+ if (owner) {
4763
+ await owner.close();
4764
+ }
4765
+ await releaseQueueOwnerLease(lease);
4337
4766
  }
4338
- await cleanupStaleQueueOwner(sessionId, owner);
4339
4767
  }
4340
4768
 
4769
+ // src/session-owner-spawn.ts
4770
+ import { spawn as spawn3 } from "child_process";
4771
+
4341
4772
  // src/session-persistence.ts
4342
4773
  import { statSync } from "fs";
4343
4774
  import fs4 from "fs/promises";
@@ -4586,6 +5017,124 @@ async function findSessionByDirectoryWalk(options) {
4586
5017
  }
4587
5018
  }
4588
5019
 
5020
+ // src/session-owner-spawn.ts
5021
+ var QUEUE_OWNER_STARTUP_TIMEOUT_MS = 1e4;
5022
+ var QUEUE_OWNER_RESPAWN_BACKOFF_MS = 250;
5023
+ function isQueueNotAcceptingError(error) {
5024
+ return error instanceof QueueConnectionError && error.detailCode === "QUEUE_NOT_ACCEPTING_REQUESTS";
5025
+ }
5026
+ function spawnDetachedQueueOwner(ownerSpawn) {
5027
+ const child = spawn3(ownerSpawn.command, ownerSpawn.args, {
5028
+ cwd: ownerSpawn.cwd,
5029
+ env: ownerSpawn.env,
5030
+ stdio: "ignore",
5031
+ detached: true
5032
+ });
5033
+ child.unref();
5034
+ }
5035
+ async function buildDefaultQueueOwnerSpawn(options, queueOwnerTtlMs) {
5036
+ const entrypoint = process.argv[1];
5037
+ if (!entrypoint) {
5038
+ throw new Error("Cannot spawn queue owner process: CLI entrypoint is missing");
5039
+ }
5040
+ const record = await resolveSessionRecord(options.sessionId);
5041
+ const args = [
5042
+ entrypoint,
5043
+ "__queue-owner",
5044
+ "--session-id",
5045
+ options.sessionId,
5046
+ "--ttl-ms",
5047
+ String(queueOwnerTtlMs),
5048
+ "--permission-mode",
5049
+ options.permissionMode
5050
+ ];
5051
+ if (options.nonInteractivePermissions) {
5052
+ args.push("--non-interactive-permissions", options.nonInteractivePermissions);
5053
+ }
5054
+ if (options.authPolicy) {
5055
+ args.push("--auth-policy", options.authPolicy);
5056
+ }
5057
+ if (options.timeoutMs != null && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
5058
+ args.push("--timeout-ms", String(Math.round(options.timeoutMs)));
5059
+ }
5060
+ if (options.verbose) {
5061
+ args.push("--verbose");
5062
+ }
5063
+ if (options.suppressSdkConsoleErrors) {
5064
+ args.push("--suppress-sdk-console-errors");
5065
+ }
5066
+ return {
5067
+ command: process.execPath,
5068
+ args,
5069
+ cwd: absolutePath(record.cwd)
5070
+ };
5071
+ }
5072
+ async function sendViaDetachedQueueOwner(options) {
5073
+ const waitForCompletion = options.waitForCompletion !== false;
5074
+ const queueOwnerTtlMs = normalizeQueueOwnerTtlMs(options.ttlMs);
5075
+ const ownerSpawn = options.queueOwnerSpawn ?? await buildDefaultQueueOwnerSpawn(options, queueOwnerTtlMs);
5076
+ const startupDeadline = Date.now() + QUEUE_OWNER_STARTUP_TIMEOUT_MS;
5077
+ let lastSpawnAttemptAt = 0;
5078
+ for (; ; ) {
5079
+ try {
5080
+ const queuedToOwner = await trySubmitToRunningOwner({
5081
+ sessionId: options.sessionId,
5082
+ message: options.message,
5083
+ permissionMode: options.permissionMode,
5084
+ nonInteractivePermissions: options.nonInteractivePermissions,
5085
+ outputFormatter: options.outputFormatter,
5086
+ errorEmissionPolicy: options.errorEmissionPolicy,
5087
+ timeoutMs: options.timeoutMs,
5088
+ suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5089
+ waitForCompletion,
5090
+ verbose: options.verbose
5091
+ });
5092
+ if (queuedToOwner) {
5093
+ return queuedToOwner;
5094
+ }
5095
+ } catch (error) {
5096
+ if (!isQueueNotAcceptingError(error)) {
5097
+ throw error;
5098
+ }
5099
+ if (Date.now() >= startupDeadline) {
5100
+ throw new QueueConnectionError(
5101
+ "Timed out waiting for detached queue owner to accept prompt requests",
5102
+ {
5103
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
5104
+ origin: "queue",
5105
+ retryable: true,
5106
+ cause: error instanceof Error ? error : void 0
5107
+ }
5108
+ );
5109
+ }
5110
+ await waitMs2(QUEUE_CONNECT_RETRY_MS);
5111
+ continue;
5112
+ }
5113
+ const now = Date.now();
5114
+ if (now >= startupDeadline) {
5115
+ throw new QueueConnectionError(
5116
+ "Timed out waiting for detached queue owner to start",
5117
+ {
5118
+ detailCode: "QUEUE_NOT_ACCEPTING_REQUESTS",
5119
+ origin: "queue",
5120
+ retryable: true
5121
+ }
5122
+ );
5123
+ }
5124
+ if (now - lastSpawnAttemptAt >= QUEUE_OWNER_RESPAWN_BACKOFF_MS) {
5125
+ spawnDetachedQueueOwner(ownerSpawn);
5126
+ lastSpawnAttemptAt = now;
5127
+ if (options.verbose) {
5128
+ process.stderr.write(
5129
+ `[acpx] starting detached queue owner for session ${options.sessionId}
5130
+ `
5131
+ );
5132
+ }
5133
+ }
5134
+ await waitMs2(QUEUE_CONNECT_RETRY_MS);
5135
+ }
5136
+ }
5137
+
4589
5138
  // src/session-runtime-history.ts
4590
5139
  var SESSION_HISTORY_MAX_ENTRIES = 500;
4591
5140
  var SESSION_HISTORY_PREVIEW_CHARS = 220;
@@ -4774,7 +5323,6 @@ async function connectAndLoadSession(options) {
4774
5323
  }
4775
5324
 
4776
5325
  // src/session-runtime.ts
4777
- var DEFAULT_QUEUE_OWNER_TTL_MS = 3e5;
4778
5326
  var INTERRUPT_CANCEL_WAIT_MS = 2500;
4779
5327
  var TimeoutError = class extends Error {
4780
5328
  constructor(timeoutMs) {
@@ -4902,15 +5450,6 @@ var DISCARD_OUTPUT_FORMATTER = {
4902
5450
  flush() {
4903
5451
  }
4904
5452
  };
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
5453
  function shouldFallbackToNewSession(error) {
4915
5454
  if (error instanceof TimeoutError || error instanceof InterruptedError) {
4916
5455
  return false;
@@ -5376,179 +5915,41 @@ async function ensureSession(options) {
5376
5915
  created: true
5377
5916
  };
5378
5917
  }
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,
5918
+ async function runQueueOwnerProcess2(options) {
5919
+ await runQueueOwnerProcess(options, {
5920
+ runQueuedTask,
5921
+ withTimeout: async (run, timeoutMs) => await withTimeout(run(), timeoutMs),
5922
+ setSessionModeFallback: async (modeId, timeoutMs) => {
5923
+ await runSessionSetModeDirect({
5924
+ sessionRecordId: options.sessionId,
5925
+ modeId,
5404
5926
  nonInteractivePermissions: options.nonInteractivePermissions,
5405
- outputFormatter: options.outputFormatter,
5406
- errorEmissionPolicy: options.errorEmissionPolicy,
5407
- timeoutMs: options.timeoutMs,
5408
- suppressSdkConsoleErrors: options.suppressSdkConsoleErrors,
5409
- waitForCompletion,
5927
+ authCredentials: options.authCredentials,
5928
+ authPolicy: options.authPolicy,
5929
+ timeoutMs,
5410
5930
  verbose: options.verbose
5411
5931
  });
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
- });
5932
+ },
5933
+ setSessionConfigOptionFallback: async (configId, value, timeoutMs) => {
5934
+ const result = await runSessionSetConfigOptionDirect({
5935
+ sessionRecordId: options.sessionId,
5936
+ configId,
5937
+ value,
5938
+ nonInteractivePermissions: options.nonInteractivePermissions,
5939
+ authCredentials: options.authCredentials,
5940
+ authPolicy: options.authPolicy,
5941
+ timeoutMs,
5942
+ verbose: options.verbose
5514
5943
  });
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);
5944
+ return result.response;
5550
5945
  }
5551
- }
5946
+ });
5947
+ }
5948
+ async function sendSession(options) {
5949
+ return await sendViaDetachedQueueOwner(options);
5950
+ }
5951
+ async function readSessionQueueOwnerStatus(sessionId) {
5952
+ return await readQueueOwnerStatus(sessionId);
5552
5953
  }
5553
5954
  async function cancelSessionPrompt(options) {
5554
5955
  const cancelled2 = await tryCancelOnRunningOwner(options);
@@ -5654,6 +6055,7 @@ var NoSessionError = class extends Error {
5654
6055
  }
5655
6056
  };
5656
6057
  var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set([
6058
+ "__queue-owner",
5657
6059
  "prompt",
5658
6060
  "exec",
5659
6061
  "cancel",
@@ -5666,25 +6068,25 @@ var TOP_LEVEL_VERBS = /* @__PURE__ */ new Set([
5666
6068
  ]);
5667
6069
  function parseOutputFormat2(value) {
5668
6070
  if (!OUTPUT_FORMATS.includes(value)) {
5669
- throw new InvalidArgumentError(
6071
+ throw new InvalidArgumentError3(
5670
6072
  `Invalid format "${value}". Expected one of: ${OUTPUT_FORMATS.join(", ")}`
5671
6073
  );
5672
6074
  }
5673
6075
  return value;
5674
6076
  }
5675
- function parseAuthPolicy2(value) {
6077
+ function parseAuthPolicy3(value) {
5676
6078
  if (!AUTH_POLICIES.includes(value)) {
5677
- throw new InvalidArgumentError(
6079
+ throw new InvalidArgumentError3(
5678
6080
  `Invalid auth policy "${value}". Expected one of: ${AUTH_POLICIES.join(", ")}`
5679
6081
  );
5680
6082
  }
5681
6083
  return value;
5682
6084
  }
5683
- function parseNonInteractivePermissionPolicy2(value) {
6085
+ function parseNonInteractivePermissionPolicy3(value) {
5684
6086
  if (!NON_INTERACTIVE_PERMISSION_POLICIES.includes(
5685
6087
  value
5686
6088
  )) {
5687
- throw new InvalidArgumentError(
6089
+ throw new InvalidArgumentError3(
5688
6090
  `Invalid non-interactive permission policy "${value}". Expected one of: ${NON_INTERACTIVE_PERMISSION_POLICIES.join(", ")}`
5689
6091
  );
5690
6092
  }
@@ -5693,35 +6095,35 @@ function parseNonInteractivePermissionPolicy2(value) {
5693
6095
  function parseTimeoutSeconds(value) {
5694
6096
  const parsed = Number(value);
5695
6097
  if (!Number.isFinite(parsed) || parsed <= 0) {
5696
- throw new InvalidArgumentError("Timeout must be a positive number of seconds");
6098
+ throw new InvalidArgumentError3("Timeout must be a positive number of seconds");
5697
6099
  }
5698
6100
  return Math.round(parsed * 1e3);
5699
6101
  }
5700
6102
  function parseTtlSeconds(value) {
5701
6103
  const parsed = Number(value);
5702
6104
  if (!Number.isFinite(parsed) || parsed < 0) {
5703
- throw new InvalidArgumentError("TTL must be a non-negative number of seconds");
6105
+ throw new InvalidArgumentError3("TTL must be a non-negative number of seconds");
5704
6106
  }
5705
6107
  return Math.round(parsed * 1e3);
5706
6108
  }
5707
6109
  function parseSessionName(value) {
5708
6110
  const trimmed = value.trim();
5709
6111
  if (trimmed.length === 0) {
5710
- throw new InvalidArgumentError("Session name must not be empty");
6112
+ throw new InvalidArgumentError3("Session name must not be empty");
5711
6113
  }
5712
6114
  return trimmed;
5713
6115
  }
5714
- function parseNonEmptyValue(label, value) {
6116
+ function parseNonEmptyValue2(label, value) {
5715
6117
  const trimmed = value.trim();
5716
6118
  if (trimmed.length === 0) {
5717
- throw new InvalidArgumentError(`${label} must not be empty`);
6119
+ throw new InvalidArgumentError3(`${label} must not be empty`);
5718
6120
  }
5719
6121
  return trimmed;
5720
6122
  }
5721
6123
  function parseHistoryLimit(value) {
5722
6124
  const parsed = Number(value);
5723
6125
  if (!Number.isInteger(parsed) || parsed <= 0) {
5724
- throw new InvalidArgumentError("Limit must be a positive integer");
6126
+ throw new InvalidArgumentError3("Limit must be a positive integer");
5725
6127
  }
5726
6128
  return parsed;
5727
6129
  }
@@ -5730,7 +6132,7 @@ function resolvePermissionMode(flags, defaultMode) {
5730
6132
  Boolean
5731
6133
  ).length;
5732
6134
  if (selected2 > 1) {
5733
- throw new InvalidArgumentError(
6135
+ throw new InvalidArgumentError3(
5734
6136
  "Use only one permission mode: --approve-all, --approve-reads, or --deny-all"
5735
6137
  );
5736
6138
  }
@@ -5757,7 +6159,7 @@ async function readPrompt(promptParts, filePath, cwd) {
5757
6159
  );
5758
6160
  const prompt2 = pieces.join("\n\n").trim();
5759
6161
  if (!prompt2) {
5760
- throw new InvalidArgumentError("Prompt from --file is empty");
6162
+ throw new InvalidArgumentError3("Prompt from --file is empty");
5761
6163
  }
5762
6164
  return prompt2;
5763
6165
  }
@@ -5766,13 +6168,13 @@ async function readPrompt(promptParts, filePath, cwd) {
5766
6168
  return joined;
5767
6169
  }
5768
6170
  if (process.stdin.isTTY) {
5769
- throw new InvalidArgumentError(
6171
+ throw new InvalidArgumentError3(
5770
6172
  "Prompt is required (pass as argument, --file, or pipe via stdin)"
5771
6173
  );
5772
6174
  }
5773
6175
  const prompt = (await readPromptInputFromStdin()).trim();
5774
6176
  if (!prompt) {
5775
- throw new InvalidArgumentError("Prompt from stdin is empty");
6177
+ throw new InvalidArgumentError3("Prompt from stdin is empty");
5776
6178
  }
5777
6179
  return prompt;
5778
6180
  }
@@ -5787,14 +6189,14 @@ function addGlobalFlags(command) {
5787
6189
  return command.option("--agent <command>", "Raw ACP agent command (escape hatch)").option("--cwd <dir>", "Working directory", process.cwd()).option(
5788
6190
  "--auth-policy <policy>",
5789
6191
  "Authentication policy: skip or fail when auth is required",
5790
- parseAuthPolicy2
6192
+ parseAuthPolicy3
5791
6193
  ).option("--approve-all", "Auto-approve all permission requests").option(
5792
6194
  "--approve-reads",
5793
6195
  "Auto-approve read/search requests and prompt for writes"
5794
6196
  ).option("--deny-all", "Deny all permission requests").option(
5795
6197
  "--non-interactive-permissions <policy>",
5796
6198
  "When prompting is unavailable: deny or fail",
5797
- parseNonInteractivePermissionPolicy2
6199
+ parseNonInteractivePermissionPolicy3
5798
6200
  ).option("--format <fmt>", "Output format: text, json, quiet", parseOutputFormat2).option(
5799
6201
  "--json-strict",
5800
6202
  "Strict JSON mode: requires --format json and suppresses non-JSON stderr output"
@@ -5851,10 +6253,10 @@ function resolveGlobalFlags(command, config) {
5851
6253
  const jsonStrict = opts.jsonStrict === true;
5852
6254
  const verbose = opts.verbose === true;
5853
6255
  if (jsonStrict && format !== "json") {
5854
- throw new InvalidArgumentError("--json-strict requires --format json");
6256
+ throw new InvalidArgumentError3("--json-strict requires --format json");
5855
6257
  }
5856
6258
  if (jsonStrict && verbose) {
5857
- throw new InvalidArgumentError("--json-strict cannot be combined with --verbose");
6259
+ throw new InvalidArgumentError3("--json-strict cannot be combined with --verbose");
5858
6260
  }
5859
6261
  return {
5860
6262
  agent: opts.agent,
@@ -5883,7 +6285,7 @@ function resolveOutputPolicy(format, jsonStrict) {
5883
6285
  function resolveAgentInvocation(explicitAgentName, globalFlags, config) {
5884
6286
  const override = globalFlags.agent?.trim();
5885
6287
  if (override && explicitAgentName) {
5886
- throw new InvalidArgumentError(
6288
+ throw new InvalidArgumentError3(
5887
6289
  "Do not combine positional agent with --agent override"
5888
6290
  );
5889
6291
  }
@@ -6561,7 +6963,12 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6561
6963
  uptime: null,
6562
6964
  lastPromptTime: null,
6563
6965
  exitCode: null,
6564
- signal: null
6966
+ signal: null,
6967
+ ownerPid: null,
6968
+ ownerStatus: null,
6969
+ ownerGeneration: null,
6970
+ ownerHeartbeatAt: null,
6971
+ ownerQueueDepth: null
6565
6972
  };
6566
6973
  if (globalFlags.format === "json") {
6567
6974
  process.stdout.write(`${JSON.stringify(payload2)}
@@ -6583,10 +6990,14 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6583
6990
  process.stdout.write(`uptime: -
6584
6991
  `);
6585
6992
  process.stdout.write(`lastPromptTime: -
6993
+ `);
6994
+ process.stdout.write(`ownerStatus: -
6586
6995
  `);
6587
6996
  return;
6588
6997
  }
6589
6998
  const running = isProcessAlive(record.pid);
6999
+ const owner = await readSessionQueueOwnerStatus(record.id);
7000
+ const ownerStatus = owner ? owner.stale ? "stale" : "active" : "inactive";
6590
7001
  const payload = {
6591
7002
  ...canonicalSessionIdentity(record),
6592
7003
  agentCommand: record.agentCommand,
@@ -6595,7 +7006,12 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6595
7006
  uptime: running ? formatUptime(record.agentStartedAt) ?? null : null,
6596
7007
  lastPromptTime: record.lastPromptAt ?? null,
6597
7008
  exitCode: running ? null : record.lastAgentExitCode ?? null,
6598
- signal: running ? null : record.lastAgentExitSignal ?? null
7009
+ signal: running ? null : record.lastAgentExitSignal ?? null,
7010
+ ownerPid: owner?.pid ?? null,
7011
+ ownerStatus,
7012
+ ownerGeneration: owner?.ownerGeneration ?? null,
7013
+ ownerHeartbeatAt: owner?.heartbeatAt ?? null,
7014
+ ownerQueueDepth: owner?.queueDepth ?? null
6599
7015
  };
6600
7016
  if (globalFlags.format === "json") {
6601
7017
  process.stdout.write(`${JSON.stringify(payload)}
@@ -6622,6 +7038,16 @@ async function handleStatus(explicitAgentName, flags, command, config) {
6622
7038
  process.stdout.write(`uptime: ${payload.uptime ?? "-"}
6623
7039
  `);
6624
7040
  process.stdout.write(`lastPromptTime: ${payload.lastPromptTime ?? "-"}
7041
+ `);
7042
+ process.stdout.write(`ownerStatus: ${payload.ownerStatus}
7043
+ `);
7044
+ process.stdout.write(`ownerPid: ${payload.ownerPid ?? "-"}
7045
+ `);
7046
+ process.stdout.write(`ownerGeneration: ${payload.ownerGeneration ?? "-"}
7047
+ `);
7048
+ process.stdout.write(`ownerHeartbeatAt: ${payload.ownerHeartbeatAt ?? "-"}
7049
+ `);
7050
+ process.stdout.write(`ownerQueueDepth: ${payload.ownerQueueDepth ?? "-"}
6625
7051
  `);
6626
7052
  if (payload.status === "dead") {
6627
7053
  process.stdout.write(`exitCode: ${payload.exitCode ?? "-"}
@@ -6726,7 +7152,7 @@ function registerSharedAgentSubcommands(parent, explicitAgentName, config, descr
6726
7152
  const setModeCommand = parent.command("set-mode").description(descriptions.setMode).argument(
6727
7153
  "<mode>",
6728
7154
  "Mode id",
6729
- (value) => parseNonEmptyValue("Mode", value)
7155
+ (value) => parseNonEmptyValue2("Mode", value)
6730
7156
  );
6731
7157
  addSessionNameOption(setModeCommand);
6732
7158
  setModeCommand.action(async function(modeId, flags) {
@@ -6735,11 +7161,11 @@ function registerSharedAgentSubcommands(parent, explicitAgentName, config, descr
6735
7161
  const setConfigCommand = parent.command("set").description(descriptions.setConfig).argument(
6736
7162
  "<key>",
6737
7163
  "Config option id",
6738
- (value) => parseNonEmptyValue("Config option key", value)
7164
+ (value) => parseNonEmptyValue2("Config option key", value)
6739
7165
  ).argument(
6740
7166
  "<value>",
6741
7167
  "Config option value",
6742
- (value) => parseNonEmptyValue("Config option value", value)
7168
+ (value) => parseNonEmptyValue2("Config option value", value)
6743
7169
  );
6744
7170
  addSessionNameOption(setConfigCommand);
6745
7171
  setConfigCommand.action(async function(key, value, flags) {
@@ -6934,7 +7360,10 @@ async function main(argv = process.argv) {
6934
7360
  requestedOutputFormat,
6935
7361
  requestedJsonStrict
6936
7362
  );
6937
- const builtInAgents = listBuiltInAgents(config.agents);
7363
+ const internalQueueOwnerFlags = parseQueueOwnerFlags(
7364
+ argv.slice(2),
7365
+ DEFAULT_QUEUE_OWNER_TTL_MS
7366
+ );
6938
7367
  const program = new Command();
6939
7368
  program.name("acpx").description("Headless CLI client for the Agent Client Protocol").enablePositionalOptions().showHelpAfterError();
6940
7369
  if (requestedJsonStrict) {
@@ -6946,56 +7375,39 @@ async function main(argv = process.argv) {
6946
7375
  });
6947
7376
  }
6948
7377
  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;
7378
+ configurePublicCli({
7379
+ program,
7380
+ argv: argv.slice(2),
7381
+ config,
7382
+ requestedJsonStrict,
7383
+ topLevelVerbs: TOP_LEVEL_VERBS,
7384
+ listBuiltInAgents,
7385
+ detectAgentToken,
7386
+ registerAgentCommand,
7387
+ registerDefaultCommands,
7388
+ handlePromptAction: async (command, promptParts) => {
7389
+ await handlePrompt(void 0, promptParts, {}, command, config);
6966
7390
  }
6967
- await handlePrompt(void 0, promptParts, {}, this, config);
6968
7391
  });
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
7392
  program.exitOverride((error) => {
6995
7393
  throw error;
6996
7394
  });
6997
7395
  await runWithOutputPolicy(requestedOutputPolicy, async () => {
6998
7396
  try {
7397
+ if (internalQueueOwnerFlags) {
7398
+ await runQueueOwnerProcess2({
7399
+ sessionId: internalQueueOwnerFlags.sessionId,
7400
+ ttlMs: internalQueueOwnerFlags.ttlMs,
7401
+ permissionMode: internalQueueOwnerFlags.permissionMode,
7402
+ nonInteractivePermissions: internalQueueOwnerFlags.nonInteractivePermissions,
7403
+ authCredentials: config.auth,
7404
+ authPolicy: internalQueueOwnerFlags.authPolicy,
7405
+ timeoutMs: internalQueueOwnerFlags.timeoutMs,
7406
+ suppressSdkConsoleErrors: internalQueueOwnerFlags.suppressSdkConsoleErrors,
7407
+ verbose: internalQueueOwnerFlags.verbose
7408
+ });
7409
+ return;
7410
+ }
6999
7411
  await program.parseAsync(argv);
7000
7412
  } catch (error) {
7001
7413
  if (error instanceof CommanderError) {