acpx 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{agent-registry-DU6mhZBL.js → agent-registry-DdKB-zia.js} +20 -17
  2. package/dist/agent-registry-DdKB-zia.js.map +1 -0
  3. package/dist/agent-registry.d.ts.map +1 -1
  4. package/dist/agent-registry.js +1 -1
  5. package/dist/{cli-CiyfEC7L.js → cli-8mVHeUXh.js} +5 -5
  6. package/dist/{cli-CiyfEC7L.js.map → cli-8mVHeUXh.js.map} +1 -1
  7. package/dist/cli.js +122 -94
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{client-C6NUFqr-.d.ts → client-DeNKZZcu.d.ts} +19 -13
  10. package/dist/client-DeNKZZcu.d.ts.map +1 -0
  11. package/dist/{flows-CTOdIk-m.js → flows-WOQNzCMC.js} +5 -5
  12. package/dist/{flows-CTOdIk-m.js.map → flows-WOQNzCMC.js.map} +1 -1
  13. package/dist/flows.d.ts +1 -1
  14. package/dist/flows.js +1 -1
  15. package/dist/{invocation-options-DbCcOAzM.js → invocation-options-B6d7TI-y.js} +7 -6
  16. package/dist/{invocation-options-DbCcOAzM.js.map → invocation-options-B6d7TI-y.js.map} +1 -1
  17. package/dist/{ipc-D3akHMEq.js → ipc-BJrvfNXr.js} +307 -90
  18. package/dist/ipc-BJrvfNXr.js.map +1 -0
  19. package/dist/{output-BPPjvJTI.js → output-CL18YmsG.js} +9 -6
  20. package/dist/output-CL18YmsG.js.map +1 -0
  21. package/dist/{queue-owner-runtime--IbbndGz.js → queue-owner-runtime-Cr0dWJuL.js} +1008 -463
  22. package/dist/queue-owner-runtime-Cr0dWJuL.js.map +1 -0
  23. package/dist/runtime.d.ts +129 -39
  24. package/dist/runtime.d.ts.map +1 -1
  25. package/dist/runtime.js +378 -176
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/{session-watch--UX5dh00.js → session-watch-B8Q_TsGf.js} +4 -4
  28. package/dist/{session-watch--UX5dh00.js.map → session-watch-B8Q_TsGf.js.map} +1 -1
  29. package/dist/{watch-BbBxO-kb.js → watch-Dp73ph8h.js} +2 -2
  30. package/dist/{watch-BbBxO-kb.js.map → watch-Dp73ph8h.js.map} +1 -1
  31. package/package.json +3 -3
  32. package/skills/acpx/SKILL.md +27 -7
  33. package/dist/agent-registry-DU6mhZBL.js.map +0 -1
  34. package/dist/client-C6NUFqr-.d.ts.map +0 -1
  35. package/dist/ipc-D3akHMEq.js.map +0 -1
  36. package/dist/output-BPPjvJTI.js.map +0 -1
  37. package/dist/queue-owner-runtime--IbbndGz.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-DU6mhZBL.js";
