commonswarm 0.1.23 → 0.1.25

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/cswarm.cjs +1307 -129
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -4381,8 +4381,8 @@ var require_RealtimeChannel = __commonJS({
4381
4381
  }
4382
4382
  /** @internal */
4383
4383
  _notThisChannelEvent(event, ref) {
4384
- const { close, error, leave, join: join14 } = constants_1.CHANNEL_EVENTS;
4385
- const events = [close, error, leave, join14];
4384
+ const { close, error, leave, join: join17 } = constants_1.CHANNEL_EVENTS;
4385
+ const events = [close, error, leave, join17];
4386
4386
  return ref && events.includes(event) && ref !== this.joinPush.ref;
4387
4387
  }
4388
4388
  /** @internal */
@@ -13505,11 +13505,14 @@ __export(cli_exports, {
13505
13505
  EXIT_RESTARTABLE: () => EXIT_RESTARTABLE,
13506
13506
  TURN_BUDGET_CREDENTIAL_MARGIN_MS: () => TURN_BUDGET_CREDENTIAL_MARGIN_MS,
13507
13507
  clampTurnBudgetToCredential: () => clampTurnBudgetToCredential,
13508
+ claudeUserPromptHookSnippet: () => claudeUserPromptHookSnippet,
13508
13509
  describeAudience: () => describeAudience,
13509
13510
  listenerFailureMessage: () => listenerFailureMessage,
13510
13511
  listenerHostLimits: () => listenerHostLimits,
13511
13512
  listenerPermissionMode: () => listenerPermissionMode,
13513
+ listenerRouteConfiguration: () => listenerRouteConfiguration,
13512
13514
  listenerStatusJson: () => listenerStatusJson,
13515
+ renderListenerStatus: () => renderListenerStatus,
13513
13516
  renderRoster: () => renderRoster,
13514
13517
  replyRefusalHint: () => replyRefusalHint,
13515
13518
  resolveDetachedClaudeExecutable: () => resolveDetachedClaudeExecutable,
@@ -13519,9 +13522,9 @@ __export(cli_exports, {
13519
13522
  module.exports = __toCommonJS(cli_exports);
13520
13523
  var import_node_crypto19 = require("node:crypto");
13521
13524
  var import_node_fs7 = require("node:fs");
13522
- var import_promises10 = require("node:fs/promises");
13523
- var import_node_path17 = require("node:path");
13524
- var import_promises11 = require("node:readline/promises");
13525
+ var import_promises11 = require("node:fs/promises");
13526
+ var import_node_path19 = require("node:path");
13527
+ var import_promises12 = require("node:readline/promises");
13525
13528
 
13526
13529
  // src/cloud/auth.ts
13527
13530
  var import_node_crypto2 = require("node:crypto");
@@ -23273,10 +23276,14 @@ function parseProfile(raw) {
23273
23276
  }
23274
23277
  return value;
23275
23278
  }
23276
- async function withFileLock(stateDirectory2, lockName, work) {
23279
+ async function withFileLock(stateDirectory2, lockName, work, options = {}) {
23277
23280
  await secureDirectory(stateDirectory2);
23278
23281
  const lockPath = (0, import_node_path.join)(stateDirectory2, `${lockName}.lock`);
23279
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
23282
+ const timeoutMs = options.timeoutMs ?? LOCK_TIMEOUT_MS;
23283
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > LOCK_TIMEOUT_MS) {
23284
+ throw new Error("credential refresh lock timeout is invalid");
23285
+ }
23286
+ const deadline = Date.now() + timeoutMs;
23280
23287
  let handle = null;
23281
23288
  while (handle === null) {
23282
23289
  try {
@@ -32310,7 +32317,8 @@ var STATES = /* @__PURE__ */ new Set([
32310
32317
  "done",
32311
32318
  "expired",
32312
32319
  "failed",
32313
- "observed"
32320
+ "observed",
32321
+ "routed_main"
32314
32322
  ]);
32315
32323
  var RELATIONS = /* @__PURE__ */ new Set(["same_owner", "cross_owner", "unknown"]);
32316
32324
  var SIGNAL_KINDS2 = /* @__PURE__ */ new Set(["ask", "note"]);
@@ -32373,7 +32381,7 @@ function parseListenerEffectRecord(raw, expectedId) {
32373
32381
  }
32374
32382
  if (row.version === 1) {
32375
32383
  rejectUnknownKeys(row, V1_EFFECT_KEYS);
32376
- if ("signalKind" in row || row.state === "observed") {
32384
+ if ("signalKind" in row || row.state === "observed" || row.state === "routed_main") {
32377
32385
  throw new Error("stored listener effect is malformed");
32378
32386
  }
32379
32387
  return upcastV1Ask(row);
@@ -32416,6 +32424,10 @@ function parseV2Record(row) {
32416
32424
  if (typeof row.commandId !== "string" || row.commandId !== "" || row.state !== "observed" || row.promptAttempts !== 0 || row.postAttempts !== 0 || row.replyBody !== null || row.replyTruncated !== false || row.replySignalId !== null || row.failureCode !== null) {
32417
32425
  throw new Error("stored listener effect is malformed");
32418
32426
  }
32427
+ } else if (row.state === "routed_main") {
32428
+ if (row.commandId !== "" || row.promptAttempts !== 0 || row.postAttempts !== 0 || row.replyBody !== null || row.replyTruncated !== false || row.replySignalId !== null || row.failureCode !== null) {
32429
+ throw new Error("stored listener effect is malformed");
32430
+ }
32419
32431
  } else {
32420
32432
  if (typeof row.commandId !== "string" || !COMMAND_ID_RE2.test(row.commandId) || row.state === "observed") {
32421
32433
  throw new Error("stored listener effect is malformed");
@@ -32478,6 +32490,14 @@ function newObservedNoteRecord(input) {
32478
32490
  updatedAt: input.updatedAt
32479
32491
  };
32480
32492
  }
32493
+ function newRoutedMainAskRecord(input) {
32494
+ const base = newObservedNoteRecord(input);
32495
+ return {
32496
+ ...base,
32497
+ signalKind: "ask",
32498
+ state: "routed_main"
32499
+ };
32500
+ }
32481
32501
  function rejectWrite() {
32482
32502
  throw new Error("listener effect write rejected");
32483
32503
  }
@@ -34207,6 +34227,186 @@ var DeliveryCommandClient = class {
34207
34227
  }
34208
34228
  };
34209
34229
 
34230
+ // src/listener/main-routing.ts
34231
+ var import_node_path14 = require("node:path");
34232
+ var UUID_RE11 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
34233
+ var MAX_QUEUE_BYTES = 1024 * 1024;
34234
+ var QUEUE_FILE = "pending-for-main.json";
34235
+ var QUEUE_LOCK = "pending-for-main";
34236
+ var LISTENER_MAIN_QUEUE_MAX = 200;
34237
+ var LISTENER_DEFER_OVER_MIN = 1;
34238
+ var LISTENER_DEFER_OVER_MAX = 1e4;
34239
+ function decideListenerRoute(route, threshold, bodyLength) {
34240
+ if (!Number.isSafeInteger(bodyLength) || bodyLength < 0) {
34241
+ throw new Error("listener route body length must be a non-negative integer");
34242
+ }
34243
+ if (route === "worker") {
34244
+ if (threshold !== null) {
34245
+ throw new Error("worker route cannot have a split threshold");
34246
+ }
34247
+ return "worker";
34248
+ }
34249
+ if (route === "main") {
34250
+ if (threshold !== null) {
34251
+ throw new Error("main route cannot have a split threshold");
34252
+ }
34253
+ return "main";
34254
+ }
34255
+ if (route !== "split") {
34256
+ throw new Error("listener route mode is invalid");
34257
+ }
34258
+ if (threshold === null || !Number.isSafeInteger(threshold) || threshold < LISTENER_DEFER_OVER_MIN || threshold > LISTENER_DEFER_OVER_MAX) {
34259
+ throw new Error("split route threshold is invalid");
34260
+ }
34261
+ return bodyLength > threshold ? "main" : "worker";
34262
+ }
34263
+ function checkedTimestamp2(value) {
34264
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
34265
+ }
34266
+ function parseEntry(value) {
34267
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
34268
+ throw new Error("stored pending-for-main entry is malformed");
34269
+ }
34270
+ const row = value;
34271
+ const allowed = /* @__PURE__ */ new Set([
34272
+ "signalId",
34273
+ "workspaceId",
34274
+ "principalId",
34275
+ "fromId",
34276
+ "fromKind",
34277
+ "senderName",
34278
+ "body",
34279
+ "createdAt",
34280
+ "queuedAt"
34281
+ ]);
34282
+ if (Object.keys(row).some((key2) => !allowed.has(key2))) {
34283
+ throw new Error("stored pending-for-main entry is malformed");
34284
+ }
34285
+ if (typeof row.signalId !== "string" || !UUID_RE11.test(row.signalId) || typeof row.workspaceId !== "string" || !UUID_RE11.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE11.test(row.principalId) || typeof row.fromId !== "string" || !UUID_RE11.test(row.fromId) || row.fromKind !== "user" && row.fromKind !== "agent" || !(row.senderName === null || typeof row.senderName === "string" && row.senderName.length <= 200) || typeof row.body !== "string" || row.body.length < 1 || row.body.length > 2e3 || !checkedTimestamp2(row.createdAt) || !checkedTimestamp2(row.queuedAt)) {
34286
+ throw new Error("stored pending-for-main entry is malformed");
34287
+ }
34288
+ return {
34289
+ signalId: row.signalId.toLowerCase(),
34290
+ workspaceId: row.workspaceId.toLowerCase(),
34291
+ principalId: row.principalId.toLowerCase(),
34292
+ fromId: row.fromId.toLowerCase(),
34293
+ fromKind: row.fromKind,
34294
+ senderName: row.senderName,
34295
+ body: row.body,
34296
+ createdAt: row.createdAt,
34297
+ queuedAt: row.queuedAt
34298
+ };
34299
+ }
34300
+ function parseFile(raw) {
34301
+ let value;
34302
+ try {
34303
+ value = JSON.parse(raw);
34304
+ } catch {
34305
+ throw new Error("stored pending-for-main queue is malformed");
34306
+ }
34307
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
34308
+ throw new Error("stored pending-for-main queue is malformed");
34309
+ }
34310
+ const row = value;
34311
+ if (Object.keys(row).some(
34312
+ (key2) => key2 !== "version" && key2 !== "entries" && key2 !== "droppedCount"
34313
+ ) || row.version !== 1 || !Array.isArray(row.entries) || row.entries.length > LISTENER_MAIN_QUEUE_MAX || !(row.droppedCount === void 0 || typeof row.droppedCount === "number" && Number.isSafeInteger(row.droppedCount) && row.droppedCount >= 0)) {
34314
+ throw new Error("stored pending-for-main queue is malformed");
34315
+ }
34316
+ const entries = row.entries.map(parseEntry);
34317
+ if (new Set(entries.map((entry) => entry.signalId)).size !== entries.length) {
34318
+ throw new Error("stored pending-for-main queue repeats a signal");
34319
+ }
34320
+ return { version: 1, entries, droppedCount: row.droppedCount ?? 0 };
34321
+ }
34322
+ var FilePendingMainQueue = class {
34323
+ path;
34324
+ directory;
34325
+ constructor(instanceDirectory) {
34326
+ if (!(0, import_node_path14.isAbsolute)(instanceDirectory)) {
34327
+ throw new Error("pending-for-main directory must be absolute");
34328
+ }
34329
+ this.directory = instanceDirectory;
34330
+ this.path = (0, import_node_path14.join)(instanceDirectory, QUEUE_FILE);
34331
+ }
34332
+ async readUnlocked() {
34333
+ const raw = await readSecureJsonFile(this.path, MAX_QUEUE_BYTES);
34334
+ return raw === null ? { version: 1, entries: [], droppedCount: 0 } : parseFile(raw);
34335
+ }
34336
+ async writeUnlocked(file) {
34337
+ const canonical = parseFile(JSON.stringify(file));
34338
+ await writeSecureJsonFile(this.path, JSON.stringify(canonical));
34339
+ }
34340
+ async read() {
34341
+ return [...(await this.readUnlocked()).entries];
34342
+ }
34343
+ async count() {
34344
+ return (await this.readUnlocked()).entries.length;
34345
+ }
34346
+ async stats() {
34347
+ const file = await this.readUnlocked();
34348
+ return { count: file.entries.length, droppedCount: file.droppedCount };
34349
+ }
34350
+ async enqueue(entry) {
34351
+ const checked = parseEntry(entry);
34352
+ return await withFileLock(this.directory, QUEUE_LOCK, async () => {
34353
+ const file = await this.readUnlocked();
34354
+ if (file.entries.some((item) => item.signalId === checked.signalId)) {
34355
+ return {
34356
+ count: file.entries.length,
34357
+ added: false,
34358
+ droppedOldest: false,
34359
+ droppedCount: file.droppedCount
34360
+ };
34361
+ }
34362
+ file.entries.push(checked);
34363
+ const droppedOldest = file.entries.length > LISTENER_MAIN_QUEUE_MAX;
34364
+ if (droppedOldest) {
34365
+ file.entries.shift();
34366
+ file.droppedCount += 1;
34367
+ }
34368
+ await this.writeUnlocked(file);
34369
+ return {
34370
+ count: file.entries.length,
34371
+ added: true,
34372
+ droppedOldest,
34373
+ droppedCount: file.droppedCount
34374
+ };
34375
+ });
34376
+ }
34377
+ async remove(signalIds, lockTimeoutMs) {
34378
+ if (signalIds.size === 0) return await this.count();
34379
+ return await withFileLock(this.directory, QUEUE_LOCK, async () => {
34380
+ const file = await this.readUnlocked();
34381
+ const entries = file.entries.filter((entry) => !signalIds.has(entry.signalId));
34382
+ if (entries.length !== file.entries.length) {
34383
+ await this.writeUnlocked({
34384
+ version: 1,
34385
+ entries,
34386
+ droppedCount: file.droppedCount
34387
+ });
34388
+ }
34389
+ return entries.length;
34390
+ }, lockTimeoutMs === void 0 ? {} : { timeoutMs: lockTimeoutMs });
34391
+ }
34392
+ };
34393
+ function pendingMainEntry(signal, principalId, provenance, now) {
34394
+ if (signal.kind !== "ask") {
34395
+ throw new Error("only directed asks can enter the pending-for-main queue");
34396
+ }
34397
+ return parseEntry({
34398
+ signalId: signal.id,
34399
+ workspaceId: signal.workspace_id,
34400
+ principalId,
34401
+ fromId: signal.from,
34402
+ fromKind: signal.from_kind,
34403
+ senderName: provenance.senderName,
34404
+ body: signal.body,
34405
+ createdAt: signal.created_at,
34406
+ queuedAt: new Date(now).toISOString()
34407
+ });
34408
+ }
34409
+
34210
34410
  // src/listener/runtime.ts
34211
34411
  var LISTENER_PAGE_LIMIT = 100;
34212
34412
  var LISTENER_IDLE_POLL_MS = 2e3;
@@ -34217,7 +34417,7 @@ var LISTENER_REPLY_ONLY_MINIMUM_MS = SIGNAL_REQUEST_TIMEOUT_MS + LISTENER_ACK_ON
34217
34417
  var LISTENER_PROMPT_START_MINIMUM_MS = SIGNAL_READ_TIMEOUT_MS + ACP_DEFAULT_REQUEST_TIMEOUT_MS + LISTENER_REPLY_ONLY_MINIMUM_MS;
34218
34418
  var LISTENER_DELIVERY_RETRY_INITIAL_MS = 500;
34219
34419
  var LISTENER_DELIVERY_RETRY_MAX_MS = 3e4;
34220
- var UUID_RE11 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
34420
+ var UUID_RE12 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
34221
34421
  var ListenerCapabilityError = class extends Error {
34222
34422
  code;
34223
34423
  constructor(code, message) {
@@ -34296,6 +34496,9 @@ function ackForTerminalEffect(record, now) {
34296
34496
  if (record.state === "observed" && record.signalKind === "note") {
34297
34497
  return { outcome: "observed", lastErrorCode: null };
34298
34498
  }
34499
+ if (record.state === "routed_main" && record.signalKind === "ask") {
34500
+ return { outcome: "observed", lastErrorCode: null };
34501
+ }
34299
34502
  if (record.state === "expired" && record.signalKind === "ask" && Date.parse(record.askUntil) <= now()) {
34300
34503
  return { outcome: "expired", lastErrorCode: null };
34301
34504
  }
@@ -34315,7 +34518,7 @@ function ackForTerminalEffect(record, now) {
34315
34518
  return { outcome: "failed_terminal", lastErrorCode: "local_effect_failed" };
34316
34519
  }
34317
34520
  function isAckableTerminalEffect(record, now) {
34318
- return record.state === "done" && record.signalKind === "ask" && !!record.replySignalId || record.state === "observed" && record.signalKind === "note" || record.state === "expired" && record.signalKind === "ask" && Date.parse(record.askUntil) <= now() || record.state === "failed" && record.signalKind === "ask";
34521
+ return record.state === "done" && record.signalKind === "ask" && !!record.replySignalId || record.state === "observed" && record.signalKind === "note" || record.state === "routed_main" && record.signalKind === "ask" || record.state === "expired" && record.signalKind === "ask" && Date.parse(record.askUntil) <= now() || record.state === "failed" && record.signalKind === "ask";
34319
34522
  }
34320
34523
  function effectPhaseBudget(record) {
34321
34524
  if (record === null || record.state === "received" || record.state === "prompting") {
@@ -34478,6 +34681,8 @@ async function runListenerRuntime(options) {
34478
34681
  const random = options.random ?? Math.random;
34479
34682
  const pageLimit = options.pageLimit ?? LISTENER_PAGE_LIMIT;
34480
34683
  const pollMs = options.pollMs ?? LISTENER_IDLE_POLL_MS;
34684
+ const routeMode = options.routeMode ?? "worker";
34685
+ const deferOverChars = options.deferOverChars ?? null;
34481
34686
  const abort = options.signal;
34482
34687
  const hasInstanceId = options.listenerInstanceId !== void 0;
34483
34688
  const hasJournal = options.deliveryJournal !== void 0;
@@ -34487,7 +34692,7 @@ async function runListenerRuntime(options) {
34487
34692
  new Error("listener instance id and delivery journal must be configured together")
34488
34693
  );
34489
34694
  }
34490
- if (hasInstanceId && !UUID_RE11.test(options.listenerInstanceId)) {
34695
+ if (hasInstanceId && !UUID_RE12.test(options.listenerInstanceId)) {
34491
34696
  return await closeBeforeStart(
34492
34697
  options.model,
34493
34698
  new Error("listener instance id must be a UUID")
@@ -34499,6 +34704,14 @@ async function runListenerRuntime(options) {
34499
34704
  new Error("an injected delivery client requires durable delivery configuration")
34500
34705
  );
34501
34706
  }
34707
+ try {
34708
+ decideListenerRoute(routeMode, deferOverChars, 0);
34709
+ if (routeMode !== "worker" && options.pendingMainQueue === void 0) {
34710
+ throw new Error("main listener routing requires a pending queue");
34711
+ }
34712
+ } catch (error) {
34713
+ return await closeBeforeStart(options.model, asError2(error));
34714
+ }
34502
34715
  let initialJournal = null;
34503
34716
  if (hasJournal) {
34504
34717
  try {
@@ -34551,6 +34764,52 @@ async function runListenerRuntime(options) {
34551
34764
  ...options.resolveSenderProvenance === void 0 ? {} : { resolveSenderProvenance: options.resolveSenderProvenance },
34552
34765
  isCredentialFailure: isCredentialLoss
34553
34766
  });
34767
+ const routeAskToMain = async (signal) => {
34768
+ let provenance = {
34769
+ senderName: null,
34770
+ operatorId: null,
34771
+ operatorName: null
34772
+ };
34773
+ if (options.resolveSenderProvenance) {
34774
+ try {
34775
+ provenance = await options.resolveSenderProvenance(signal, {
34776
+ ...abort ? { signal: abort } : {},
34777
+ deadlineMs: now() + SIGNAL_READ_TIMEOUT_MS
34778
+ });
34779
+ } catch {
34780
+ }
34781
+ }
34782
+ const queued = await options.pendingMainQueue.enqueue(
34783
+ pendingMainEntry(signal, options.principalId, provenance, now())
34784
+ );
34785
+ options.onEvent?.({
34786
+ type: "main_queue",
34787
+ signalId: signal.id,
34788
+ pendingCount: queued.count,
34789
+ droppedOldest: queued.droppedOldest,
34790
+ droppedCount: queued.droppedCount,
34791
+ ts: eventTime(now)
34792
+ });
34793
+ const existing = await options.store.read(signal.id);
34794
+ if (existing !== null) {
34795
+ if (!sameEffectSignal(existing, signal) || existing.state !== "routed_main") {
34796
+ throw new Error("stored listener effect does not match the main-routed ask");
34797
+ }
34798
+ return existing;
34799
+ }
34800
+ await options.store.write(newRoutedMainAskRecord({
34801
+ signalId: signal.id,
34802
+ body: signal.body,
34803
+ until: signal.until,
34804
+ senderOwnerRelation: signal.sender_owner_relation ?? "unknown",
34805
+ updatedAt: eventTime(now)
34806
+ }));
34807
+ const persisted = await options.store.read(signal.id);
34808
+ if (persisted === null || !sameEffectSignal(persisted, signal) || persisted.state !== "routed_main") {
34809
+ throw new Error("main-routed listener effect could not be verified");
34810
+ }
34811
+ return persisted;
34812
+ };
34554
34813
  let malformedWarnings = 0;
34555
34814
  const readPage = options.readPage ?? (async (input) => await readAgentSignalPage(
34556
34815
  options.target,
@@ -34716,11 +34975,13 @@ async function runListenerRuntime(options) {
34716
34975
  break;
34717
34976
  }
34718
34977
  if (!ready) {
34719
- try {
34720
- await options.model.start();
34721
- } catch (error) {
34722
- stop = { reason: "fatal", error: asError2(error) };
34723
- break;
34978
+ if (routeMode !== "main") {
34979
+ try {
34980
+ await options.model.start();
34981
+ } catch (error) {
34982
+ stop = { reason: "fatal", error: asError2(error) };
34983
+ break;
34984
+ }
34724
34985
  }
34725
34986
  ready = true;
34726
34987
  options.onEvent?.({
@@ -35022,56 +35283,81 @@ async function runListenerRuntime(options) {
35022
35283
  ts: eventTime(now)
35023
35284
  });
35024
35285
  } else if (signal.kind === "ask") {
35025
- let processAttempt = 0;
35026
- while (terminal === null) {
35027
- const before = await options.store.read(signal.id);
35028
- if (before !== null && !sameEffectSignal(before, signal)) {
35029
- throw new Error("stored listener effect does not match the authoritative delivery");
35030
- }
35031
- const requiredBudget = effectPhaseBudget(before);
35032
- if (leasedUntilMs <= now() + requiredBudget) {
35033
- await sleep2(
35034
- Math.max(
35035
- 0,
35036
- leasedUntilMs + LISTENER_DELIVERY_SAFETY_MARGIN_MS - now()
35037
- ),
35038
- abort
35039
- );
35040
- if (abort?.aborted) {
35041
- stop = { reason: "cancelled" };
35042
- break;
35043
- }
35044
- if (now() >= leasedUntilMs + LISTENER_DELIVERY_SAFETY_MARGIN_MS) {
35045
- await journal.clearActive(eventTime(now));
35046
- after = null;
35047
- }
35048
- break;
35049
- }
35050
- const processed = await engine.process(signal);
35051
- const effect = "record" in processed ? processed.record : null;
35286
+ const decision = decideListenerRoute(
35287
+ routeMode,
35288
+ deferOverChars,
35289
+ signal.body.length
35290
+ );
35291
+ options.onEvent?.({
35292
+ type: "routing_decision",
35293
+ signalId: signal.id,
35294
+ routeMode,
35295
+ decision,
35296
+ threshold: deferOverChars,
35297
+ bodyLength: signal.body.length,
35298
+ ts: eventTime(now)
35299
+ });
35300
+ if (decision === "main") {
35301
+ terminal = await routeAskToMain(signal);
35052
35302
  options.onEvent?.({
35053
35303
  type: "effect",
35054
35304
  signalId: signal.id,
35055
- status: processed.status,
35056
- failureCode: effect?.failureCode ?? null,
35305
+ status: "routed_main",
35306
+ failureCode: null,
35057
35307
  ts: eventTime(now)
35058
35308
  });
35059
- if (processed.status === "ignored") {
35060
- throw new Error("claimed delivery was ignored by the listener engine");
35061
- }
35062
- if (processed.status === "retry_pending") {
35063
- processAttempt += 1;
35064
- await sleep2(
35065
- deliveryRetryDelay(processAttempt, null, random),
35066
- abort
35067
- );
35068
- if (abort?.aborted) {
35069
- stop = { reason: "cancelled" };
35309
+ } else {
35310
+ let processAttempt = 0;
35311
+ while (terminal === null) {
35312
+ const before = await options.store.read(signal.id);
35313
+ if (before !== null && !sameEffectSignal(before, signal)) {
35314
+ throw new Error("stored listener effect does not match the authoritative delivery");
35315
+ }
35316
+ const requiredBudget = effectPhaseBudget(before);
35317
+ if (leasedUntilMs <= now() + requiredBudget) {
35318
+ await sleep2(
35319
+ Math.max(
35320
+ 0,
35321
+ leasedUntilMs + LISTENER_DELIVERY_SAFETY_MARGIN_MS - now()
35322
+ ),
35323
+ abort
35324
+ );
35325
+ if (abort?.aborted) {
35326
+ stop = { reason: "cancelled" };
35327
+ break;
35328
+ }
35329
+ if (now() >= leasedUntilMs + LISTENER_DELIVERY_SAFETY_MARGIN_MS) {
35330
+ await journal.clearActive(eventTime(now));
35331
+ after = null;
35332
+ }
35070
35333
  break;
35071
35334
  }
35072
- continue;
35335
+ const processed = await engine.process(signal);
35336
+ const effect = "record" in processed ? processed.record : null;
35337
+ options.onEvent?.({
35338
+ type: "effect",
35339
+ signalId: signal.id,
35340
+ status: processed.status,
35341
+ failureCode: effect?.failureCode ?? null,
35342
+ ts: eventTime(now)
35343
+ });
35344
+ if (processed.status === "ignored") {
35345
+ throw new Error("claimed delivery was ignored by the listener engine");
35346
+ }
35347
+ if (processed.status === "retry_pending") {
35348
+ processAttempt += 1;
35349
+ await sleep2(
35350
+ deliveryRetryDelay(processAttempt, null, random),
35351
+ abort
35352
+ );
35353
+ if (abort?.aborted) {
35354
+ stop = { reason: "cancelled" };
35355
+ break;
35356
+ }
35357
+ continue;
35358
+ }
35359
+ terminal = processed.record;
35073
35360
  }
35074
- terminal = processed.record;
35075
35361
  }
35076
35362
  } else {
35077
35363
  throw new Error("claimed delivery has an unsupported signal kind");
@@ -35144,6 +35430,31 @@ async function runListenerRuntime(options) {
35144
35430
  let result;
35145
35431
  try {
35146
35432
  await readOrReplaceUnreadableEffect(options.store, signal, now);
35433
+ const decision = decideListenerRoute(
35434
+ routeMode,
35435
+ deferOverChars,
35436
+ signal.body.length
35437
+ );
35438
+ options.onEvent?.({
35439
+ type: "routing_decision",
35440
+ signalId: signal.id,
35441
+ routeMode,
35442
+ decision,
35443
+ threshold: deferOverChars,
35444
+ bodyLength: signal.body.length,
35445
+ ts: eventTime(now)
35446
+ });
35447
+ if (decision === "main") {
35448
+ const record2 = await routeAskToMain(signal);
35449
+ options.onEvent?.({
35450
+ type: "effect",
35451
+ signalId: signal.id,
35452
+ status: "routed_main",
35453
+ failureCode: null,
35454
+ ts: eventTime(now)
35455
+ });
35456
+ continue;
35457
+ }
35147
35458
  result = await engine.process(signal);
35148
35459
  } catch (error) {
35149
35460
  if (abort?.aborted) {
@@ -35203,8 +35514,8 @@ async function runListenerRuntime(options) {
35203
35514
  // src/listener/control.ts
35204
35515
  var import_node_net = require("node:net");
35205
35516
  var import_promises9 = require("node:fs/promises");
35206
- var import_node_path14 = require("node:path");
35207
- var UUID_RE12 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35517
+ var import_node_path15 = require("node:path");
35518
+ var UUID_RE13 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35208
35519
  var MAX_STATUS_BYTES = 16 * 1024;
35209
35520
  var MAX_CONTROL_BYTES = 8 * 1024;
35210
35521
  var CONTROL_TIMEOUT_MS = 2e3;
@@ -35218,19 +35529,19 @@ var ListenerAlreadyRunningError = class extends Error {
35218
35529
  };
35219
35530
  function listenerPaths(options) {
35220
35531
  const root = options.stateDirectory ?? defaultListenerStateDirectory();
35221
- if (!(0, import_node_path14.isAbsolute)(root)) {
35532
+ if (!(0, import_node_path15.isAbsolute)(root)) {
35222
35533
  throw new Error("listener state directory must be absolute");
35223
35534
  }
35224
35535
  const key2 = listenerInstanceKey(options);
35225
- const instanceDirectory = (0, import_node_path14.join)(root, key2);
35536
+ const instanceDirectory = (0, import_node_path15.join)(root, key2);
35226
35537
  const uid2 = typeof process.getuid === "function" ? process.getuid() : process.pid;
35227
- const controlDirectory = process.platform === "win32" ? "" : (0, import_node_path14.join)("/tmp", `cswarm-control-${uid2}`);
35228
- const socketPath = process.platform === "win32" ? `\\\\.\\pipe\\cswarm-${key2}` : (0, import_node_path14.join)(controlDirectory, `${key2.slice(0, 32)}.sock`);
35538
+ const controlDirectory = process.platform === "win32" ? "" : (0, import_node_path15.join)("/tmp", `cswarm-control-${uid2}`);
35539
+ const socketPath = process.platform === "win32" ? `\\\\.\\pipe\\cswarm-${key2}` : (0, import_node_path15.join)(controlDirectory, `${key2.slice(0, 32)}.sock`);
35229
35540
  return {
35230
35541
  key: key2,
35231
35542
  instanceDirectory,
35232
- statusPath: (0, import_node_path14.join)(instanceDirectory, "status.json"),
35233
- logPath: (0, import_node_path14.join)(instanceDirectory, "events.ndjson"),
35543
+ statusPath: (0, import_node_path15.join)(instanceDirectory, "status.json"),
35544
+ logPath: (0, import_node_path15.join)(instanceDirectory, "events.ndjson"),
35234
35545
  socketPath
35235
35546
  };
35236
35547
  }
@@ -35258,7 +35569,11 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
35258
35569
  "lastTerminalDeliveryFailureCount",
35259
35570
  "lastTerminalDeliveryFailureAt",
35260
35571
  "lastClaimAt",
35261
- "lastAckAt"
35572
+ "lastAckAt",
35573
+ "routeMode",
35574
+ "deferOverChars",
35575
+ "pendingForMainCount",
35576
+ "droppedForMainCount"
35262
35577
  ]);
35263
35578
  var STATUS_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
35264
35579
  "leaseId",
@@ -35303,12 +35618,17 @@ function parseStatus(raw) {
35303
35618
  throw new Error("stored listener status is malformed");
35304
35619
  }
35305
35620
  }
35306
- const nullableUuid2 = (candidate) => candidate === null || typeof candidate === "string" && UUID_RE12.test(candidate);
35621
+ const nullableUuid2 = (candidate) => candidate === null || typeof candidate === "string" && UUID_RE13.test(candidate);
35307
35622
  const nullableCount = (candidate) => candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
35308
35623
  const nullableTimestamp = (candidate) => candidate === null || typeof candidate === "string" && Number.isFinite(Date.parse(candidate));
35309
- if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE12.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE12.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE12.test(row.principalId) || !Number.isSafeInteger(row.pid) || row.pid < 1 || typeof row.state !== "string" || !["starting", "ready", "stopping", "stopped", "failed"].includes(row.state) || typeof row.startedAt !== "string" || !Number.isFinite(Date.parse(row.startedAt)) || !(row.readyAt === null || typeof row.readyAt === "string" && Number.isFinite(Date.parse(row.readyAt))) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt)) || !(row.stoppedAt === null || typeof row.stoppedAt === "string" && Number.isFinite(Date.parse(row.stoppedAt))) || !nullableUuid2(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || !(row.lastWorkerStderrTail === void 0 || row.lastWorkerStderrTail === null || typeof row.lastWorkerStderrTail === "string" && row.lastWorkerStderrTail.length > 0 && row.lastWorkerStderrTail.length <= 2048 && !/swm_(?:agt|inv|cap)_/i.test(row.lastWorkerStderrTail)) || typeof row.logPath !== "string" || !(0, import_node_path14.isAbsolute)(row.logPath) || !(row.deliveryMode === void 0 || row.deliveryMode === null || typeof row.deliveryMode === "string" && STATUS_DELIVERY_MODES.has(row.deliveryMode)) || !(row.pendingDeliveryCount === void 0 || nullableCount(row.pendingDeliveryCount)) || !(row.lastTerminalDeliveryFailureCount === void 0 || nullableCount(row.lastTerminalDeliveryFailureCount)) || !(row.lastTerminalDeliveryFailureAt === void 0 || nullableTimestamp(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp(row.lastAckAt))) {
35624
+ if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE13.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE13.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE13.test(row.principalId) || !Number.isSafeInteger(row.pid) || row.pid < 1 || typeof row.state !== "string" || !["starting", "ready", "stopping", "stopped", "failed"].includes(row.state) || typeof row.startedAt !== "string" || !Number.isFinite(Date.parse(row.startedAt)) || !(row.readyAt === null || typeof row.readyAt === "string" && Number.isFinite(Date.parse(row.readyAt))) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt)) || !(row.stoppedAt === null || typeof row.stoppedAt === "string" && Number.isFinite(Date.parse(row.stoppedAt))) || !nullableUuid2(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || !(row.lastWorkerStderrTail === void 0 || row.lastWorkerStderrTail === null || typeof row.lastWorkerStderrTail === "string" && row.lastWorkerStderrTail.length > 0 && row.lastWorkerStderrTail.length <= 2048 && !/swm_(?:agt|inv|cap)_/i.test(row.lastWorkerStderrTail)) || typeof row.logPath !== "string" || !(0, import_node_path15.isAbsolute)(row.logPath) || !(row.deliveryMode === void 0 || row.deliveryMode === null || typeof row.deliveryMode === "string" && STATUS_DELIVERY_MODES.has(row.deliveryMode)) || !(row.pendingDeliveryCount === void 0 || nullableCount(row.pendingDeliveryCount)) || !(row.lastTerminalDeliveryFailureCount === void 0 || nullableCount(row.lastTerminalDeliveryFailureCount)) || !(row.lastTerminalDeliveryFailureAt === void 0 || nullableTimestamp(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp(row.lastAckAt)) || !(row.routeMode === void 0 || row.routeMode === "worker" || row.routeMode === "main" || row.routeMode === "split") || !(row.deferOverChars === void 0 || row.deferOverChars === null || typeof row.deferOverChars === "number" && Number.isSafeInteger(row.deferOverChars) && row.deferOverChars >= 1 && row.deferOverChars <= 1e4) || !(row.pendingForMainCount === void 0 || typeof row.pendingForMainCount === "number" && Number.isSafeInteger(row.pendingForMainCount) && row.pendingForMainCount >= 0) || !(row.droppedForMainCount === void 0 || typeof row.droppedForMainCount === "number" && Number.isSafeInteger(row.droppedForMainCount) && row.droppedForMainCount >= 0)) {
35310
35625
  throw new Error("stored listener status is malformed");
35311
35626
  }
35627
+ const routeMode = row.routeMode ?? "worker";
35628
+ const deferOverChars = row.deferOverChars ?? null;
35629
+ if (routeMode === "split" && deferOverChars === null || routeMode !== "split" && deferOverChars !== null) {
35630
+ throw new Error("stored listener status routing fields are malformed");
35631
+ }
35312
35632
  return {
35313
35633
  ...row,
35314
35634
  deliveryMode: row.deliveryMode ?? null,
@@ -35317,7 +35637,11 @@ function parseStatus(raw) {
35317
35637
  lastTerminalDeliveryFailureAt: row.lastTerminalDeliveryFailureAt ?? null,
35318
35638
  lastClaimAt: row.lastClaimAt ?? null,
35319
35639
  lastAckAt: row.lastAckAt ?? null,
35320
- lastWorkerStderrTail: row.lastWorkerStderrTail ?? null
35640
+ lastWorkerStderrTail: row.lastWorkerStderrTail ?? null,
35641
+ routeMode,
35642
+ deferOverChars,
35643
+ pendingForMainCount: row.pendingForMainCount ?? 0,
35644
+ droppedForMainCount: row.droppedForMainCount ?? 0
35321
35645
  };
35322
35646
  }
35323
35647
  async function writeListenerStatus(paths, status) {
@@ -35359,7 +35683,13 @@ async function appendListenerEvent(paths, event) {
35359
35683
  // bounded by the supervisor, and the prompt-turn budget behind a timeout.
35360
35684
  // Local log only — this file never feeds a server payload.
35361
35685
  "worker_stderr_tail",
35362
- "turn_budget_ms"
35686
+ "turn_budget_ms",
35687
+ "route_mode",
35688
+ "route_decision",
35689
+ "defer_over_chars",
35690
+ "body_length",
35691
+ "pending_main_count",
35692
+ "dropped_count"
35363
35693
  ]);
35364
35694
  const deliveryModes = /* @__PURE__ */ new Set(["durable_claim", "cursor_fallback"]);
35365
35695
  const deliveryOutcomes = /* @__PURE__ */ new Set([
@@ -35368,6 +35698,8 @@ async function appendListenerEvent(paths, event) {
35368
35698
  "expired",
35369
35699
  "failed_terminal"
35370
35700
  ]);
35701
+ const routeModes = /* @__PURE__ */ new Set(["worker", "main", "split"]);
35702
+ const routeDecisions = /* @__PURE__ */ new Set(["worker", "main"]);
35371
35703
  for (const [key2, value] of Object.entries(event)) {
35372
35704
  if (!allowed.has(key2)) {
35373
35705
  throw new Error(`listener event field is not allowed: ${key2}`);
@@ -35381,6 +35713,18 @@ async function appendListenerEvent(paths, event) {
35381
35713
  if (key2 === "outcome" && !(value === null || typeof value === "string" && deliveryOutcomes.has(value))) {
35382
35714
  throw new Error("listener event outcome is not allowed");
35383
35715
  }
35716
+ if (key2 === "route_mode" && !(typeof value === "string" && routeModes.has(value))) {
35717
+ throw new Error("listener event route mode is not allowed");
35718
+ }
35719
+ if (key2 === "route_decision" && !(typeof value === "string" && routeDecisions.has(value))) {
35720
+ throw new Error("listener event route decision is not allowed");
35721
+ }
35722
+ if (key2 === "defer_over_chars" && !(value === null || typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 1e4)) {
35723
+ throw new Error("listener event split threshold is not allowed");
35724
+ }
35725
+ if ((key2 === "body_length" || key2 === "pending_main_count" || key2 === "dropped_count") && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) {
35726
+ throw new Error("listener event main-route count is not allowed");
35727
+ }
35384
35728
  if (key2 === "worker_stderr_tail" && !(typeof value === "string" && value.length > 0 && value.length <= 2048)) {
35385
35729
  throw new Error("listener event stderr tail is not allowed");
35386
35730
  }
@@ -35448,7 +35792,7 @@ function writeResponse(socket, response) {
35448
35792
  }
35449
35793
  async function startupLock(paths) {
35450
35794
  await ensureSecureStateDirectory(paths.instanceDirectory);
35451
- const lockPath = (0, import_node_path14.join)(paths.instanceDirectory, "starting.lock");
35795
+ const lockPath = (0, import_node_path15.join)(paths.instanceDirectory, "starting.lock");
35452
35796
  const deadline = Date.now() + START_LOCK_WAIT_MS;
35453
35797
  while (Date.now() < deadline) {
35454
35798
  let handle;
@@ -35489,7 +35833,7 @@ async function startupLock(paths) {
35489
35833
  async function prepareSocket(paths) {
35490
35834
  if (process.platform !== "win32") {
35491
35835
  const uid2 = typeof process.getuid === "function" ? process.getuid() : process.pid;
35492
- const directory = (0, import_node_path14.join)("/tmp", `cswarm-control-${uid2}`);
35836
+ const directory = (0, import_node_path15.join)("/tmp", `cswarm-control-${uid2}`);
35493
35837
  await ensureSecureStateDirectory(directory);
35494
35838
  }
35495
35839
  try {
@@ -35624,7 +35968,7 @@ async function queryListenerControl(paths, command2, timeoutMs = CONTROL_TIMEOUT
35624
35968
 
35625
35969
  // src/listener/supervisor.ts
35626
35970
  var import_node_crypto18 = require("node:crypto");
35627
- var UUID_RE13 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35971
+ var UUID_RE14 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35628
35972
  var LISTENER_RESTART_MAX_ATTEMPTS = 5;
35629
35973
  var LISTENER_RESTART_INITIAL_MS = 1e3;
35630
35974
  var LISTENER_RESTART_MAX_MS = 6e4;
@@ -35712,6 +36056,10 @@ async function runListenerSupervisor(options) {
35712
36056
  lastTerminalDeliveryFailureAt: null,
35713
36057
  lastClaimAt: null,
35714
36058
  lastAckAt: null,
36059
+ routeMode: options.routeMode ?? "worker",
36060
+ deferOverChars: options.deferOverChars ?? null,
36061
+ pendingForMainCount: 0,
36062
+ droppedForMainCount: 0,
35715
36063
  logPath: options.paths.logPath
35716
36064
  };
35717
36065
  let writes = Promise.resolve();
@@ -35747,7 +36095,7 @@ async function runListenerSupervisor(options) {
35747
36095
  // before the socket can answer, before any status/event persistence.
35748
36096
  initialize: prepare ? async () => {
35749
36097
  const selected = await prepare(proposedInstanceId);
35750
- if (!selected || typeof selected !== "object" || typeof selected.instanceId !== "string" || !UUID_RE13.test(selected.instanceId)) {
36098
+ if (!selected || typeof selected !== "object" || typeof selected.instanceId !== "string" || !UUID_RE14.test(selected.instanceId)) {
35751
36099
  throw new Error("listener prepare returned an invalid instance id");
35752
36100
  }
35753
36101
  status = { ...status, instanceId: selected.instanceId };
@@ -35897,6 +36245,36 @@ async function runListenerSupervisor(options) {
35897
36245
  });
35898
36246
  return;
35899
36247
  }
36248
+ if (event.type === "routing_decision") {
36249
+ log({
36250
+ ts: event.ts,
36251
+ event: "listener_routing_decision",
36252
+ signal_id: event.signalId,
36253
+ route_mode: event.routeMode,
36254
+ route_decision: event.decision,
36255
+ defer_over_chars: event.threshold,
36256
+ body_length: event.bodyLength
36257
+ });
36258
+ return;
36259
+ }
36260
+ if (event.type === "main_queue") {
36261
+ status = {
36262
+ ...status,
36263
+ pendingForMainCount: event.pendingCount,
36264
+ droppedForMainCount: event.droppedCount,
36265
+ lastSignalId: event.signalId,
36266
+ updatedAt: event.ts
36267
+ };
36268
+ persist();
36269
+ log({
36270
+ ts: event.ts,
36271
+ event: event.droppedOldest ? "listener_main_queue_oldest_dropped" : "listener_main_queue",
36272
+ signal_id: event.signalId,
36273
+ pending_main_count: event.pendingCount,
36274
+ dropped_count: event.droppedOldest ? 1 : 0
36275
+ });
36276
+ return;
36277
+ }
35900
36278
  const unknown = event;
35901
36279
  log({
35902
36280
  ts: typeof unknown.ts === "string" && Number.isFinite(Date.parse(unknown.ts)) ? unknown.ts : iso2(now),
@@ -36068,9 +36446,9 @@ async function waitForListenerReady(paths, options = {}) {
36068
36446
  }
36069
36447
 
36070
36448
  // src/listener/delivery-journal.ts
36071
- var import_node_path15 = require("node:path");
36449
+ var import_node_path16 = require("node:path");
36072
36450
  var import_node_util2 = require("node:util");
36073
- var UUID_RE14 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
36451
+ var UUID_RE15 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
36074
36452
  var COMMAND_ID_RE3 = /^[A-Za-z0-9_-]{8,72}$/;
36075
36453
  var SIGNAL_FINGERPRINT_RE = /^[0-9a-f]{64}$/;
36076
36454
  var MAX_JOURNAL_BYTES = 8192;
@@ -36164,7 +36542,7 @@ var ALLOWED_ERROR_CODES = /* @__PURE__ */ new Set([
36164
36542
  "credential_unavailable"
36165
36543
  ]);
36166
36544
  function claimCommandId(listenerInstanceId, claimOrdinal) {
36167
- if (!UUID_RE14.test(listenerInstanceId)) {
36545
+ if (!UUID_RE15.test(listenerInstanceId)) {
36168
36546
  throw new Error("stored delivery journal is malformed");
36169
36547
  }
36170
36548
  if (!Number.isSafeInteger(claimOrdinal) || claimOrdinal < 0) {
@@ -36179,7 +36557,7 @@ function claimCommandId(listenerInstanceId, claimOrdinal) {
36179
36557
  return id;
36180
36558
  }
36181
36559
  function ackCommandId(leaseId) {
36182
- if (!UUID_RE14.test(leaseId)) {
36560
+ if (!UUID_RE15.test(leaseId)) {
36183
36561
  throw new Error("stored delivery journal is malformed");
36184
36562
  }
36185
36563
  const cleanLease = leaseId.toLowerCase().replace(/-/g, "");
@@ -36262,19 +36640,19 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
36262
36640
  if (row.version !== 1) {
36263
36641
  throw new Error("stored delivery journal is malformed");
36264
36642
  }
36265
- if (typeof row.workspaceId !== "string" || !UUID_RE14.test(row.workspaceId) || row.workspaceId !== row.workspaceId.toLowerCase()) {
36643
+ if (typeof row.workspaceId !== "string" || !UUID_RE15.test(row.workspaceId) || row.workspaceId !== row.workspaceId.toLowerCase()) {
36266
36644
  throw new Error("stored delivery journal is malformed");
36267
36645
  }
36268
36646
  if (expectedWorkspaceId && row.workspaceId !== expectedWorkspaceId.toLowerCase()) {
36269
36647
  throw new Error("stored delivery journal is malformed");
36270
36648
  }
36271
- if (typeof row.principalId !== "string" || !UUID_RE14.test(row.principalId) || row.principalId !== row.principalId.toLowerCase()) {
36649
+ if (typeof row.principalId !== "string" || !UUID_RE15.test(row.principalId) || row.principalId !== row.principalId.toLowerCase()) {
36272
36650
  throw new Error("stored delivery journal is malformed");
36273
36651
  }
36274
36652
  if (expectedPrincipalId && row.principalId !== expectedPrincipalId.toLowerCase()) {
36275
36653
  throw new Error("stored delivery journal is malformed");
36276
36654
  }
36277
- if (typeof row.listenerInstanceId !== "string" || !UUID_RE14.test(row.listenerInstanceId) || row.listenerInstanceId !== row.listenerInstanceId.toLowerCase()) {
36655
+ if (typeof row.listenerInstanceId !== "string" || !UUID_RE15.test(row.listenerInstanceId) || row.listenerInstanceId !== row.listenerInstanceId.toLowerCase()) {
36278
36656
  throw new Error("stored delivery journal is malformed");
36279
36657
  }
36280
36658
  if (!Number.isSafeInteger(row.nextClaimOrdinal) || row.nextClaimOrdinal < 0) {
@@ -36338,10 +36716,10 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
36338
36716
  if (active.claimLastAttemptAt === null) {
36339
36717
  throw new Error("stored delivery journal is malformed");
36340
36718
  }
36341
- if (typeof active.signalId !== "string" || !UUID_RE14.test(active.signalId) || active.signalId !== active.signalId.toLowerCase()) {
36719
+ if (typeof active.signalId !== "string" || !UUID_RE15.test(active.signalId) || active.signalId !== active.signalId.toLowerCase()) {
36342
36720
  throw new Error("stored delivery journal is malformed");
36343
36721
  }
36344
- if (typeof active.leaseId !== "string" || !UUID_RE14.test(active.leaseId) || active.leaseId !== active.leaseId.toLowerCase()) {
36722
+ if (typeof active.leaseId !== "string" || !UUID_RE15.test(active.leaseId) || active.leaseId !== active.leaseId.toLowerCase()) {
36345
36723
  throw new Error("stored delivery journal is malformed");
36346
36724
  }
36347
36725
  if (!isValidIsoTimestamp(active.leasedUntil) || Date.parse(active.leasedUntil) <= Date.parse(active.claimCreatedAt)) {
@@ -36413,7 +36791,7 @@ var FileListenerDeliveryJournal = class {
36413
36791
  ["profileId", "workspaceId", "principalId"],
36414
36792
  "delivery journal configuration rejected"
36415
36793
  );
36416
- if (typeof options.profileId !== "string" || !options.profileId || options.profileId.includes("\0") || typeof options.workspaceId !== "string" || !UUID_RE14.test(options.workspaceId) || typeof options.principalId !== "string" || !UUID_RE14.test(options.principalId)) {
36794
+ if (typeof options.profileId !== "string" || !options.profileId || options.profileId.includes("\0") || typeof options.workspaceId !== "string" || !UUID_RE15.test(options.workspaceId) || typeof options.principalId !== "string" || !UUID_RE15.test(options.principalId)) {
36417
36795
  throw new Error("delivery journal configuration rejected");
36418
36796
  }
36419
36797
  if (options.stateDirectory !== void 0) {
@@ -36428,11 +36806,11 @@ var FileListenerDeliveryJournal = class {
36428
36806
  stateDirectory: options.stateDirectory
36429
36807
  });
36430
36808
  const root = this.options.stateDirectory ?? defaultListenerStateDirectory();
36431
- if (!(0, import_node_path15.isAbsolute)(root)) {
36809
+ if (!(0, import_node_path16.isAbsolute)(root)) {
36432
36810
  throw new Error("delivery journal configuration rejected");
36433
36811
  }
36434
- this.instanceDirectory = (0, import_node_path15.join)(root, listenerInstanceKey(this.options));
36435
- this.journalPath = (0, import_node_path15.join)(this.instanceDirectory, "delivery-journal.json");
36812
+ this.instanceDirectory = (0, import_node_path16.join)(root, listenerInstanceKey(this.options));
36813
+ this.journalPath = (0, import_node_path16.join)(this.instanceDirectory, "delivery-journal.json");
36436
36814
  }
36437
36815
  async readRecordUnlocked() {
36438
36816
  let raw;
@@ -36534,7 +36912,7 @@ var FileListenerDeliveryJournal = class {
36534
36912
  ["signalId", "leaseId", "leasedUntil"],
36535
36913
  "delivery journal mutation rejected"
36536
36914
  );
36537
- if (typeof input.signalId !== "string" || !UUID_RE14.test(input.signalId) || typeof input.leaseId !== "string" || !UUID_RE14.test(input.leaseId) || !isValidIsoTimestamp(input.leasedUntil) || input.signalFingerprint !== void 0 && (typeof input.signalFingerprint !== "string" || !SIGNAL_FINGERPRINT_RE.test(input.signalFingerprint))) {
36915
+ if (typeof input.signalId !== "string" || !UUID_RE15.test(input.signalId) || typeof input.leaseId !== "string" || !UUID_RE15.test(input.leaseId) || !isValidIsoTimestamp(input.leasedUntil) || input.signalFingerprint !== void 0 && (typeof input.signalFingerprint !== "string" || !SIGNAL_FINGERPRINT_RE.test(input.signalFingerprint))) {
36538
36916
  throw new Error("delivery journal mutation rejected");
36539
36917
  }
36540
36918
  const canonicalSignalId = input.signalId.toLowerCase();
@@ -36666,7 +37044,7 @@ async function openListenerDeliveryJournal(options) {
36666
37044
  ["profileId", "workspaceId", "principalId", "proposedListenerInstanceId"],
36667
37045
  "delivery journal configuration rejected"
36668
37046
  );
36669
- if (typeof options.profileId !== "string" || !options.profileId || options.profileId.includes("\0") || typeof options.workspaceId !== "string" || !UUID_RE14.test(options.workspaceId) || typeof options.principalId !== "string" || !UUID_RE14.test(options.principalId) || typeof options.proposedListenerInstanceId !== "string" || !UUID_RE14.test(options.proposedListenerInstanceId)) {
37047
+ if (typeof options.profileId !== "string" || !options.profileId || options.profileId.includes("\0") || typeof options.workspaceId !== "string" || !UUID_RE15.test(options.workspaceId) || typeof options.principalId !== "string" || !UUID_RE15.test(options.principalId) || typeof options.proposedListenerInstanceId !== "string" || !UUID_RE15.test(options.proposedListenerInstanceId)) {
36670
37048
  throw new Error("delivery journal configuration rejected");
36671
37049
  }
36672
37050
  if (options.stateDirectory !== void 0) {
@@ -36761,9 +37139,9 @@ async function openListenerDeliveryJournal(options) {
36761
37139
 
36762
37140
  // src/listener/detach.ts
36763
37141
  var import_node_child_process7 = require("node:child_process");
36764
- var import_node_path16 = require("node:path");
37142
+ var import_node_path17 = require("node:path");
36765
37143
  function isNativeAbsolutePath(value, platform = process.platform) {
36766
- return platform === "win32" ? import_node_path16.win32.isAbsolute(value) : import_node_path16.posix.isAbsolute(value);
37144
+ return platform === "win32" ? import_node_path17.win32.isAbsolute(value) : import_node_path17.posix.isAbsolute(value);
36767
37145
  }
36768
37146
  function listenerNodeExecArgv(values2) {
36769
37147
  const safe = [];
@@ -36843,7 +37221,9 @@ function buildListenerChildArgs(spec) {
36843
37221
  ...provider === "codex" && codexExe ? ["--codex-executable", codexExe] : [],
36844
37222
  ...spec.model ? ["--model", spec.model] : [],
36845
37223
  ...provider === "grok" && spec.effort ? ["--effort", spec.effort] : [],
36846
- ...spec.turnBudget ? ["--turn-budget", spec.turnBudget] : []
37224
+ ...spec.turnBudget ? ["--turn-budget", spec.turnBudget] : [],
37225
+ ...spec.route && spec.route !== "worker" ? ["--route", spec.route] : [],
37226
+ ...spec.deferOver !== void 0 ? ["--defer-over", String(spec.deferOver)] : []
36847
37227
  ];
36848
37228
  }
36849
37229
  async function spawnDetachedListener(options) {
@@ -36876,6 +37256,503 @@ async function spawnDetachedListener(options) {
36876
37256
  return child;
36877
37257
  }
36878
37258
 
37259
+ // src/listener/hook.ts
37260
+ var import_promises10 = require("node:fs/promises");
37261
+ var import_node_path18 = require("node:path");
37262
+ var UUID_RE16 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
37263
+ var TOKEN_RE = /^swm_agt_[A-Za-z0-9_-]{43}$/;
37264
+ var INSTANCE_KEY_RE = /^[0-9a-f]{64}$/;
37265
+ var MAX_HOOK_CREDENTIAL_BYTES = 8 * 1024;
37266
+ var MAX_HOOK_SURFACE_BYTES = 128 * 1024;
37267
+ var MAX_GLOBAL_STATE_BYTES = 4 * 1024;
37268
+ var LISTENER_CREDENTIAL_FILE = "listener-credential.json";
37269
+ var RETIRED_HOOK_CREDENTIAL_FILE = "hook-credential.json";
37270
+ var HOOK_SURFACE_FILE = "hook-surface.json";
37271
+ var GLOBAL_STATE_FILE = "hook-check.json";
37272
+ var HOOK_SURFACE_LOCK = "hook-surface";
37273
+ var GLOBAL_STATE_LOCK = "hook-check";
37274
+ var HOOK_LOCK_TIMEOUT_MS = 250;
37275
+ var HOOK_CHECK_TIMEOUT_MS = 3e3;
37276
+ var HOOK_DEFAULT_COOLDOWN_SECONDS = 30;
37277
+ var HOOK_SURFACED_IDS_MAX = 1024;
37278
+ var HOOK_BODY_PREVIEW_CHARS = 240;
37279
+ function exactKeys2(row, keys) {
37280
+ const expected = new Set(keys);
37281
+ return Object.keys(row).length === expected.size && Object.keys(row).every((key2) => expected.has(key2));
37282
+ }
37283
+ function parseListenerCredential(raw) {
37284
+ let value;
37285
+ try {
37286
+ value = JSON.parse(raw);
37287
+ } catch {
37288
+ throw new Error("stored listener hook credential is malformed");
37289
+ }
37290
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37291
+ throw new Error("stored listener hook credential is malformed");
37292
+ }
37293
+ const row = value;
37294
+ if (!exactKeys2(row, [
37295
+ "version",
37296
+ "profileId",
37297
+ "targetUrl",
37298
+ "anonKey",
37299
+ "workspaceId",
37300
+ "principalId",
37301
+ "credential",
37302
+ "updatedAt"
37303
+ ]) || row.version !== 1 || typeof row.profileId !== "string" || !/^[0-9a-f]{24}$/.test(row.profileId) || typeof row.targetUrl !== "string" || typeof row.anonKey !== "string" || row.anonKey.length < 1 || row.anonKey.length > 4096 || typeof row.workspaceId !== "string" || !UUID_RE16.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE16.test(row.principalId) || typeof row.credential !== "string" || !TOKEN_RE.test(row.credential) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt))) {
37304
+ throw new Error("stored listener hook credential is malformed");
37305
+ }
37306
+ const target2 = cloudTarget(row.targetUrl, row.anonKey);
37307
+ if (target2.profileId !== row.profileId) {
37308
+ throw new Error("stored listener hook credential target does not match its profile");
37309
+ }
37310
+ return {
37311
+ version: 1,
37312
+ profileId: row.profileId,
37313
+ targetUrl: target2.url,
37314
+ anonKey: target2.anonKey,
37315
+ workspaceId: row.workspaceId.toLowerCase(),
37316
+ principalId: row.principalId.toLowerCase(),
37317
+ credential: row.credential,
37318
+ updatedAt: row.updatedAt
37319
+ };
37320
+ }
37321
+ async function writeListenerCredentialState(instanceDirectory, input) {
37322
+ if (!(0, import_node_path18.isAbsolute)(instanceDirectory)) {
37323
+ throw new Error("listener hook state directory must be absolute");
37324
+ }
37325
+ const record = parseListenerCredential(JSON.stringify({
37326
+ version: 1,
37327
+ profileId: input.target.profileId,
37328
+ targetUrl: input.target.url,
37329
+ anonKey: input.target.anonKey,
37330
+ workspaceId: input.workspaceId,
37331
+ principalId: input.principalId,
37332
+ credential: input.credential,
37333
+ updatedAt: new Date(input.now ?? Date.now()).toISOString()
37334
+ }));
37335
+ await writeSecureJsonFile(
37336
+ (0, import_node_path18.join)(instanceDirectory, LISTENER_CREDENTIAL_FILE),
37337
+ JSON.stringify(record)
37338
+ );
37339
+ await deleteSecureJsonFile(
37340
+ (0, import_node_path18.join)(instanceDirectory, RETIRED_HOOK_CREDENTIAL_FILE)
37341
+ ).catch(() => void 0);
37342
+ }
37343
+ async function readListenerCredentialState(instanceDirectory) {
37344
+ const raw = await readSecureJsonFile(
37345
+ (0, import_node_path18.join)(instanceDirectory, LISTENER_CREDENTIAL_FILE),
37346
+ MAX_HOOK_CREDENTIAL_BYTES
37347
+ );
37348
+ return raw === null ? null : parseListenerCredential(raw);
37349
+ }
37350
+ function parseSurface(raw) {
37351
+ let value;
37352
+ try {
37353
+ value = JSON.parse(raw);
37354
+ } catch {
37355
+ throw new Error("stored listener hook surface state is malformed");
37356
+ }
37357
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37358
+ throw new Error("stored listener hook surface state is malformed");
37359
+ }
37360
+ const row = value;
37361
+ if (Object.keys(row).some(
37362
+ (key2) => key2 !== "version" && key2 !== "surfacedSignalIds" && key2 !== "reportedDroppedCount" && key2 !== "credentialFailureReported"
37363
+ ) || row.version !== 1 || !Array.isArray(row.surfacedSignalIds) || row.surfacedSignalIds.length > HOOK_SURFACED_IDS_MAX || row.surfacedSignalIds.some((id) => typeof id !== "string" || !UUID_RE16.test(id)) || !(row.reportedDroppedCount === void 0 || typeof row.reportedDroppedCount === "number" && Number.isSafeInteger(row.reportedDroppedCount) && row.reportedDroppedCount >= 0) || !(row.credentialFailureReported === void 0 || typeof row.credentialFailureReported === "boolean")) {
37364
+ throw new Error("stored listener hook surface state is malformed");
37365
+ }
37366
+ const ids = row.surfacedSignalIds.map((id) => String(id).toLowerCase());
37367
+ if (new Set(ids).size !== ids.length) {
37368
+ throw new Error("stored listener hook surface state repeats a signal");
37369
+ }
37370
+ return {
37371
+ version: 1,
37372
+ surfacedSignalIds: ids,
37373
+ reportedDroppedCount: typeof row.reportedDroppedCount === "number" ? row.reportedDroppedCount : 0,
37374
+ credentialFailureReported: row.credentialFailureReported === true
37375
+ };
37376
+ }
37377
+ var FileHookSurfaceStore = class {
37378
+ constructor(instanceDirectory) {
37379
+ this.instanceDirectory = instanceDirectory;
37380
+ if (!(0, import_node_path18.isAbsolute)(instanceDirectory)) {
37381
+ throw new Error("listener hook surface directory must be absolute");
37382
+ }
37383
+ this.path = (0, import_node_path18.join)(instanceDirectory, HOOK_SURFACE_FILE);
37384
+ }
37385
+ instanceDirectory;
37386
+ path;
37387
+ async stage(items, droppedCount) {
37388
+ return await withFileLock(this.instanceDirectory, HOOK_SURFACE_LOCK, async () => {
37389
+ const raw = await readSecureJsonFile(this.path, MAX_HOOK_SURFACE_BYTES);
37390
+ const state = raw === null ? {
37391
+ version: 1,
37392
+ surfacedSignalIds: [],
37393
+ reportedDroppedCount: 0,
37394
+ credentialFailureReported: false
37395
+ } : parseSurface(raw);
37396
+ const seen = new Set(state.surfacedSignalIds);
37397
+ const unseen = [];
37398
+ for (const item of items) {
37399
+ const signalId = item.signalId.toLowerCase();
37400
+ if (!UUID_RE16.test(signalId) || seen.has(signalId)) continue;
37401
+ seen.add(signalId);
37402
+ unseen.push(item);
37403
+ }
37404
+ return {
37405
+ unseen,
37406
+ droppedSinceLastCheck: droppedCount < state.reportedDroppedCount ? droppedCount : droppedCount - state.reportedDroppedCount,
37407
+ credentialFailureReported: state.credentialFailureReported
37408
+ };
37409
+ }, { timeoutMs: HOOK_LOCK_TIMEOUT_MS });
37410
+ }
37411
+ async commit(options) {
37412
+ await withFileLock(this.instanceDirectory, HOOK_SURFACE_LOCK, async () => {
37413
+ const raw = await readSecureJsonFile(this.path, MAX_HOOK_SURFACE_BYTES);
37414
+ const state = raw === null ? {
37415
+ version: 1,
37416
+ surfacedSignalIds: [],
37417
+ reportedDroppedCount: 0,
37418
+ credentialFailureReported: false
37419
+ } : parseSurface(raw);
37420
+ const seen = new Set(state.surfacedSignalIds);
37421
+ for (const signalId of options.signalIds ?? []) {
37422
+ const checked = signalId.toLowerCase();
37423
+ if (UUID_RE16.test(checked)) seen.add(checked);
37424
+ }
37425
+ await writeSecureJsonFile(
37426
+ this.path,
37427
+ JSON.stringify(parseSurface(JSON.stringify({
37428
+ version: 1,
37429
+ surfacedSignalIds: [...seen].slice(-HOOK_SURFACED_IDS_MAX),
37430
+ reportedDroppedCount: options.droppedCount ?? state.reportedDroppedCount,
37431
+ credentialFailureReported: options.credentialFailureReported ?? state.credentialFailureReported
37432
+ })))
37433
+ );
37434
+ }, { timeoutMs: HOOK_LOCK_TIMEOUT_MS });
37435
+ }
37436
+ async claimUnseen(items) {
37437
+ const staged = await this.stage(items, 0);
37438
+ if (staged.unseen.length > 0) {
37439
+ await this.commit({ signalIds: staged.unseen.map((item) => item.signalId) });
37440
+ }
37441
+ return staged.unseen;
37442
+ }
37443
+ };
37444
+ function parseGlobalState(raw) {
37445
+ let value;
37446
+ try {
37447
+ value = JSON.parse(raw);
37448
+ } catch {
37449
+ throw new Error("stored hook cooldown state is malformed");
37450
+ }
37451
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
37452
+ throw new Error("stored hook cooldown state is malformed");
37453
+ }
37454
+ const row = value;
37455
+ if (!exactKeys2(row, ["version", "lastCheckAt"]) || row.version !== 1 || typeof row.lastCheckAt !== "number" || !Number.isSafeInteger(row.lastCheckAt) || row.lastCheckAt < 0) {
37456
+ throw new Error("stored hook cooldown state is malformed");
37457
+ }
37458
+ return { version: 1, lastCheckAt: row.lastCheckAt };
37459
+ }
37460
+ async function reserveCheck(stateDirectory2, cooldownMs, now) {
37461
+ return await withFileLock(stateDirectory2, GLOBAL_STATE_LOCK, async () => {
37462
+ const path = (0, import_node_path18.join)(stateDirectory2, GLOBAL_STATE_FILE);
37463
+ const raw = await readSecureJsonFile(path, MAX_GLOBAL_STATE_BYTES);
37464
+ const previous = raw === null ? null : parseGlobalState(raw);
37465
+ if (previous !== null && now - previous.lastCheckAt < cooldownMs) return false;
37466
+ await writeSecureJsonFile(path, JSON.stringify({ version: 1, lastCheckAt: now }));
37467
+ return true;
37468
+ }, { timeoutMs: HOOK_LOCK_TIMEOUT_MS });
37469
+ }
37470
+ function processIsAlive(pid) {
37471
+ try {
37472
+ process.kill(pid, 0);
37473
+ return true;
37474
+ } catch (error) {
37475
+ return error.code === "EPERM";
37476
+ }
37477
+ }
37478
+ async function statusContext(stateDirectory2, key2, instanceDirectory) {
37479
+ const provisional = {
37480
+ key: key2,
37481
+ instanceDirectory,
37482
+ statusPath: (0, import_node_path18.join)(instanceDirectory, "status.json"),
37483
+ logPath: (0, import_node_path18.join)(instanceDirectory, "events.ndjson"),
37484
+ socketPath: ""
37485
+ };
37486
+ const status = await readListenerStatus(provisional).catch(() => null);
37487
+ if (status === null) return null;
37488
+ const paths = listenerPaths({
37489
+ profileId: status.profileId,
37490
+ workspaceId: status.workspaceId,
37491
+ principalId: status.principalId,
37492
+ stateDirectory: stateDirectory2
37493
+ });
37494
+ if (paths.key !== key2 || paths.instanceDirectory !== instanceDirectory) return null;
37495
+ return { paths, status };
37496
+ }
37497
+ async function listenerIsLive(context) {
37498
+ if (context.status.state === "stopped" || context.status.state === "failed" || !processIsAlive(context.status.pid)) {
37499
+ return false;
37500
+ }
37501
+ try {
37502
+ await queryListenerControl(context.paths, "status", 250);
37503
+ return true;
37504
+ } catch {
37505
+ return false;
37506
+ }
37507
+ }
37508
+ async function discoverContexts(stateDirectory2, isListenerLive = listenerIsLive) {
37509
+ let entries;
37510
+ try {
37511
+ entries = await (0, import_promises10.readdir)(stateDirectory2, { withFileTypes: true });
37512
+ } catch (error) {
37513
+ if (error.code === "ENOENT") return [];
37514
+ throw error;
37515
+ }
37516
+ const contexts = [];
37517
+ for (const entry of entries) {
37518
+ if (!entry.isDirectory() || !INSTANCE_KEY_RE.test(entry.name)) continue;
37519
+ const instanceDirectory = (0, import_node_path18.join)(stateDirectory2, entry.name);
37520
+ const storedStatus = await statusContext(
37521
+ stateDirectory2,
37522
+ entry.name,
37523
+ instanceDirectory
37524
+ );
37525
+ const statusIsLive = storedStatus === null ? null : await isListenerLive(storedStatus);
37526
+ if (statusIsLive === false) continue;
37527
+ await deleteSecureJsonFile(
37528
+ (0, import_node_path18.join)(instanceDirectory, RETIRED_HOOK_CREDENTIAL_FILE)
37529
+ ).catch(() => void 0);
37530
+ try {
37531
+ const credential = await readListenerCredentialState(instanceDirectory);
37532
+ contexts.push({
37533
+ instanceDirectory,
37534
+ paths: storedStatus?.paths ?? null,
37535
+ status: storedStatus?.status ?? null,
37536
+ credential,
37537
+ credentialReadFailed: credential === null && storedStatus !== null
37538
+ });
37539
+ } catch {
37540
+ if (storedStatus === null) continue;
37541
+ contexts.push({
37542
+ instanceDirectory,
37543
+ paths: storedStatus.paths,
37544
+ status: storedStatus.status,
37545
+ credential: null,
37546
+ credentialReadFailed: true
37547
+ });
37548
+ }
37549
+ }
37550
+ return contexts;
37551
+ }
37552
+ function entryFromSignal(signal, principalId, directory, now) {
37553
+ const senderName = signal.from_kind === "agent" ? directory?.agents.find((agent) => agent.principal_id === signal.from)?.name ?? null : directory?.members.find((member) => member.user_id === signal.from)?.display_name ?? null;
37554
+ return {
37555
+ signalId: signal.id,
37556
+ workspaceId: signal.workspace_id,
37557
+ principalId,
37558
+ fromId: signal.from,
37559
+ fromKind: signal.from_kind,
37560
+ senderName,
37561
+ body: signal.body,
37562
+ createdAt: signal.created_at,
37563
+ queuedAt: new Date(now).toISOString()
37564
+ };
37565
+ }
37566
+ function preview(value) {
37567
+ let text = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
37568
+ if (text.length > HOOK_BODY_PREVIEW_CHARS) {
37569
+ text = `${text.slice(0, HOOK_BODY_PREVIEW_CHARS - 1)}\u2026`;
37570
+ }
37571
+ return JSON.stringify(text);
37572
+ }
37573
+ function renderHookSignal(item) {
37574
+ const sender = item.senderName === null ? item.fromId : item.senderName;
37575
+ return [
37576
+ `[CSWARM MESSAGE from ${item.fromKind} ${JSON.stringify(sender)}; workspace ${item.workspaceId}]`,
37577
+ preview(item.body),
37578
+ `operator reply command: cswarm reply ${item.signalId} "<answer>" --workspace-id ${item.workspaceId}`
37579
+ ].join("\n");
37580
+ }
37581
+ async function inboxItems(context, options) {
37582
+ const stored = context.credential;
37583
+ const target2 = cloudTarget(stored.targetUrl, stored.anonKey);
37584
+ const readOptions = {
37585
+ ...options.fetcher ? { fetcher: options.fetcher } : {},
37586
+ signal: options.signal,
37587
+ deadlineMs: options.deadlineMs,
37588
+ now: options.now
37589
+ };
37590
+ const page = await readAgentSignalPage(
37591
+ target2,
37592
+ { kind: "agent", token: stored.credential },
37593
+ {
37594
+ workspaceId: stored.workspaceId,
37595
+ inbox: true,
37596
+ ascending: false,
37597
+ limit: 100,
37598
+ includeStale: false
37599
+ },
37600
+ readOptions,
37601
+ { tolerateMalformedRows: true, maxMalformedRows: 3 }
37602
+ );
37603
+ const directed = page.signals.filter(
37604
+ (signal) => (signal.kind === "ask" || signal.kind === "note") && signal.workspace_id === stored.workspaceId && signal.to_agent === stored.principalId
37605
+ );
37606
+ if (directed.length === 0) return [];
37607
+ let directory = null;
37608
+ try {
37609
+ directory = await readAgentSignalDirectory(
37610
+ target2,
37611
+ stored.credential,
37612
+ stored.workspaceId,
37613
+ readOptions
37614
+ );
37615
+ } catch {
37616
+ directory = null;
37617
+ }
37618
+ const candidates = directed.map((signal) => entryFromSignal(signal, stored.principalId, directory, options.now())).sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
37619
+ return candidates;
37620
+ }
37621
+ function renderDroppedAsks(count2) {
37622
+ return `${count2} routed asks were dropped from the overflow queue; check cswarm inbox. The signals remain in the inbox.`;
37623
+ }
37624
+ var CREDENTIAL_READ_WARNING = "CommonSwarm could not read the configured listener credential safely. The hook is not checking routed asks. Restart the listener with a fresh credential; any credential state file must be mode 0600. Then run cswarm listen status.";
37625
+ var CREDENTIAL_401_WARNING = "CommonSwarm could not authenticate a listener credential (HTTP 401). The hook is not checking routed asks. Restart the listener with a fresh credential, then run cswarm listen status.";
37626
+ async function checkListenerHooks(options) {
37627
+ try {
37628
+ const stateDirectory2 = options.stateDirectory ?? defaultListenerStateDirectory();
37629
+ if (!(0, import_node_path18.isAbsolute)(stateDirectory2)) return "";
37630
+ const now = options.now ?? Date.now;
37631
+ const cooldownSeconds = options.cooldownSeconds ?? HOOK_DEFAULT_COOLDOWN_SECONDS;
37632
+ if (!Number.isSafeInteger(cooldownSeconds) || cooldownSeconds < 0 || cooldownSeconds > 86400) {
37633
+ return "";
37634
+ }
37635
+ const contexts = await discoverContexts(
37636
+ stateDirectory2,
37637
+ options.isListenerLive ?? listenerIsLive
37638
+ );
37639
+ if (contexts.length === 0) return "";
37640
+ const networkAllowed = await reserveCheck(
37641
+ stateDirectory2,
37642
+ cooldownSeconds * 1e3,
37643
+ now()
37644
+ );
37645
+ const checks = await Promise.all(contexts.map(async (context) => {
37646
+ const queue = new FilePendingMainQueue(context.instanceDirectory);
37647
+ const pending = await queue.read();
37648
+ const stats = await queue.stats();
37649
+ let network = [];
37650
+ let credentialFailure = context.credentialReadFailed ? "read" : null;
37651
+ let credentialHealthy = false;
37652
+ if (networkAllowed && !options.signal.aborted && context.credential !== null) {
37653
+ try {
37654
+ network = await inboxItems(context, {
37655
+ ...options.fetcher ? { fetcher: options.fetcher } : {},
37656
+ signal: options.signal,
37657
+ deadlineMs: options.deadlineMs,
37658
+ now
37659
+ });
37660
+ credentialHealthy = true;
37661
+ } catch (error) {
37662
+ if (followHttpDetails(error)?.status === 401) credentialFailure = "401";
37663
+ }
37664
+ }
37665
+ return {
37666
+ context,
37667
+ queue,
37668
+ pending,
37669
+ droppedCount: stats.droppedCount,
37670
+ network,
37671
+ credentialFailure,
37672
+ credentialHealthy
37673
+ };
37674
+ }));
37675
+ const commits = [];
37676
+ const blocks = [];
37677
+ const emittedCredentialWarnings = /* @__PURE__ */ new Set();
37678
+ for (const check of checks) {
37679
+ const store2 = new FileHookSurfaceStore(check.context.instanceDirectory);
37680
+ const staged = await store2.stage(
37681
+ [...check.pending, ...check.network],
37682
+ check.droppedCount
37683
+ );
37684
+ blocks.push(...staged.unseen.map(renderHookSignal));
37685
+ const reportDrops = staged.droppedSinceLastCheck > 0;
37686
+ if (reportDrops) blocks.push(renderDroppedAsks(staged.droppedSinceLastCheck));
37687
+ const reportCredentialFailure = check.credentialFailure !== null && !staged.credentialFailureReported;
37688
+ if (reportCredentialFailure) {
37689
+ const warning = check.credentialFailure === "401" ? CREDENTIAL_401_WARNING : CREDENTIAL_READ_WARNING;
37690
+ if (!emittedCredentialWarnings.has(warning)) {
37691
+ emittedCredentialWarnings.add(warning);
37692
+ blocks.push(warning);
37693
+ }
37694
+ }
37695
+ const pendingSignalIds = new Set(check.pending.map((entry) => entry.signalId));
37696
+ commits.push({
37697
+ check,
37698
+ store: store2,
37699
+ signalIds: staged.unseen.map((item) => item.signalId),
37700
+ printedPendingSignalIds: staged.unseen.map((item) => item.signalId).filter((signalId) => pendingSignalIds.has(signalId)),
37701
+ reportDrops,
37702
+ reportCredentialFailure
37703
+ });
37704
+ }
37705
+ const output = blocks.join("\n\n");
37706
+ if (output.length > 0) await (options.write ?? (() => void 0))(output);
37707
+ for (const commit of commits) {
37708
+ await commit.store.commit({
37709
+ signalIds: commit.signalIds,
37710
+ ...commit.reportDrops ? { droppedCount: commit.check.droppedCount } : {},
37711
+ ...commit.reportCredentialFailure ? { credentialFailureReported: true } : commit.check.credentialHealthy ? { credentialFailureReported: false } : {}
37712
+ });
37713
+ const remainingCount = await commit.check.queue.remove(
37714
+ new Set(commit.printedPendingSignalIds),
37715
+ HOOK_LOCK_TIMEOUT_MS
37716
+ );
37717
+ if (commit.check.context.paths !== null && commit.check.context.status !== null) {
37718
+ const latest = await readListenerStatus(commit.check.context.paths).catch(() => null);
37719
+ if (latest !== null && latest.pendingForMainCount !== remainingCount) {
37720
+ await writeListenerStatus(commit.check.context.paths, {
37721
+ ...latest,
37722
+ pendingForMainCount: remainingCount
37723
+ });
37724
+ }
37725
+ }
37726
+ }
37727
+ return output;
37728
+ } catch {
37729
+ return "";
37730
+ }
37731
+ }
37732
+ async function runListenerHookCheck(options = {}) {
37733
+ const controller = new AbortController();
37734
+ const deadlineMs = Date.now() + HOOK_CHECK_TIMEOUT_MS;
37735
+ let timer2;
37736
+ try {
37737
+ const checking = checkListenerHooks({
37738
+ ...options,
37739
+ signal: controller.signal,
37740
+ deadlineMs
37741
+ });
37742
+ const timedOut = new Promise((resolve) => {
37743
+ timer2 = setTimeout(() => {
37744
+ controller.abort();
37745
+ resolve("");
37746
+ }, HOOK_CHECK_TIMEOUT_MS);
37747
+ });
37748
+ return await Promise.race([checking, timedOut]);
37749
+ } catch {
37750
+ return "";
37751
+ } finally {
37752
+ if (timer2 !== void 0) clearTimeout(timer2);
37753
+ }
37754
+ }
37755
+
36879
37756
  // src/cli.ts
36880
37757
  var import_meta = {};
36881
37758
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
@@ -36888,7 +37765,9 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
36888
37765
  "claude-executable",
36889
37766
  "codex-executable",
36890
37767
  "confirm",
37768
+ "cooldown",
36891
37769
  "cwd",
37770
+ "defer-over",
36892
37771
  "device-id",
36893
37772
  "effort",
36894
37773
  "email",
@@ -36920,6 +37799,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
36920
37799
  "principal-id",
36921
37800
  "provider",
36922
37801
  "reveal-anon-key",
37802
+ "route",
36923
37803
  "run-id",
36924
37804
  "since",
36925
37805
  "site",
@@ -36934,7 +37814,8 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
36934
37814
  "url",
36935
37815
  "version",
36936
37816
  "wait",
36937
- "workspace-id"
37817
+ "workspace-id",
37818
+ "write"
36938
37819
  ]);
36939
37820
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
36940
37821
  "agent-token-stdin",
@@ -36952,9 +37833,10 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
36952
37833
  "local",
36953
37834
  "ndjson",
36954
37835
  "no-browser",
36955
- "reveal-anon-key"
37836
+ "reveal-anon-key",
37837
+ "write"
36956
37838
  ]);
36957
- var UUID_RE15 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
37839
+ var UUID_RE17 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
36958
37840
  var AGENT_CREDENTIAL_MESSAGE = "Agent credential minted. It is bound to this task and run so the agent's work stays scoped and attributable.";
36959
37841
  var AGENT_CREDENTIAL_MESSAGE_D088 = "Agent credential minted. It is bound to this run, so the agent's work is attributable to it.";
36960
37842
  var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
@@ -36962,8 +37844,8 @@ var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
36962
37844
  AGENT_CREDENTIAL_MESSAGE_D088
36963
37845
  ];
36964
37846
  function packageVersion() {
36965
- if ("0.1.23".length > 0) {
36966
- return "0.1.23";
37847
+ if ("0.1.25".length > 0) {
37848
+ return "0.1.25";
36967
37849
  }
36968
37850
  try {
36969
37851
  const value = JSON.parse(
@@ -37091,9 +37973,12 @@ Usage:
37091
37973
  cswarm file rm <name|file-id> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
37092
37974
  cswarm file restore <name|file-id> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
37093
37975
  cswarm feedback "<text>" --kind bug|idea|friction [--about <ref>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
37094
- cswarm listen start --agent-token-stdin [--url <url> --anon-key <key>] --workspace-id <uuid> --provider grok|opencode|claude|codex [--cwd <absolute-path>] [--model <model>] [--effort <level>] [--permissions deny|allow] [--grok-executable <path>] [--opencode-executable <path>] [--claude-executable <path>] [--codex-executable <path>] [--turn-budget <duration>] [--foreground] [--json]
37976
+ cswarm listen start --agent-token-stdin [--url <url> --anon-key <key>] --workspace-id <uuid> --provider grok|opencode|claude|codex [--cwd <absolute-path>] [--model <model>] [--effort <level>] [--permissions deny|allow] [--grok-executable <path>] [--opencode-executable <path>] [--claude-executable <path>] [--codex-executable <path>] [--turn-budget <duration>] [--route worker|main|split] [--defer-over <chars>] [--foreground] [--json]
37095
37977
  cswarm listen status [--url <url> --anon-key <key>] --workspace-id <uuid> --principal-id <uuid> [--json]
37096
37978
  cswarm listen stop [--url <url> --anon-key <key>] --workspace-id <uuid> --principal-id <uuid> [--json]
37979
+ cswarm hook check [--cooldown <seconds>]
37980
+ cswarm hook install claude [--write]
37981
+ cswarm hook uninstall claude --write
37097
37982
  cswarm new "<workspace name>" [--url <url> --anon-key <key>] [--json]
37098
37983
  cswarm new --name "<workspace name>" [--url <url> --anon-key <key>] [--json]
37099
37984
  cswarm workspaces [--url <url> --anon-key <key>] [--json]
@@ -37129,6 +38014,10 @@ Credential selection for command/dogfood:
37129
38014
  read and command, nothing persisted -- either form
37130
38015
  feedback command only, nothing persisted -- either form
37131
38016
  listen start persists durable state, rotates -- needs expires_at
38017
+ hook check reads the listener's owned 0600 credential state;
38018
+ never accepts or prints a credential
38019
+ hook install/uninstall
38020
+ edits only local Claude Code settings; no credential
37132
38021
  token revoke names what it revokes -- needs token_id
37133
38022
  workspace close is human-session-only; it never accepts an agent token
37134
38023
 
@@ -37153,6 +38042,15 @@ TTL); a turn that lands just before a rotation can be clamped to the ~5m
37153
38042
  renewal lead, and if it times out there, durable delivery retries it on the
37154
38043
  fresh credential.
37155
38044
 
38045
+ listen start --route worker|main|split chooses where directed asks go. worker
38046
+ is the unchanged default. main queues every ask for the interactive session.
38047
+ split queues asks whose body is longer than --defer-over <chars>; the bound is
38048
+ 1..10000 and an equal-length ask stays on the worker path. Run cswarm hook check
38049
+ to surface queued asks. hook check has its own 3s ceiling, exits 0 on every
38050
+ outcome, and skips network checks made within --cooldown seconds (default 30).
38051
+ hook install claude prints the UserPromptSubmit JSON by default and changes the
38052
+ project's .claude/settings.json only with --write; uninstall also requires --write.
38053
+
37156
38054
  Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, new, and workspace close require a
37157
38055
  stored human login. Agent self-surrender of a token uses --agent-token-stdin and never takes the secret on argv. Invite-link accept signs in when needed, then accepts and
37158
38056
  registers one principal. Invitation links, agent credentials, and capability links
@@ -37281,7 +38179,7 @@ function parsedAgentCredential(value) {
37281
38179
  const withExpiry = [...requiredKeys, "expires_at"].sort();
37282
38180
  const actualKeys = Object.keys(artifact).sort();
37283
38181
  const shape = actualKeys.length === requiredKeys.length ? requiredKeys : withExpiry;
37284
- if (actualKeys.length !== shape.length || !actualKeys.every((key2, index) => key2 === shape[index]) || !ACCEPTED_AGENT_CREDENTIAL_MESSAGES.includes(artifact.message) || artifact.status !== "accepted" || typeof artifact.principal_id !== "string" || !UUID_RE15.test(artifact.principal_id) || typeof artifact.token_id !== "string" || !UUID_RE15.test(artifact.token_id) || typeof artifact.run_id !== "string" || !UUID_RE15.test(artifact.run_id) || typeof artifact.agent_token !== "string") {
38182
+ if (actualKeys.length !== shape.length || !actualKeys.every((key2, index) => key2 === shape[index]) || !ACCEPTED_AGENT_CREDENTIAL_MESSAGES.includes(artifact.message) || artifact.status !== "accepted" || typeof artifact.principal_id !== "string" || !UUID_RE17.test(artifact.principal_id) || typeof artifact.token_id !== "string" || !UUID_RE17.test(artifact.token_id) || typeof artifact.run_id !== "string" || !UUID_RE17.test(artifact.run_id) || typeof artifact.agent_token !== "string") {
37285
38183
  throw new Error("agent credential JSON is malformed");
37286
38184
  }
37287
38185
  let expiresAt = null;
@@ -37362,7 +38260,7 @@ async function stdinInviteLink() {
37362
38260
  return link;
37363
38261
  }
37364
38262
  async function confirmationLine(prompt) {
37365
- const reader = (0, import_promises11.createInterface)({
38263
+ const reader = (0, import_promises12.createInterface)({
37366
38264
  input: process.stdin,
37367
38265
  output: process.stderr,
37368
38266
  terminal: Boolean(process.stdin.isTTY)
@@ -37519,7 +38417,7 @@ async function runNew(args) {
37519
38417
  project: {
37520
38418
  workspace_id: created,
37521
38419
  name,
37522
- stream_id: typeof response.stream_id === "string" && UUID_RE15.test(response.stream_id) ? response.stream_id : null
38420
+ stream_id: typeof response.stream_id === "string" && UUID_RE17.test(response.stream_id) ? response.stream_id : null
37523
38421
  }
37524
38422
  });
37525
38423
  return;
@@ -38293,7 +39191,7 @@ async function runLinkNew(args) {
38293
39191
  2
38294
39192
  );
38295
39193
  const taskId = args.required("task-id");
38296
- if (!UUID_RE15.test(taskId)) {
39194
+ if (!UUID_RE17.test(taskId)) {
38297
39195
  throw new Error("--task-id must be the work item's UUID");
38298
39196
  }
38299
39197
  const site = capabilitySiteOrigin(
@@ -38353,7 +39251,7 @@ async function runLinkRevoke(args) {
38353
39251
  2
38354
39252
  );
38355
39253
  const capabilityId = args.required("capability-id");
38356
- if (!UUID_RE15.test(capabilityId)) {
39254
+ if (!UUID_RE17.test(capabilityId)) {
38357
39255
  throw new Error(
38358
39256
  "--capability-id must be the id printed when the link was created"
38359
39257
  );
@@ -38568,6 +39466,33 @@ function listenerTurnBudgetMs(value) {
38568
39466
  }
38569
39467
  return milliseconds;
38570
39468
  }
39469
+ function listenerRouteConfiguration(routeValue, deferOverValue) {
39470
+ const routeMode = routeValue ?? "worker";
39471
+ if (routeMode !== "worker" && routeMode !== "main" && routeMode !== "split") {
39472
+ throw new Error("--route must be worker, main, or split");
39473
+ }
39474
+ if (routeMode !== "split") {
39475
+ if (deferOverValue !== void 0) {
39476
+ throw new Error("--defer-over is only valid with --route split");
39477
+ }
39478
+ return { routeMode, deferOverChars: null };
39479
+ }
39480
+ if (deferOverValue === void 0) {
39481
+ throw new Error("--route split requires --defer-over <chars>");
39482
+ }
39483
+ if (!/^\d+$/.test(deferOverValue)) {
39484
+ throw new Error(
39485
+ `--defer-over must be an integer from ${LISTENER_DEFER_OVER_MIN} to ${LISTENER_DEFER_OVER_MAX}`
39486
+ );
39487
+ }
39488
+ const deferOverChars = Number(deferOverValue);
39489
+ if (!Number.isSafeInteger(deferOverChars) || deferOverChars < LISTENER_DEFER_OVER_MIN || deferOverChars > LISTENER_DEFER_OVER_MAX) {
39490
+ throw new Error(
39491
+ `--defer-over must be an integer from ${LISTENER_DEFER_OVER_MIN} to ${LISTENER_DEFER_OVER_MAX}`
39492
+ );
39493
+ }
39494
+ return { routeMode, deferOverChars };
39495
+ }
38571
39496
  var TURN_BUDGET_CREDENTIAL_MARGIN_MS = 6e4;
38572
39497
  function clampTurnBudgetToCredential(budgetMs, credentialExpiresAt, nowMs) {
38573
39498
  if (credentialExpiresAt === null) return budgetMs;
@@ -38862,7 +39787,7 @@ async function runReply(args) {
38862
39787
  "json"
38863
39788
  ], 3);
38864
39789
  const signalId = args.positionals[1];
38865
- if (signalId === void 0 || !UUID_RE15.test(signalId)) {
39790
+ if (signalId === void 0 || !UUID_RE17.test(signalId)) {
38866
39791
  throw new Error("reply requires the signal UUID being answered");
38867
39792
  }
38868
39793
  const body = args.positionals[2];
@@ -39190,7 +40115,7 @@ async function runInboxFollowCommand(args) {
39190
40115
  }
39191
40116
  }
39192
40117
  function listenerUuid(value, flag) {
39193
- if (!value || !UUID_RE15.test(value)) {
40118
+ if (!value || !UUID_RE17.test(value)) {
39194
40119
  throw new Error(`--${flag} must be a UUID`);
39195
40120
  }
39196
40121
  return value.toLowerCase();
@@ -39203,7 +40128,7 @@ function listenerPermissionMode(value) {
39203
40128
  function listenerStateDirectory(args) {
39204
40129
  const value = args.optional("state-dir");
39205
40130
  if (value === void 0) return void 0;
39206
- if (!(0, import_node_path17.isAbsolute)(value)) {
40131
+ if (!(0, import_node_path19.isAbsolute)(value)) {
39207
40132
  throw new Error("--state-dir must be an absolute path");
39208
40133
  }
39209
40134
  return value;
@@ -39363,18 +40288,25 @@ function listenerStatusJson(status, permissionMode) {
39363
40288
  lastTerminalDeliveryFailureAt: status.lastTerminalDeliveryFailureAt ?? null,
39364
40289
  lastClaimAt: status.lastClaimAt ?? null,
39365
40290
  lastAckAt: status.lastAckAt ?? null,
40291
+ routeMode: status.routeMode ?? "worker",
40292
+ deferOverChars: status.deferOverChars ?? null,
40293
+ pendingForMainCount: status.pendingForMainCount ?? 0,
40294
+ droppedForMainCount: status.droppedForMainCount ?? 0,
39366
40295
  ...mode3 ? {
39367
40296
  permission_mode: mode3,
39368
40297
  /* "allowed once" alone overstates it: allowOnceOrDeny selects allow_once only when the
39369
40298
  * host OFFERS that option, and denies otherwise. Both review arms flagged the
39370
40299
  * unqualified form on 4844b4e7. */
39371
- same_owner_delivery: mode3 === "allow" ? "worker session; tool requests allowed once each when the host offers allow_once, otherwise denied" : "worker session; tool requests denied",
39372
- cross_owner_delivery: mode3 === "allow" ? "same worker session with sender provenance; tool requests allowed once each when the host offers allow_once, otherwise denied" : "same worker session with sender provenance; tool requests denied"
40300
+ same_owner_delivery: status.routeMode === "main" ? "interactive session; no ACP worker prompt" : mode3 === "allow" ? "worker session; tool requests allowed once each when the host offers allow_once, otherwise denied" : "worker session; tool requests denied",
40301
+ cross_owner_delivery: status.routeMode === "main" ? "interactive session; no ACP worker prompt" : mode3 === "allow" ? "same worker session with sender provenance; tool requests allowed once each when the host offers allow_once, otherwise denied" : "same worker session with sender provenance; tool requests denied"
39373
40302
  } : {},
39374
40303
  host_limits: listenerHostLimits(status.provider)
39375
40304
  };
39376
40305
  }
39377
40306
  function renderListenerStatus(status) {
40307
+ const routeMode = status.routeMode ?? "worker";
40308
+ const pendingForMainCount = status.pendingForMainCount ?? 0;
40309
+ const droppedForMainCount = status.droppedForMainCount ?? 0;
39378
40310
  const lines = [
39379
40311
  `Listener ${status.state} for agent ${status.principalId}.`,
39380
40312
  `Provider: ${status.provider}; process: ${status.pid}; started: ${status.startedAt}.`,
@@ -39401,6 +40333,23 @@ function renderListenerStatus(status) {
39401
40333
  `Pending deliveries reported by the service: ${status.pendingDeliveryCount}.`
39402
40334
  );
39403
40335
  }
40336
+ lines.push(
40337
+ routeMode === "split" ? `Ask route: split; bodies over ${status.deferOverChars} characters wait for this interactive session.` : routeMode === "main" ? "Ask route: main; directed asks wait for this interactive session." : "Ask route: worker (default)."
40338
+ );
40339
+ if (routeMode !== "worker") {
40340
+ lines.push(`Asks waiting for this session: ${pendingForMainCount}.`);
40341
+ lines.push(`Routed asks dropped from the overflow queue: ${droppedForMainCount}.`);
40342
+ if (droppedForMainCount > 0) {
40343
+ lines.push(
40344
+ "The signals remain in the inbox. Recover them with: cswarm inbox"
40345
+ );
40346
+ }
40347
+ if (pendingForMainCount > 0) {
40348
+ lines.push(
40349
+ `${pendingForMainCount} asks waiting for this session; they surface at your next prompt, or run cswarm hook check.`
40350
+ );
40351
+ }
40352
+ }
39404
40353
  if (status.lastTerminalDeliveryFailureCount !== null && status.lastTerminalDeliveryFailureCount > 0) {
39405
40354
  lines.push(
39406
40355
  `The last claim reported ${status.lastTerminalDeliveryFailureCount} terminal delivery failures; they remain recorded, and the listener will keep receiving.`
@@ -39485,7 +40434,7 @@ function resolveDetachedClaudeExecutable(executable = "claude-agent-acp", pathEn
39485
40434
  } catch (error) {
39486
40435
  const code = error.code;
39487
40436
  if (typeof code === "string") {
39488
- if ((0, import_node_path17.isAbsolute)(executable) || executable.includes("/") || executable.includes("\\")) {
40437
+ if ((0, import_node_path19.isAbsolute)(executable) || executable.includes("/") || executable.includes("\\")) {
39489
40438
  const detail = error instanceof Error ? error.message : code;
39490
40439
  throw new Error(
39491
40440
  `could not use --claude-executable: ${detail}; reinstall the measured bridge with npm install -g @agentclientprotocol/claude-agent-acp@0.64.2 if this path should be replaced`
@@ -39502,7 +40451,7 @@ function resolveDetachedCodexExecutable(executable = "codex-acp", pathEnv = proc
39502
40451
  } catch (error) {
39503
40452
  const code = error.code;
39504
40453
  if (typeof code === "string") {
39505
- if ((0, import_node_path17.isAbsolute)(executable) || executable.includes("/") || executable.includes("\\")) {
40454
+ if ((0, import_node_path19.isAbsolute)(executable) || executable.includes("/") || executable.includes("\\")) {
39506
40455
  const detail = error instanceof Error ? error.message : code;
39507
40456
  throw new Error(
39508
40457
  `could not use --codex-executable: ${detail}; reinstall the measured bridge with npm install -g @agentclientprotocol/codex-acp@1.1.9 if this path should be replaced`
@@ -39547,11 +40496,32 @@ async function runConfiguredListener(options) {
39547
40496
  principalId: options.principalId,
39548
40497
  ...options.stateDirectory ? { stateDirectory: options.stateDirectory } : {}
39549
40498
  });
39550
- const credentialSession = await agentSession(
40499
+ const liveCredentialSession = await agentSession(
39551
40500
  options.cloud,
39552
40501
  options.workspaceId,
39553
40502
  options.agent
39554
40503
  );
40504
+ let storedCredential = null;
40505
+ const credentialSession = {
40506
+ bearer: async () => {
40507
+ const credential = await liveCredentialSession.bearer();
40508
+ if (credential !== storedCredential) {
40509
+ await writeListenerCredentialState(paths.instanceDirectory, {
40510
+ target: options.cloud,
40511
+ workspaceId: options.workspaceId,
40512
+ principalId: options.principalId,
40513
+ credential
40514
+ }).then(() => {
40515
+ storedCredential = credential;
40516
+ });
40517
+ }
40518
+ const stored = await readListenerCredentialState(paths.instanceDirectory);
40519
+ if (stored === null || stored.credential !== credential) {
40520
+ throw new Error("listener credential state did not preserve the live credential");
40521
+ }
40522
+ return stored.credential;
40523
+ }
40524
+ };
39555
40525
  const resolveSenderProvenance = async (signal, context) => {
39556
40526
  const credential = await credentialSession.bearer();
39557
40527
  const senderDirectory = await readAgentSignalDirectory(
@@ -39579,7 +40549,7 @@ async function runConfiguredListener(options) {
39579
40549
  }
39580
40550
  const applied = resolveTurnBudgetOrDefer(
39581
40551
  turnBudgetMs,
39582
- credentialSession.expiry,
40552
+ liveCredentialSession.expiry,
39583
40553
  Date.now(),
39584
40554
  renewalFailed
39585
40555
  );
@@ -39629,6 +40599,9 @@ async function runConfiguredListener(options) {
39629
40599
  };
39630
40600
  let selectedJournal;
39631
40601
  let selectedListenerInstanceId;
40602
+ const routeMode = options.routeMode ?? "worker";
40603
+ const deferOverChars = options.deferOverChars ?? null;
40604
+ const pendingMainQueue = new FilePendingMainQueue(paths.instanceDirectory);
39632
40605
  process.on("SIGINT", onProcessSignal);
39633
40606
  process.on("SIGTERM", onProcessSignal);
39634
40607
  try {
@@ -39639,6 +40612,8 @@ async function runConfiguredListener(options) {
39639
40612
  principalId: options.principalId,
39640
40613
  provider: options.provider,
39641
40614
  permissionMode: options.permissionMode,
40615
+ routeMode,
40616
+ deferOverChars,
39642
40617
  // The bound a timeout event reports: the last turn's clamped budget when
39643
40618
  // one has run, else the configured cap.
39644
40619
  getTurnBudgetMs: () => lastAppliedTurnBudgetMs ?? turnBudgetMs,
@@ -39677,7 +40652,10 @@ async function runConfiguredListener(options) {
39677
40652
  declareModel: listenerModelLabel(options.provider),
39678
40653
  listenerInstanceId,
39679
40654
  deliveryJournal: selectedJournal,
39680
- resolveSenderProvenance
40655
+ resolveSenderProvenance,
40656
+ routeMode,
40657
+ deferOverChars,
40658
+ pendingMainQueue
39681
40659
  });
39682
40660
  }
39683
40661
  });
@@ -39702,6 +40680,8 @@ async function runListenStart(args) {
39702
40680
  "codex-executable",
39703
40681
  "state-dir",
39704
40682
  "turn-budget",
40683
+ "route",
40684
+ "defer-over",
39705
40685
  "foreground",
39706
40686
  "json"
39707
40687
  ], 2);
@@ -39713,6 +40693,10 @@ async function runListenStart(args) {
39713
40693
  const provider = listenerProvider(args);
39714
40694
  validateListenerProviderFlags(args, provider);
39715
40695
  const turnBudgetMs = listenerTurnBudgetMs(args.optional("turn-budget"));
40696
+ const routing = listenerRouteConfiguration(
40697
+ args.optional("route"),
40698
+ args.optional("defer-over")
40699
+ );
39716
40700
  const cloud = await target(args);
39717
40701
  const workspaceId2 = listenerUuid(
39718
40702
  args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
@@ -39722,7 +40706,7 @@ async function runListenStart(args) {
39722
40706
  assertDurableListenerCredential(agent);
39723
40707
  const principalId = agent.principalId;
39724
40708
  const cwd = args.optional("cwd") ?? process.cwd();
39725
- if (!(0, import_node_path17.isAbsolute)(cwd)) throw new Error("--cwd must be an absolute path");
40709
+ if (!(0, import_node_path19.isAbsolute)(cwd)) throw new Error("--cwd must be an absolute path");
39726
40710
  const permissionMode = listenerPermissionMode(args.optional("permissions"));
39727
40711
  const stateDirectory2 = listenerStateDirectory(args);
39728
40712
  const paths = listenerPaths({
@@ -39748,6 +40732,7 @@ async function runListenStart(args) {
39748
40732
  permissionMode,
39749
40733
  provider,
39750
40734
  turnBudgetMs,
40735
+ ...routing,
39751
40736
  ...args.optional("model") ? { model: args.required("model") } : {},
39752
40737
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39753
40738
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
@@ -39758,7 +40743,7 @@ async function runListenStart(args) {
39758
40743
  });
39759
40744
  } else {
39760
40745
  const entrypoint = process.argv[1];
39761
- if (!entrypoint || !(0, import_node_path17.isAbsolute)(entrypoint)) {
40746
+ if (!entrypoint || !(0, import_node_path19.isAbsolute)(entrypoint)) {
39762
40747
  throw new Error("cannot locate the cswarm executable for detached start");
39763
40748
  }
39764
40749
  const artifact = JSON.stringify(agentCredentialArtifact({
@@ -39795,6 +40780,8 @@ async function runListenStart(args) {
39795
40780
  permissionMode,
39796
40781
  provider,
39797
40782
  nodeExecArgv: process.execArgv,
40783
+ route: routing.routeMode,
40784
+ ...routing.deferOverChars === null ? {} : { deferOver: routing.deferOverChars },
39798
40785
  ...stateDirectory2 ? { stateDirectory: stateDirectory2 } : {},
39799
40786
  ...args.optional("model") ? { model: args.required("model") } : {},
39800
40787
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
@@ -39828,17 +40815,36 @@ async function runListenStart(args) {
39828
40815
  listenerFailureMessage(status.lastErrorCode ?? "unknown_error", provider)
39829
40816
  );
39830
40817
  }
40818
+ if ((status.routeMode ?? "worker") !== "worker") {
40819
+ const recordedPending = status.pendingForMainCount ?? 0;
40820
+ const recordedDropped = status.droppedForMainCount ?? 0;
40821
+ const queueStats = await new FilePendingMainQueue(
40822
+ paths.instanceDirectory
40823
+ ).stats().catch(() => ({ count: recordedPending, droppedCount: recordedDropped }));
40824
+ status = {
40825
+ ...status,
40826
+ pendingForMainCount: queueStats.count,
40827
+ droppedForMainCount: queueStats.droppedCount
40828
+ };
40829
+ }
39831
40830
  if (args.has("json")) {
39832
40831
  printJson(listenerStatusJson(status, permissionMode));
39833
40832
  return;
39834
40833
  }
39835
- const hostNote = provider === "opencode" ? "The OpenCode worker uses one private auth/config home and your selected project cwd. Every sender reaches that worker with sender and operator provenance in the prompt. Tool requests are approved one at a time by default, when the worker asks and the host offers a one-time approval; --permissions deny refuses them. The deny canary does not cover steady-state allow.\n" : provider === "claude" ? "The Claude worker uses your selected cwd and normal Claude Code keychain/OAuth state through claude-agent-acp 0.64.2. Every sender reaches that worker with sender and operator provenance in the prompt.\n" : provider === "codex" ? "The Codex worker uses your selected cwd and normal ChatGPT/Codex auth through codex-acp 1.1.9. CommonSwarm selects read-only mode before its deny canary. Every sender reaches that worker with sender and operator provenance in the prompt.\n" : "The Grok worker uses your selected cwd and local Grok configuration, including user and cmux hooks. Every sender reaches that worker with sender and operator provenance in the prompt.\n";
40834
+ const routingNote = routing.routeMode === "main" ? "Directed asks are queued for your interactive session and never prompt the ACP worker. Run cswarm hook check to surface them.\n" : routing.routeMode === "split" ? `Directed asks over ${routing.deferOverChars} characters are queued for your interactive session; shorter asks use the worker. Run cswarm hook check to surface queued asks.
40835
+ ` : "";
40836
+ const workerAudience = routing.routeMode === "main" ? "Directed asks do not reach that worker." : routing.routeMode === "split" ? "Only asks at or below the split threshold reach that worker, with sender and operator provenance in the prompt." : "Every sender reaches that worker with sender and operator provenance in the prompt.";
40837
+ const hostNote = provider === "opencode" ? `The OpenCode worker uses one private auth/config home and your selected project cwd. ${workerAudience} Tool requests are approved one at a time by default, when the worker asks and the host offers a one-time approval; --permissions deny refuses them. The deny canary does not cover steady-state allow.
40838
+ ` : provider === "claude" ? `The Claude worker uses your selected cwd and normal Claude Code keychain/OAuth state through claude-agent-acp 0.64.2. ${workerAudience}
40839
+ ` : provider === "codex" ? `The Codex worker uses your selected cwd and normal ChatGPT/Codex auth through codex-acp 1.1.9. CommonSwarm selects read-only mode before its deny canary. ${workerAudience}
40840
+ ` : `The Grok worker uses your selected cwd and local Grok configuration, including user and cmux hooks. ${workerAudience}
40841
+ `;
39836
40842
  process.stdout.write(
39837
40843
  `${args.has("foreground") ? "Listener stopped." : "Listener is ready and will keep receiving after this command exits."}
39838
40844
  ${renderListenerStatus(status)}
39839
40845
  Same-owner tool requests are ${permissionMode === "allow" ? "approved one at a time, when the worker asks and the host offers a one-time approval" : "denied. This worker can reply to messages but cannot do anything it must ask permission for; restart with --permissions allow if that is not what you want"}. The same permission mode applies to every sender relation.
39840
40846
  The short credential rotates while this process remains alive and secure local state is available; a person reauthorises after the 30-day horizon.
39841
- ` + hostNote + `Use listen status/stop with --workspace-id ${workspaceId2} --principal-id ${principalId} and the same Cloud target.
40847
+ ` + routingNote + hostNote + `Use listen status/stop with --workspace-id ${workspaceId2} --principal-id ${principalId} and the same Cloud target.
39842
40848
  `
39843
40849
  );
39844
40850
  }
@@ -39857,18 +40863,24 @@ async function runListenSupervisor(args) {
39857
40863
  "claude-executable",
39858
40864
  "codex-executable",
39859
40865
  "state-dir",
39860
- "turn-budget"
40866
+ "turn-budget",
40867
+ "route",
40868
+ "defer-over"
39861
40869
  ], 1);
39862
40870
  const provider = listenerProvider(args);
39863
40871
  validateListenerProviderFlags(args, provider);
39864
40872
  const turnBudgetMs = listenerTurnBudgetMs(args.optional("turn-budget"));
40873
+ const routing = listenerRouteConfiguration(
40874
+ args.optional("route"),
40875
+ args.optional("defer-over")
40876
+ );
39865
40877
  const cloud = await target(args);
39866
40878
  const workspaceId2 = listenerUuid(args.optional("workspace-id"), "workspace-id");
39867
40879
  const principalId = listenerUuid(args.optional("principal-id"), "principal-id");
39868
40880
  const agent = await stdinCredential();
39869
40881
  assertDurableListenerCredential(agent, principalId);
39870
40882
  const cwd = args.required("cwd");
39871
- if (!(0, import_node_path17.isAbsolute)(cwd)) throw new Error("--cwd must be an absolute path");
40883
+ if (!(0, import_node_path19.isAbsolute)(cwd)) throw new Error("--cwd must be an absolute path");
39872
40884
  const status = await runConfiguredListener({
39873
40885
  cloud,
39874
40886
  workspaceId: workspaceId2,
@@ -39878,6 +40890,7 @@ async function runListenSupervisor(args) {
39878
40890
  permissionMode: listenerPermissionMode(args.optional("permissions")),
39879
40891
  provider,
39880
40892
  turnBudgetMs,
40893
+ ...routing,
39881
40894
  ...args.optional("model") ? { model: args.required("model") } : {},
39882
40895
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39883
40896
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
@@ -39910,7 +40923,7 @@ async function runListenStatusOrStop(args, command2) {
39910
40923
  principalId,
39911
40924
  ...stateDirectory2 ? { stateDirectory: stateDirectory2 } : {}
39912
40925
  });
39913
- const status = command2 === "stop" ? await stopListener(paths) : await effectiveListenerStatus(paths);
40926
+ let status = command2 === "stop" ? await stopListener(paths) : await effectiveListenerStatus(paths);
39914
40927
  if (status === null) {
39915
40928
  if (args.has("json")) {
39916
40929
  printJson({ status: "not_found", workspace_id: workspaceId2, principal_id: principalId });
@@ -39919,6 +40932,18 @@ async function runListenStatusOrStop(args, command2) {
39919
40932
  }
39920
40933
  return;
39921
40934
  }
40935
+ if ((status.routeMode ?? "worker") !== "worker") {
40936
+ const recordedPending = status.pendingForMainCount ?? 0;
40937
+ const recordedDropped = status.droppedForMainCount ?? 0;
40938
+ const queueStats = await new FilePendingMainQueue(
40939
+ paths.instanceDirectory
40940
+ ).stats().catch(() => ({ count: recordedPending, droppedCount: recordedDropped }));
40941
+ status = {
40942
+ ...status,
40943
+ pendingForMainCount: queueStats.count,
40944
+ droppedForMainCount: queueStats.droppedCount
40945
+ };
40946
+ }
39922
40947
  if (args.has("json")) {
39923
40948
  printJson(listenerStatusJson(status));
39924
40949
  } else {
@@ -39938,6 +40963,148 @@ async function runListen(args) {
39938
40963
  }
39939
40964
  throw new UsageError("listen requires start, status, or stop");
39940
40965
  }
40966
+ var CLAUDE_HOOK_COMMAND = "cswarm hook check";
40967
+ function claudeUserPromptHookSnippet() {
40968
+ return {
40969
+ hooks: {
40970
+ UserPromptSubmit: [
40971
+ {
40972
+ hooks: [
40973
+ {
40974
+ type: "command",
40975
+ command: CLAUDE_HOOK_COMMAND
40976
+ }
40977
+ ]
40978
+ }
40979
+ ]
40980
+ }
40981
+ };
40982
+ }
40983
+ function projectClaudeSettingsPath() {
40984
+ return (0, import_node_path19.join)(process.cwd(), ".claude", "settings.json");
40985
+ }
40986
+ function readProjectSettings(path) {
40987
+ let raw;
40988
+ try {
40989
+ raw = (0, import_node_fs7.readFileSync)(path, "utf8");
40990
+ } catch (error) {
40991
+ if (error.code === "ENOENT") return {};
40992
+ throw error;
40993
+ }
40994
+ const value = JSON.parse(raw);
40995
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
40996
+ throw new Error("local .claude/settings.json must contain a JSON object");
40997
+ }
40998
+ return value;
40999
+ }
41000
+ function installClaudeHook(settings) {
41001
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? { ...settings.hooks } : {};
41002
+ const current = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
41003
+ const alreadyInstalled = current.some((group) => {
41004
+ if (!group || typeof group !== "object" || Array.isArray(group)) return false;
41005
+ const commands = group.hooks;
41006
+ return Array.isArray(commands) && commands.some(
41007
+ (hook) => hook && typeof hook === "object" && !Array.isArray(hook) && hook.type === "command" && hook.command === CLAUDE_HOOK_COMMAND
41008
+ );
41009
+ });
41010
+ if (!alreadyInstalled) {
41011
+ const snippetHooks = claudeUserPromptHookSnippet().hooks.UserPromptSubmit;
41012
+ current.push(snippetHooks[0]);
41013
+ }
41014
+ hooks.UserPromptSubmit = current;
41015
+ return { ...settings, hooks };
41016
+ }
41017
+ function uninstallClaudeHook(settings) {
41018
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks)) {
41019
+ return settings;
41020
+ }
41021
+ const hooks = { ...settings.hooks };
41022
+ if (!Array.isArray(hooks.UserPromptSubmit)) return settings;
41023
+ const groups = [];
41024
+ for (const group of hooks.UserPromptSubmit) {
41025
+ if (!group || typeof group !== "object" || Array.isArray(group)) {
41026
+ groups.push(group);
41027
+ continue;
41028
+ }
41029
+ const row = { ...group };
41030
+ if (!Array.isArray(row.hooks)) {
41031
+ groups.push(group);
41032
+ continue;
41033
+ }
41034
+ row.hooks = row.hooks.filter((hook) => !(hook && typeof hook === "object" && !Array.isArray(hook) && hook.type === "command" && hook.command === CLAUDE_HOOK_COMMAND));
41035
+ if (row.hooks.length > 0) groups.push(row);
41036
+ }
41037
+ if (groups.length > 0) hooks.UserPromptSubmit = groups;
41038
+ else delete hooks.UserPromptSubmit;
41039
+ if (Object.keys(hooks).length === 0) {
41040
+ const result = { ...settings };
41041
+ delete result.hooks;
41042
+ return result;
41043
+ }
41044
+ return { ...settings, hooks };
41045
+ }
41046
+ async function runHook(args) {
41047
+ const command2 = args.positionals[1];
41048
+ if (command2 === "check") {
41049
+ args.assertShape(["cooldown"], 2);
41050
+ const rawCooldown = args.optional("cooldown");
41051
+ const cooldownSeconds = rawCooldown === void 0 ? void 0 : Number(rawCooldown);
41052
+ if (cooldownSeconds !== void 0 && (!/^\d+$/.test(rawCooldown) || !Number.isSafeInteger(cooldownSeconds) || cooldownSeconds < 0 || cooldownSeconds > 86400)) {
41053
+ return;
41054
+ }
41055
+ const hardExit = setTimeout(() => {
41056
+ process.exit(0);
41057
+ }, 3e3);
41058
+ hardExit.unref();
41059
+ try {
41060
+ await runListenerHookCheck({
41061
+ ...cooldownSeconds === void 0 ? {} : { cooldownSeconds },
41062
+ write: async (output) => {
41063
+ await new Promise((resolve, reject) => {
41064
+ process.stdout.write(`${output}
41065
+ `, (error) => {
41066
+ if (error) reject(error);
41067
+ else resolve();
41068
+ });
41069
+ });
41070
+ }
41071
+ });
41072
+ return;
41073
+ } finally {
41074
+ clearTimeout(hardExit);
41075
+ }
41076
+ }
41077
+ if (command2 !== "install" && command2 !== "uninstall") {
41078
+ throw new UsageError("hook requires check, install, or uninstall");
41079
+ }
41080
+ args.assertShape(["write"], 3);
41081
+ if (args.positionals[2] !== "claude") {
41082
+ throw new Error("hook install/uninstall currently supports claude");
41083
+ }
41084
+ if (command2 === "uninstall" && !args.has("write")) {
41085
+ throw new Error("hook uninstall claude requires --write");
41086
+ }
41087
+ const snippet = claudeUserPromptHookSnippet();
41088
+ if (!args.has("write")) {
41089
+ process.stdout.write(`${JSON.stringify(snippet, null, 2)}
41090
+ `);
41091
+ return;
41092
+ }
41093
+ const path = projectClaudeSettingsPath();
41094
+ const settings = readProjectSettings(path);
41095
+ const updated = command2 === "install" ? installClaudeHook(settings) : uninstallClaudeHook(settings);
41096
+ (0, import_node_fs7.mkdirSync)((0, import_node_path19.dirname)(path), { recursive: true });
41097
+ (0, import_node_fs7.writeFileSync)(path, `${JSON.stringify(updated, null, 2)}
41098
+ `, {
41099
+ encoding: "utf8",
41100
+ mode: 384
41101
+ });
41102
+ process.stdout.write(
41103
+ command2 === "install" ? `Installed the Claude Code UserPromptSubmit hook in ${path}. It runs: ${CLAUDE_HOOK_COMMAND}
41104
+ ` : `Removed the CommonSwarm UserPromptSubmit hook from ${path}. Other settings were kept.
41105
+ `
41106
+ );
41107
+ }
39941
41108
  function formatFileSize(value) {
39942
41109
  const bytes = Number(value ?? 0);
39943
41110
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
@@ -39972,7 +41139,7 @@ async function fileRows(context) {
39972
41139
  );
39973
41140
  }
39974
41141
  async function resolveFileSelector(context, selector) {
39975
- if (UUID_RE15.test(selector)) return selector.toLowerCase();
41142
+ if (UUID_RE17.test(selector)) return selector.toLowerCase();
39976
41143
  const rows3 = await fileRows(context);
39977
41144
  const match = rows3.find(
39978
41145
  (row) => row.name.toLowerCase() === selector.toLowerCase()
@@ -39994,7 +41161,7 @@ async function runFilePut(args) {
39994
41161
  } catch {
39995
41162
  throw new Error(`could not read ${localPath}; check the path and permissions`);
39996
41163
  }
39997
- const name = args.optional("name") ?? (0, import_node_path17.basename)(localPath);
41164
+ const name = args.optional("name") ?? (0, import_node_path19.basename)(localPath);
39998
41165
  if (bytes.byteLength > FILE_MAX_VERSION_BYTES) {
39999
41166
  throw new Error(
40000
41167
  `this file is ${formatFileSize(bytes.byteLength)}; the per-file limit is ${formatFileSize(FILE_MAX_VERSION_BYTES)}, so the upload was not started`
@@ -40096,7 +41263,7 @@ async function runFileGet(args) {
40096
41263
  credential: context.selected.bearer
40097
41264
  };
40098
41265
  const grant = await fileDownloadUrl(send, { fileId, versionN });
40099
- const destination = args.optional("out") ?? (0, import_node_path17.basename)(grant.name);
41266
+ const destination = args.optional("out") ?? (0, import_node_path19.basename)(grant.name);
40100
41267
  const bytes = await getObject(context.cloud, grant.download_path);
40101
41268
  writeDestination(destination, bytes, args.has("force"), import_node_fs7.writeFileSync);
40102
41269
  if (args.has("json")) {
@@ -40312,10 +41479,10 @@ async function runSeed(args) {
40312
41479
  throw new Error("DATABASE_URL is required for the fixture bridge");
40313
41480
  }
40314
41481
  const tokenOut = process.env.SEED_TOKEN_OUT;
40315
- if (!tokenOut || !(0, import_node_path17.isAbsolute)(tokenOut)) {
41482
+ if (!tokenOut || !(0, import_node_path19.isAbsolute)(tokenOut)) {
40316
41483
  throw new Error("SEED_TOKEN_OUT must be an absolute path");
40317
41484
  }
40318
- const tokenFile = await (0, import_promises10.open)(tokenOut, "wx", 384).catch((error) => {
41485
+ const tokenFile = await (0, import_promises11.open)(tokenOut, "wx", 384).catch((error) => {
40319
41486
  if (error.code === "EEXIST") {
40320
41487
  throw new Error("SEED_TOKEN_OUT already exists; refusing to overwrite it");
40321
41488
  }
@@ -40354,7 +41521,7 @@ async function runSeed(args) {
40354
41521
  tokenWritten = true;
40355
41522
  }
40356
41523
  await tokenFile.close();
40357
- if (!tokenWritten) await (0, import_promises10.unlink)(tokenOut);
41524
+ if (!tokenWritten) await (0, import_promises11.unlink)(tokenOut);
40358
41525
  process.stdout.write(`${JSON.stringify({
40359
41526
  userId: result.userId,
40360
41527
  membershipRole: result.membershipRole,
@@ -40367,7 +41534,7 @@ async function runSeed(args) {
40367
41534
  `);
40368
41535
  } catch (error) {
40369
41536
  await tokenFile.close().catch(() => void 0);
40370
- if (!tokenWritten) await (0, import_promises10.unlink)(tokenOut).catch(() => void 0);
41537
+ if (!tokenWritten) await (0, import_promises11.unlink)(tokenOut).catch(() => void 0);
40371
41538
  throw error;
40372
41539
  }
40373
41540
  }
@@ -40392,6 +41559,10 @@ async function main() {
40392
41559
  await runListenSupervisor(args);
40393
41560
  return;
40394
41561
  }
41562
+ if (verb === "hook") {
41563
+ await runHook(args);
41564
+ return;
41565
+ }
40395
41566
  if (verb === "listen") {
40396
41567
  await runListen(args);
40397
41568
  return;
@@ -40543,6 +41714,10 @@ function safeParagraph(message) {
40543
41714
  return message.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").slice(0, 2e3);
40544
41715
  }
40545
41716
  main().catch((error) => {
41717
+ if (process.argv[2] === "hook" && process.argv[3] === "check") {
41718
+ process.exitCode = 0;
41719
+ return;
41720
+ }
40546
41721
  if (error instanceof RenewalReauthorisationRequired || error instanceof RenewalRevoked) {
40547
41722
  process.stderr.write(`${safeParagraph(error.message)}
40548
41723
  `);
@@ -40591,11 +41766,14 @@ ${usage()}
40591
41766
  EXIT_RESTARTABLE,
40592
41767
  TURN_BUDGET_CREDENTIAL_MARGIN_MS,
40593
41768
  clampTurnBudgetToCredential,
41769
+ claudeUserPromptHookSnippet,
40594
41770
  describeAudience,
40595
41771
  listenerFailureMessage,
40596
41772
  listenerHostLimits,
40597
41773
  listenerPermissionMode,
41774
+ listenerRouteConfiguration,
40598
41775
  listenerStatusJson,
41776
+ renderListenerStatus,
40599
41777
  renderRoster,
40600
41778
  replyRefusalHint,
40601
41779
  resolveDetachedClaudeExecutable,