1
+ import { n as AGENT_REGISTRY, r as BUILT_IN_AGENT_PACKAGES, t as AGENT_ARGV_REGISTRY } from "./agent-registry-DdKB-zia.js";
2
2
  import { statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import fs$1 from "node:fs/promises";
@@ -9,6 +9,7 @@ import { root } from "@openclaw/fs-safe";
9
9
  import { z } from "zod";
10
10
  import { createHash, randomInt, randomUUID } from "node:crypto";
11
11
  import { isHardlinkFallbackError } from "@openclaw/fs-safe/durability";
12
+ import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
12
13
  import net from "node:net";
13
14
  //#region src/errors.ts
14
15
  var AcpxOperationalError = class extends Error {
@@ -179,8 +180,8 @@ var QueueConnectionError = class extends AcpxOperationalError {};
179
180
  var QueueProtocolError = class extends AcpxOperationalError {};
180
181
  var PermissionDeniedError = class extends AcpxOperationalError {};
181
182
  var PermissionPromptUnavailableError = class extends AcpxOperationalError {
182
- constructor() {
183
- super("Permission prompt unavailable in non-interactive mode");
183
+ constructor(message = "Permission prompt unavailable in non-interactive mode") {
184
+ super(message);
184
185
  }
185
186
  };
186
187
  //#endregion
@@ -310,6 +311,15 @@ function isProcessAlive(pid) {
310
311
  return false;
311
312
  }
312
313
  }
314
+ function isProcessDefinitelyDead(pid) {
315
+ if (!pid || !Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) return false;
316
+ try {
317
+ process.kill(pid, 0);
318
+ return false;
319
+ } catch (error) {
320
+ return error.code === "ESRCH";
321
+ }
322
+ }
313
323
  //#endregion
314
324
  //#region \0@oxc-project+runtime@0.149.0/helpers/esm/usingCtx.js
315
325
  function _usingCtx() {
@@ -1871,11 +1881,12 @@ function closedAtOrLastUsedAt(record) {
1871
1881
  return record.closedAt ?? record.lastUsedAt;
1872
1882
  }
1873
1883
  function isSessionStreamFile(fileName, safeId) {
1874
- return fileName === `${safeId}.stream.ndjson` || fileName === `${safeId}.stream.lock` || fileName.startsWith(`${safeId}.stream.`);
1884
+ const segmentPrefix = `${safeId}.stream.`;
1885
+ return fileName === `${safeId}.stream.ndjson` || fileName === `${safeId}.stream.lock` || fileName.startsWith(segmentPrefix) && /^\d+\.ndjson$/.test(fileName.slice(segmentPrefix.length));
1875
1886
  }
1876
1887
  async function pruneSessions(options = {}) {
1877
1888
  await ensureSessionDir();
1878
- const records = await loadPrunableRecords(filterPruneCandidates(await loadSessionIndexEntries(), options.agentCommand), options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0));
1889
+ const records = await loadPrunableRecords((await loadSessionIndexEntries()).filter((entry) => isPruneCandidate(entry, options.agentCommand)), options.before ?? (options.olderThanMs != null ? new Date(Date.now() - options.olderThanMs) : void 0), options.agentCommand);
1879
1890
  if (options.dryRun) return {
1880
1891
  pruned: records,
1881
1892
  bytesFreed: 0,
@@ -1895,15 +1906,15 @@ async function pruneSessions(options = {}) {
1895
1906
  dryRun: false
1896
1907
  };
1897
1908
  }
1898
- function filterPruneCandidates(entries, agentCommand) {
1899
- return entries.filter((entry) => entry.closed && (!agentCommand || entry.agentCommand === agentCommand));
1909
+ function isPruneCandidate(record, agentCommand) {
1910
+ return record.closed === true && (!agentCommand || record.agentCommand === agentCommand);
1900
1911
  }
1901
- async function loadPrunableRecords(entries, cutoff) {
1912
+ async function loadPrunableRecords(entries, cutoff, agentCommand) {
1902
1913
  const records = [];
1903
1914
  const cutoffIso = cutoff?.toISOString();
1904
1915
  for (const entry of entries) {
1905
1916
  const record = await loadRecordFromIndexEntry(entry);
1906
- if (record && isBeforeCutoff(record, cutoffIso)) records.push(record);
1917
+ if (record && isPruneCandidate(record, agentCommand) && isBeforeCutoff(record, cutoffIso)) records.push(record);
1907
1918
  }
1908
1919
  return records;
1909
1920
  }
@@ -2124,6 +2135,156 @@ function queueSocketPath(sessionId, homeDir = os.homedir()) {
2124
2135
  return path.join(queueSocketBaseDir(homeDir) ?? "/tmp", `${key}.sock`);
2125
2136
  }
2126
2137
  //#endregion
2138
+ //#region src/session/queue/lease-mutation.ts
2139
+ const GUARD_WAIT_MS = 2e3;
2140
+ const localMutations = /* @__PURE__ */ new Map();
2141
+ var QueueLeaseGuardSettlementError = class extends QueueConnectionError {
2142
+ constructor(errors) {
2143
+ super("Queue lease guard settlement failed; cleanup remains retryable", {
2144
+ detailCode: "QUEUE_LEASE_GUARD_SETTLEMENT_FAILED",
2145
+ origin: "queue",
2146
+ retryable: false,
2147
+ cause: new AggregateError(errors, "Queue lease mutation and settlement errors")
2148
+ });
2149
+ }
2150
+ };
2151
+ async function canonicalLeasePath(sessionId) {
2152
+ const requested = queueLockFilePath(sessionId);
2153
+ try {
2154
+ return path.join(await fs$1.realpath(path.dirname(requested)), path.basename(requested));
2155
+ } catch (error) {
2156
+ if (error.code === "ENOENT") return path.resolve(requested);
2157
+ throw error;
2158
+ }
2159
+ }
2160
+ function guardOwnerHasExited({ payload }) {
2161
+ return payload !== null && typeof payload === "object" && "pid" in payload && typeof payload.pid === "number" && isProcessDefinitelyDead(payload.pid);
2162
+ }
2163
+ async function acquireGuard(lockPath) {
2164
+ return await acquireFileLock(lockPath, {
2165
+ managerKey: "acpx.queue-lease-mutation",
2166
+ lockPath: `${lockPath}.guard`,
2167
+ staleMs: Infinity,
2168
+ timeoutMs: GUARD_WAIT_MS,
2169
+ retry: {
2170
+ minTimeout: 5,
2171
+ maxTimeout: 15,
2172
+ factor: 1,
2173
+ randomize: false
2174
+ },
2175
+ staleRecovery: "remove-if-unchanged",
2176
+ payload: () => ({ pid: process.pid }),
2177
+ shouldReclaim: guardOwnerHasExited,
2178
+ shouldRemoveStaleLock: guardOwnerHasExited
2179
+ });
2180
+ }
2181
+ function matchesReservation(value, expected) {
2182
+ if (!value || typeof value !== "object") return false;
2183
+ const record = value;
2184
+ return record.pid === expected.pid && record.sessionId === expected.sessionId && record.ownerGeneration === expected.ownerGeneration;
2185
+ }
2186
+ async function rollbackReservation(lockPath, expected) {
2187
+ try {
2188
+ if (!(await fs$1.lstat(lockPath)).isFile()) return;
2189
+ const raw = await fs$1.readFile(lockPath, "utf8");
2190
+ let value;
2191
+ try {
2192
+ value = JSON.parse(raw);
2193
+ } catch {
2194
+ return;
2195
+ }
2196
+ if (matchesReservation(value, expected)) await fs$1.unlink(lockPath);
2197
+ } catch (error) {
2198
+ if (error.code !== "ENOENT") throw error;
2199
+ }
2200
+ }
2201
+ async function settleReceipt(lockPath, receipt) {
2202
+ if (receipt.reservation) {
2203
+ if (!await receipt.guard.verifyStillHeld()) {
2204
+ await receipt.guard.release();
2205
+ receipt.guard = await acquireGuard(lockPath);
2206
+ }
2207
+ await rollbackReservation(lockPath, receipt.reservation);
2208
+ receipt.reservation = void 0;
2209
+ }
2210
+ await receipt.guard.release();
2211
+ }
2212
+ async function settleFailedGuard(lockPath, state) {
2213
+ const failed = state.failed;
2214
+ if (!failed) return;
2215
+ try {
2216
+ await settleReceipt(lockPath, failed);
2217
+ if (state.failed === failed) state.failed = void 0;
2218
+ } catch (error) {
2219
+ throw new QueueLeaseGuardSettlementError([error]);
2220
+ }
2221
+ }
2222
+ async function locallySerialized(lockPath, run) {
2223
+ let state = localMutations.get(lockPath);
2224
+ if (!state) {
2225
+ state = { tail: Promise.resolve() };
2226
+ localMutations.set(lockPath, state);
2227
+ }
2228
+ const current = state;
2229
+ const operation = current.tail.then(async () => await run(current));
2230
+ const tail = operation.then(() => {}, () => {});
2231
+ current.tail = tail;
2232
+ try {
2233
+ return await operation;
2234
+ } finally {
2235
+ if (current.tail === tail && !current.failed && localMutations.get(lockPath) === current) localMutations.delete(lockPath);
2236
+ }
2237
+ }
2238
+ async function settlePendingQueueLeaseGuard(sessionId) {
2239
+ if (localMutations.size === 0) return;
2240
+ const lockPath = await canonicalLeasePath(sessionId);
2241
+ if (!localMutations.has(lockPath)) return;
2242
+ await locallySerialized(lockPath, async (state) => await settleFailedGuard(lockPath, state));
2243
+ }
2244
+ async function withQueueLeaseMutation(sessionId, mutate, reservation) {
2245
+ await fs$1.mkdir(path.dirname(queueLockFilePath(sessionId)), {
2246
+ recursive: true,
2247
+ mode: 448
2248
+ });
2249
+ const lockPath = await canonicalLeasePath(sessionId);
2250
+ const expected = reservation ? {
2251
+ pid: reservation.pid,
2252
+ sessionId: reservation.sessionId,
2253
+ ownerGeneration: reservation.ownerGeneration
2254
+ } : void 0;
2255
+ const publishedReservation = () => reservation?.published ? expected : void 0;
2256
+ return await locallySerialized(lockPath, async (state) => {
2257
+ await settleFailedGuard(lockPath, state);
2258
+ const guard = await acquireGuard(lockPath);
2259
+ let outcome;
2260
+ try {
2261
+ outcome = {
2262
+ ok: true,
2263
+ value: await mutate()
2264
+ };
2265
+ } catch (error) {
2266
+ outcome = {
2267
+ ok: false,
2268
+ error
2269
+ };
2270
+ }
2271
+ const published = publishedReservation();
2272
+ const receipt = {
2273
+ guard,
2274
+ reservation: outcome.ok ? void 0 : published
2275
+ };
2276
+ try {
2277
+ await settleReceipt(lockPath, receipt);
2278
+ } catch (error) {
2279
+ if (outcome.ok) receipt.reservation = published;
2280
+ state.failed = receipt;
2281
+ throw new QueueLeaseGuardSettlementError(outcome.ok ? [error] : [outcome.error, error]);
2282
+ }
2283
+ if (!outcome.ok) throw outcome.error;
2284
+ return outcome.value;
2285
+ });
2286
+ }
2287
+ //#endregion
2127
2288
  //#region src/session/queue/lease-store.ts
2128
2289
  const PROCESS_SIGTERM_GRACE_MS = process.platform === "win32" ? 4e3 : 12e3;
2129
2290
  const PROCESS_SIGKILL_GRACE_MS = 1500;
@@ -2141,12 +2302,18 @@ function parseQueueOwnerRecord(raw) {
2141
2302
  heartbeatAt: record.heartbeatAt,
2142
2303
  ownerGeneration: record.ownerGeneration,
2143
2304
  queueDepth: record.queueDepth,
2144
- ...record.sharedRuntime === true ? { sharedRuntime: true } : {},
2145
- ...record.sessionWatch === true ? { sessionWatch: true } : {},
2305
+ ...parseQueueOwnerCapabilities(record),
2146
2306
  ...typeof record.mcpConfigPath === "string" ? { mcpConfigPath: record.mcpConfigPath } : {},
2147
2307
  ...typeof record.mcpConfigFingerprint === "string" ? { mcpConfigFingerprint: record.mcpConfigFingerprint } : {}
2148
2308
  };
2149
2309
  }
2310
+ function parseQueueOwnerCapabilities(record) {
2311
+ return {
2312
+ ...record.sharedRuntime === true ? { sharedRuntime: true } : {},
2313
+ ...record.sessionWatch === true ? { sessionWatch: true } : {},
2314
+ ...record.persistsControlState === true ? { persistsControlState: true } : {}
2315
+ };
2316
+ }
2150
2317
  function hasValidQueueOwnerRecordFields(record) {
2151
2318
  return !Array.isArray(record) && isPositiveInteger(record.pid) && typeof record.sessionId === "string" && typeof record.socketPath === "string" && typeof record.createdAt === "string" && typeof record.heartbeatAt === "string" && isPositiveInteger(record.ownerGeneration) && isNonNegativeInteger(record.queueDepth);
2152
2319
  }
@@ -2199,15 +2366,20 @@ async function removeSocketFile(socketPath) {
2199
2366
  if (error.code !== "ENOENT") throw error;
2200
2367
  }
2201
2368
  }
2202
- async function waitForProcessExit(pid, timeoutMs) {
2369
+ async function waitForProcessExit(pid, timeoutMs, hasExited) {
2203
2370
  const deadline = Date.now() + Math.max(0, timeoutMs);
2204
2371
  while (Date.now() <= deadline) {
2205
- if (!isProcessAlive(pid)) return true;
2372
+ if (hasExited(pid)) return true;
2206
2373
  await waitMs(PROCESS_POLL_MS);
2207
2374
  }
2208
- return !isProcessAlive(pid);
2375
+ return hasExited(pid);
2209
2376
  }
2210
2377
  async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
2378
+ await withQueueLeaseMutation(sessionId, async () => {
2379
+ await cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent);
2380
+ });
2381
+ }
2382
+ async function cleanupGuardedQueueOwnerFiles(sessionId, socketPath, isCurrent) {
2211
2383
  if (!await isCurrent()) return;
2212
2384
  await removeSocketFile(socketPath).catch(() => {});
2213
2385
  if (!await isCurrent()) return;
@@ -2217,7 +2389,10 @@ async function cleanupQueueOwnerFiles(sessionId, socketPath, isCurrent) {
2217
2389
  }
2218
2390
  async function ownsQueueLease(owner, requireStale = false) {
2219
2391
  const current = await readQueueOwnerRecord(owner.sessionId);
2220
- return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && current.sessionId === owner.sessionId && (!requireStale || !ownerIsAlive(current) || isQueueOwnerHeartbeatStale(current));
2392
+ return matchesQueueOwner(current, owner) && (!requireStale || isQueueOwnerHeartbeatStale(current));
2393
+ }
2394
+ function matchesQueueOwner(current, expected) {
2395
+ return current?.sessionId === expected.sessionId && current.pid === expected.pid && current.ownerGeneration === expected.ownerGeneration;
2221
2396
  }
2222
2397
  function ownerIsAlive(owner) {
2223
2398
  return owner.pid === process.pid || isProcessAlive(owner.pid);
@@ -2234,37 +2409,34 @@ async function readQueueOwnerRecord(sessionId) {
2234
2409
  }
2235
2410
  async function terminateProcess(pid, beforeSignal) {
2236
2411
  if (!isProcessAlive(pid)) return false;
2237
- for (const [signal, graceMs] of [["SIGTERM", PROCESS_SIGTERM_GRACE_MS], ["SIGKILL", PROCESS_SIGKILL_GRACE_MS]]) {
2412
+ return await terminateWithDispatch(pid, async (signal) => {
2238
2413
  if (beforeSignal && !await beforeSignal(signal)) return false;
2239
- try {
2240
- process.kill(pid, signal);
2241
- } catch {
2242
- return false;
2243
- }
2244
- if (await waitForProcessExit(pid, graceMs)) return true;
2414
+ return dispatchSignal(pid, signal);
2415
+ }, (target) => !isProcessAlive(target));
2416
+ }
2417
+ function dispatchSignal(pid, signal) {
2418
+ try {
2419
+ process.kill(pid, signal);
2420
+ return true;
2421
+ } catch {
2422
+ return false;
2423
+ }
2424
+ }
2425
+ async function terminateWithDispatch(pid, dispatch, hasExited) {
2426
+ for (const [signal, graceMs] of [["SIGTERM", PROCESS_SIGTERM_GRACE_MS], ["SIGKILL", PROCESS_SIGKILL_GRACE_MS]]) {
2427
+ if (!await dispatch(signal)) return false;
2428
+ if (await waitForProcessExit(pid, graceMs, hasExited)) return true;
2245
2429
  }
2246
2430
  return false;
2247
2431
  }
2248
2432
  async function resolveUsableQueueOwner(sessionId, owner) {
2249
- if (ownerIsAlive(owner) && !isQueueOwnerHeartbeatStale(owner)) return owner;
2250
- await terminateQueueOwnerForSession(sessionId, owner, true);
2251
- const current = await readQueueOwnerRecord(sessionId);
2252
- return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration && ownerIsAlive(current) && !isQueueOwnerHeartbeatStale(current) ? current : void 0;
2253
- }
2254
- async function readQueueOwnerStatus(sessionId) {
2433
+ await settlePendingQueueLeaseGuard(sessionId);
2255
2434
  const observed = await readQueueOwnerRecord(sessionId);
2256
- if (!observed) return;
2257
- const owner = await resolveUsableQueueOwner(sessionId, observed);
2258
- if (!owner) return;
2259
- return {
2260
- pid: owner.pid,
2261
- socketPath: owner.socketPath,
2262
- heartbeatAt: owner.heartbeatAt,
2263
- ownerGeneration: owner.ownerGeneration,
2264
- queueDepth: owner.queueDepth,
2265
- alive: true,
2266
- stale: isQueueOwnerHeartbeatStale(owner)
2267
- };
2435
+ if (!matchesQueueOwner(observed, owner)) return;
2436
+ if (ownerIsAlive(observed) && !isQueueOwnerHeartbeatStale(observed)) return observed;
2437
+ await terminateQueueOwnerForSession(sessionId, observed, true);
2438
+ const current = await readQueueOwnerRecord(sessionId);
2439
+ return matchesQueueOwner(current, observed) && ownerIsAlive(current) && !isQueueOwnerHeartbeatStale(current) ? current : void 0;
2268
2440
  }
2269
2441
  async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, nowIsoFactory = nowIso) {
2270
2442
  const { mcpConfigPath, clock } = resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory);
@@ -2285,21 +2457,30 @@ async function tryAcquireQueueOwnerLease(sessionId, mcpConfigOrNowIsoFactory, no
2285
2457
  updates: Promise.resolve(),
2286
2458
  released: false
2287
2459
  };
2460
+ const reservation = {
2461
+ pid: lease.pid,
2462
+ sessionId,
2463
+ ownerGeneration,
2464
+ published: false
2465
+ };
2288
2466
  try {
2289
- await stageQueueOwnerRecord(lease, 0, () => createdAt, async (tempPath, payload) => {
2290
- try {
2291
- await fs$1.link(tempPath, lockPath);
2292
- } catch (error) {
2293
- if (!isHardlinkFallbackError(error)) throw error;
2294
- await fs$1.writeFile(lockPath, payload, {
2295
- encoding: "utf8",
2296
- flag: "wx",
2297
- mode: 384
2298
- });
2299
- }
2300
- });
2301
- await removeSocketFile(socketPath).catch(() => {});
2302
- return lease;
2467
+ return await withQueueLeaseMutation(sessionId, async () => {
2468
+ await stageQueueOwnerRecord(lease, 0, clock, async (tempPath, payload) => {
2469
+ try {
2470
+ await fs$1.link(tempPath, lockPath);
2471
+ } catch (error) {
2472
+ if (!isHardlinkFallbackError(error)) throw error;
2473
+ await fs$1.writeFile(lockPath, payload, {
2474
+ encoding: "utf8",
2475
+ flag: "wx",
2476
+ mode: 384
2477
+ });
2478
+ }
2479
+ reservation.published = true;
2480
+ });
2481
+ await removeSocketFile(socketPath).catch(() => {});
2482
+ return lease;
2483
+ }, reservation);
2303
2484
  } catch (error) {
2304
2485
  return await handleLeaseCollision(sessionId, error);
2305
2486
  }
@@ -2324,18 +2505,20 @@ async function handleLeaseCollision(sessionId, error) {
2324
2505
  }
2325
2506
  async function cleanupAbandonedReservation(sessionId) {
2326
2507
  const lockPath = queueLockFilePath(sessionId);
2327
- try {
2328
- const observed = await fs$1.lstat(lockPath, { bigint: true });
2329
- if (!observed.isFile() || Date.now() - Number(observed.mtimeMs) <= QUEUE_OWNER_STALE_HEARTBEAT_MS) return;
2330
- const raw = await fs$1.readFile(lockPath, "utf8");
2331
- if (await readQueueOwnerRecord(sessionId)) return;
2332
- await cleanupQueueOwnerFiles(sessionId, queueSocketPath(sessionId), async () => {
2333
- const current = await fs$1.lstat(lockPath, { bigint: true });
2334
- return current.isFile() && current.dev === observed.dev && current.ino === observed.ino && current.mtimeNs === observed.mtimeNs && await fs$1.readFile(lockPath, "utf8") === raw;
2335
- });
2336
- } catch (error) {
2337
- if (error.code !== "ENOENT") throw error;
2338
- }
2508
+ await withQueueLeaseMutation(sessionId, async () => {
2509
+ try {
2510
+ const observed = await fs$1.lstat(lockPath, { bigint: true });
2511
+ if (!observed.isFile() || Date.now() - Number(observed.mtimeMs) <= QUEUE_OWNER_STALE_HEARTBEAT_MS) return;
2512
+ const raw = await fs$1.readFile(lockPath, "utf8");
2513
+ if (await readQueueOwnerRecord(sessionId)) return;
2514
+ await cleanupGuardedQueueOwnerFiles(sessionId, queueSocketPath(sessionId), async () => {
2515
+ const current = await fs$1.lstat(lockPath, { bigint: true });
2516
+ return current.isFile() && current.dev === observed.dev && current.ino === observed.ino && current.mtimeNs === observed.mtimeNs && await fs$1.readFile(lockPath, "utf8") === raw;
2517
+ });
2518
+ } catch (error) {
2519
+ if (error.code !== "ENOENT") throw error;
2520
+ }
2521
+ });
2339
2522
  }
2340
2523
  function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
2341
2524
  if (typeof mcpConfigOrNowIsoFactory === "string") return {
@@ -2358,8 +2541,11 @@ function resolveLeaseArguments(mcpConfigOrNowIsoFactory, nowIsoFactory) {
2358
2541
  function refreshQueueOwnerLease(lease, options, nowIsoFactory = nowIso) {
2359
2542
  if (lease.released) return Promise.resolve();
2360
2543
  const update = lease.updates.then(async () => {
2361
- await stageQueueOwnerRecord(lease, options.queueDepth, nowIsoFactory, async (tempPath) => {
2362
- if (await ownsQueueLease(lease)) await fs$1.rename(tempPath, lease.lockPath);
2544
+ await withQueueLeaseMutation(lease.sessionId, async () => {
2545
+ if (!await ownsQueueLease(lease)) return;
2546
+ await stageQueueOwnerRecord(lease, options.queueDepth, nowIsoFactory, async (tempPath) => {
2547
+ await fs$1.rename(tempPath, lease.lockPath);
2548
+ });
2363
2549
  });
2364
2550
  });
2365
2551
  lease.updates = update.catch(() => {});
@@ -2376,6 +2562,7 @@ async function stageQueueOwnerRecord(lease, queueDepth, clock, publish) {
2376
2562
  queueDepth: Math.max(0, Math.round(queueDepth)),
2377
2563
  sharedRuntime: true,
2378
2564
  sessionWatch: true,
2565
+ persistsControlState: true,
2379
2566
  ...lease.mcpConfigPath ? { mcpConfigPath: lease.mcpConfigPath } : {},
2380
2567
  ...lease.mcpConfigFingerprint ? { mcpConfigFingerprint: lease.mcpConfigFingerprint } : {}
2381
2568
  }, null, 2);
@@ -2398,11 +2585,15 @@ async function releaseQueueOwnerLease(lease) {
2398
2585
  await cleanupQueueOwnerFiles(lease.sessionId, lease.socketPath, () => ownsQueueLease(lease));
2399
2586
  }
2400
2587
  async function terminateQueueOwnerForSession(sessionId, expectedOwner, requireStale = false) {
2588
+ await settlePendingQueueLeaseGuard(sessionId);
2401
2589
  const owner = expectedOwner ?? await readQueueOwnerRecord(sessionId);
2402
2590
  if (!owner || owner.sessionId !== sessionId) return;
2403
- if (ownerIsAlive(owner)) await terminateProcess(owner.pid, (signal) => ownsQueueLease(owner, requireStale && signal === "SIGTERM"));
2404
- if (ownerIsAlive(owner)) return;
2405
- await cleanupQueueOwnerFiles(sessionId, owner.socketPath, () => ownsQueueLease(owner));
2591
+ if (owner.pid !== process.pid && isProcessAlive(owner.pid)) await terminateWithDispatch(owner.pid, (signal) => withQueueLeaseMutation(sessionId, async () => {
2592
+ if (!await ownsQueueLease(owner, requireStale && signal === "SIGTERM")) return false;
2593
+ return dispatchSignal(owner.pid, signal);
2594
+ }), isProcessDefinitelyDead);
2595
+ if (!isProcessDefinitelyDead(owner.pid)) return;
2596
+ await cleanupQueueOwnerFiles(sessionId, owner.socketPath, async () => isProcessDefinitelyDead(owner.pid) && await ownsQueueLease(owner));
2406
2597
  }
2407
2598
  async function waitMs(ms) {
2408
2599
  await new Promise((resolve) => {
@@ -2456,7 +2647,12 @@ async function connectToQueueOwner(owner, maxAttempts = QUEUE_CONNECT_ATTEMPTS)
2456
2647
  }
2457
2648
  //#endregion
2458
2649
  //#region src/session/queue/ipc-health.ts
2650
+ async function isStillCurrent(owner) {
2651
+ const current = await readQueueOwnerRecord(owner.sessionId);
2652
+ return current?.pid === owner.pid && current.ownerGeneration === owner.ownerGeneration;
2653
+ }
2459
2654
  async function probeQueueOwnerHealth(sessionId) {
2655
+ await settlePendingQueueLeaseGuard(sessionId);
2460
2656
  const ownerRecord = await readQueueOwnerRecord(sessionId);
2461
2657
  if (!ownerRecord) return {
2462
2658
  sessionId,
@@ -2465,7 +2661,7 @@ async function probeQueueOwnerHealth(sessionId) {
2465
2661
  socketReachable: false,
2466
2662
  pidAlive: false
2467
2663
  };
2468
- const owner = await readQueueOwnerStatus(sessionId);
2664
+ const owner = await resolveUsableQueueOwner(sessionId, ownerRecord);
2469
2665
  if (!owner) return {
2470
2666
  sessionId,
2471
2667
  hasLease: false,
@@ -2473,10 +2669,9 @@ async function probeQueueOwnerHealth(sessionId) {
2473
2669
  socketReachable: false,
2474
2670
  pidAlive: false
2475
2671
  };
2476
- const pidAlive = owner.alive;
2477
2672
  let socketReachable = false;
2478
2673
  try {
2479
- const socket = await connectToQueueOwner(ownerRecord, 2);
2674
+ const socket = await connectToQueueOwner(owner, 2);
2480
2675
  if (socket) {
2481
2676
  socketReachable = true;
2482
2677
  if (!socket.destroyed) socket.end();
@@ -2484,12 +2679,19 @@ async function probeQueueOwnerHealth(sessionId) {
2484
2679
  } catch {
2485
2680
  socketReachable = false;
2486
2681
  }
2682
+ if (!await isStillCurrent(owner)) return {
2683
+ sessionId,
2684
+ hasLease: false,
2685
+ healthy: false,
2686
+ socketReachable: false,
2687
+ pidAlive: false
2688
+ };
2487
2689
  return {
2488
2690
  sessionId,
2489
2691
  hasLease: true,
2490
2692
  healthy: socketReachable,
2491
2693
  socketReachable,
2492
- pidAlive,
2694
+ pidAlive: true,
2493
2695
  pid: owner.pid,
2494
2696
  socketPath: owner.socketPath,
2495
2697
  ownerGeneration: owner.ownerGeneration,
@@ -3051,6 +3253,12 @@ async function runQueueOwnerRequest(options) {
3051
3253
  onAbort();
3052
3254
  return;
3053
3255
  }
3256
+ try {
3257
+ options.assertDispatch?.();
3258
+ } catch (error) {
3259
+ finishReject(error);
3260
+ return;
3261
+ }
3054
3262
  try {
3055
3263
  socket.write(`${requestLine}\n`);
3056
3264
  } catch (error) {
@@ -3164,10 +3372,11 @@ async function submitToQueueOwner(owner, options) {
3164
3372
  }
3165
3373
  });
3166
3374
  }
3167
- async function submitControlToQueueOwner(owner, request, isExpectedResponse) {
3375
+ async function submitControlToQueueOwner(owner, request, isExpectedResponse, assertDispatch) {
3168
3376
  return await runQueueOwnerRequest({
3169
3377
  owner,
3170
3378
  request,
3379
+ assertDispatch,
3171
3380
  onMessage: (message, { state, resolve, reject }) => {
3172
3381
  if (message.type === "error") {
3173
3382
  reject(queueConnectionErrorFromOwner(message, false));
@@ -3209,25 +3418,25 @@ async function submitCancelToQueueOwner(owner, targetRequestId) {
3209
3418
  ...targetRequestId !== void 0 ? { targetRequestId } : {}
3210
3419
  }, (message) => message.type === "cancel_result"))?.cancelled;
3211
3420
  }
3212
- async function submitSetModeToQueueOwner(owner, modeId, timeoutMs) {
3421
+ async function submitSetModeToQueueOwner(owner, modeId, timeoutMs, assertDispatch) {
3213
3422
  return await submitControlToQueueOwner(owner, {
3214
3423
  type: "set_mode",
3215
3424
  requestId: randomUUID(),
3216
3425
  ownerGeneration: owner.ownerGeneration,
3217
3426
  modeId,
3218
3427
  timeoutMs
3219
- }, (message) => message.type === "set_mode_result") ? true : void 0;
3428
+ }, (message) => message.type === "set_mode_result", assertDispatch) ? true : void 0;
3220
3429
  }
3221
- async function submitSetModelToQueueOwner(owner, modelId, timeoutMs) {
3430
+ async function submitSetModelToQueueOwner(owner, modelId, timeoutMs, assertDispatch) {
3222
3431
  return await submitControlToQueueOwner(owner, {
3223
3432
  type: "set_model",
3224
3433
  requestId: randomUUID(),
3225
3434
  ownerGeneration: owner.ownerGeneration,
3226
3435
  modelId,
3227
3436
  timeoutMs
3228
- }, (message) => message.type === "set_model_result");
3437
+ }, (message) => message.type === "set_model_result", assertDispatch);
3229
3438
  }
3230
- async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs) {
3439
+ async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs, assertDispatch) {
3231
3440
  return (await submitControlToQueueOwner(owner, {
3232
3441
  type: "set_config_option",
3233
3442
  requestId: randomUUID(),
@@ -3235,7 +3444,7 @@ async function submitSetConfigOptionToQueueOwner(owner, configId, value, timeout
3235
3444
  configId,
3236
3445
  value,
3237
3446
  timeoutMs
3238
- }, (message) => message.type === "set_config_option_result"))?.response;
3447
+ }, (message) => message.type === "set_config_option_result", assertDispatch))?.response;
3239
3448
  }
3240
3449
  async function submitCloseSessionToQueueOwner(owner, timeoutMs) {
3241
3450
  return (await submitControlToQueueOwner(owner, {
@@ -3317,34 +3526,42 @@ async function tryCancelOnRunningOwner(options) {
3317
3526
  submit: (owner) => submitCancelToQueueOwner(owner, options.targetRequestId)
3318
3527
  });
3319
3528
  }
3320
- async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose) {
3529
+ async function withOwnerControlPersistence(owner, operation) {
3530
+ const persistsControlState = owner.persistsControlState === true;
3531
+ const value = await operation;
3532
+ return value === void 0 ? void 0 : {
3533
+ value,
3534
+ persistsControlState
3535
+ };
3536
+ }
3537
+ async function trySetModeOnRunningOwner(sessionId, modeId, timeoutMs, verbose, assertDispatch) {
3321
3538
  return await tryControlOnRunningOwner({
3322
3539
  sessionId,
3323
3540
  verbose,
3324
3541
  requestName: "set_mode",
3325
3542
  logPrefix: "[acpx] requested session/set_mode on owner pid",
3326
- submit: (owner) => submitSetModeToQueueOwner(owner, modeId, timeoutMs)
3543
+ submit: (owner) => withOwnerControlPersistence(owner, submitSetModeToQueueOwner(owner, modeId, timeoutMs, assertDispatch))
3327
3544
  });
3328
3545
  }
3329
- async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose) {
3546
+ async function trySetModelOnRunningOwner(sessionId, modelId, timeoutMs, verbose, assertDispatch) {
3330
3547
  return await tryControlOnRunningOwner({
3331
3548
  sessionId,
3332
3549
  verbose,
3333
3550
  requestName: "set_model",
3334
3551
  logPrefix: "[acpx] requested a model config update on owner pid",
3335
- submit: (owner) => submitSetModelToQueueOwner(owner, modelId, timeoutMs)
3552
+ submit: (owner) => withOwnerControlPersistence(owner, submitSetModelToQueueOwner(owner, modelId, timeoutMs, assertDispatch))
3336
3553
  });
3337
3554
  }
3338
- async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose) {
3555
+ async function trySetConfigOptionOnRunningOwner(sessionId, configId, value, timeoutMs, verbose, assertDispatch) {
3339
3556
  return await tryControlOnRunningOwner({
3340
3557
  sessionId,
3341
3558
  verbose,
3342
3559
  requestName: "set_config_option",
3343
3560
  logPrefix: "[acpx] requested session/set_config_option on owner pid",
3344
- submit: (owner) => submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs)
3561
+ submit: (owner) => withOwnerControlPersistence(owner, submitSetConfigOptionToQueueOwner(owner, configId, value, timeoutMs, assertDispatch))
3345
3562
  });
3346
3563
  }
3347
3564
  //#endregion
3348
- export { startPerfTimer as $, listSessions as A, AgentSpawnError as At, extractAgentSessionId as B, SessionModeReplayError as Bt, textPrompt as C, OUTPUT_ERROR_ORIGINS as Ct, findSession as D, SESSION_RECORD_SCHEMA as Dt, findGitRepositoryRoot as E, PERMISSION_POLICY_ACTIONS as Et, resolveSessionRecord as F, GeminiAcpStartupTimeoutError as Ft, watchSession as G, SESSION_JOURNAL_SCHEMA as H, SessionNotFoundError as Ht, writeSessionRecord as I, PermissionDeniedError as It, incrementPerfCounter as J, formatPerfMetric as K, assertPersistedKeyPolicy as L, PermissionPromptUnavailableError as Lt, normalizeName as M, AuthPolicyError as Mt, pruneSessions as N, ClaudeAcpSessionCreateTimeoutError as Nt, findSessionByDirectoryWalk as O, AcpxOperationalError as Ot, readSessionRecord as P, CopilotAcpUnsupportedError as Pt, setPerfGauge as Q, parseSessionRecord as R, QueueConnectionError as Rt, promptToDisplayText as S, OUTPUT_ERROR_CODES as St, absolutePath as T, PERMISSION_MODES as Tt, SessionJournalReader as U, SessionResumeRequiredError as Ut, normalizeAgentSessionId as V, SessionModelReplayError as Vt, SessionWatchError as W, UnsupportedPromptContentError as Wt, recordPerfDuration as X, measurePerf as Y, resetPerfMetrics as Z, waitMs as _, formatUnknownErrorMessage as _t, trySetModelOnRunningOwner as a, parsePermissionNotice as at, mergePromptSourceWithText as b, EXIT_CODES as bt, queueRequestExceedsLimit as c, defaultSessionEventLog as ct, readQueueOwnerRecord as d, sessionEventLockPath as dt, extractSessionUpdateNotification as et, refreshQueueOwnerLease as f, sessionEventSegmentPath as ft, tryAcquireQueueOwnerLease as g, extractAcpError as gt, terminateQueueOwnerForSession as h, isProcessAlive as ht, trySetModeOnRunningOwner as i, parseJsonRpcErrorMessage as it, listSessionsForAgent as j, AgentStartupError as jt, isoNow as k, AgentDisconnectedError as kt, parseQueueRequest as l, sessionBaseDir as lt, terminateProcess as m, writePrivateJsonFile as mt, tryCloseSessionOnRunningOwner as n, isAcpMessageObject as nt, trySubmitToRunningOwner as o, parsePromptStopReason as ot, releaseQueueOwnerLease as p, writePrivateFile as pt, getPerfMetricsSnapshot as q, trySetConfigOptionOnRunningOwner as r, isSessionUpdateNotification as rt, queueRequestByteLimit as s, DEFAULT_EVENT_SEGMENT_MAX_BYTES as st, tryCancelOnRunningOwner as t, isAcpJsonRpcMessage as tt, probeQueueOwnerHealth as u, sessionEventActivePath as ut, PromptInputValidationError as v, isAcpResourceNotFoundError as vt, DEFAULT_HISTORY_LIMIT as w, OUTPUT_FORMATS as wt, parsePromptSource as x, NON_INTERACTIVE_PERMISSION_POLICIES as xt, getUnsupportedPromptContentMessage as y, AUTH_POLICIES as yt, serializeSessionRecordForDisk as z, SessionConfigOptionReplayError as zt };
3565
+ export { setPerfGauge as $, isoNow as A, AcpxOperationalError as At, serializeSessionRecordForDisk as B, QueueConnectionError as Bt, promptToDisplayText as C, NON_INTERACTIVE_PERMISSION_POLICIES as Ct, findGitRepositoryRoot as D, PERMISSION_MODES as Dt, absolutePath as E, OUTPUT_FORMATS as Et, readSessionRecord as F, ClaudeAcpSessionCreateTimeoutError as Ft, SessionWatchError as G, SessionResumeRequiredError as Gt, normalizeAgentSessionId as H, SessionModeReplayError as Ht, resolveSessionRecord as I, CopilotAcpUnsupportedError as It, getPerfMetricsSnapshot as J, watchSession as K, UnsupportedPromptContentError as Kt, writeSessionRecord as L, GeminiAcpStartupTimeoutError as Lt, listSessionsForAgent as M, AgentSpawnError as Mt, normalizeName as N, AgentStartupError as Nt, findSession as O, PERMISSION_POLICY_ACTIONS as Ot, pruneSessions as P, AuthPolicyError as Pt, resetPerfMetrics as Q, assertPersistedKeyPolicy as R, PermissionDeniedError as Rt, parsePromptSource as S, EXIT_CODES as St, DEFAULT_HISTORY_LIMIT as T, OUTPUT_ERROR_ORIGINS as Tt, SESSION_JOURNAL_SCHEMA as U, SessionModelReplayError as Ut, extractAgentSessionId as V, SessionConfigOptionReplayError as Vt, SessionJournalReader as W, SessionNotFoundError as Wt, measurePerf as X, incrementPerfCounter as Y, recordPerfDuration as Z, waitMs as _, isProcessDefinitelyDead as _t, trySetModelOnRunningOwner as a, parseJsonRpcErrorMessage as at, getUnsupportedPromptContentMessage as b, isAcpResourceNotFoundError as bt, queueRequestExceedsLimit as c, DEFAULT_EVENT_SEGMENT_MAX_BYTES as ct, readQueueOwnerRecord as d, sessionEventActivePath as dt, startPerfTimer as et, refreshQueueOwnerLease as f, sessionEventLockPath as ft, tryAcquireQueueOwnerLease as g, isProcessAlive as gt, terminateQueueOwnerForSession as h, writePrivateJsonFile as ht, trySetModeOnRunningOwner as i, isSessionUpdateNotification as it, listSessions as j, AgentDisconnectedError as jt, findSessionByDirectoryWalk as k, SESSION_RECORD_SCHEMA as kt, parseQueueRequest as l, defaultSessionEventLog as lt, terminateProcess as m, writePrivateFile as mt, tryCloseSessionOnRunningOwner as n, isAcpJsonRpcMessage as nt, trySubmitToRunningOwner as o, parsePermissionNotice as ot, releaseQueueOwnerLease as p, sessionEventSegmentPath as pt, formatPerfMetric as q, trySetConfigOptionOnRunningOwner as r, isAcpMessageObject as rt, queueRequestByteLimit as s, parsePromptStopReason as st, tryCancelOnRunningOwner as t, extractSessionUpdateNotification as tt, probeQueueOwnerHealth as u, sessionBaseDir as ut, QueueLeaseGuardSettlementError as v, extractAcpError as vt, textPrompt as w, OUTPUT_ERROR_CODES as wt, mergePromptSourceWithText as x, AUTH_POLICIES as xt, PromptInputValidationError as y, formatUnknownErrorMessage as yt, parseSessionRecord as z, PermissionPromptUnavailableError as zt };
3349
3566
 
3350
- //# sourceMappingURL=ipc-D3akHMEq.js.map
3567
+ //# sourceMappingURL=ipc-BJrvfNXr.js.map