commonswarm 0.1.44 → 0.1.45

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 +1055 -93
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -13503,6 +13503,7 @@ var require_main3 = __commonJS({
13503
13503
  var cli_exports = {};
13504
13504
  __export(cli_exports, {
13505
13505
  EXIT_RESTARTABLE: () => EXIT_RESTARTABLE,
13506
+ ListenerUnattendedRefusedError: () => ListenerUnattendedRefusedError,
13506
13507
  TURN_BUDGET_CREDENTIAL_MARGIN_MS: () => TURN_BUDGET_CREDENTIAL_MARGIN_MS,
13507
13508
  clampTurnBudgetToCredential: () => clampTurnBudgetToCredential,
13508
13509
  claudeUserPromptHookSnippet: () => claudeUserPromptHookSnippet,
@@ -13521,12 +13522,12 @@ __export(cli_exports, {
13521
13522
  });
13522
13523
  module.exports = __toCommonJS(cli_exports);
13523
13524
  var import_node_crypto20 = require("node:crypto");
13524
- var import_node_child_process8 = require("node:child_process");
13525
+ var import_node_child_process9 = require("node:child_process");
13525
13526
  var import_node_fs7 = require("node:fs");
13526
- var import_promises11 = require("node:fs/promises");
13527
+ var import_promises12 = require("node:fs/promises");
13527
13528
  var import_node_os10 = require("node:os");
13528
13529
  var import_node_path21 = require("node:path");
13529
- var import_promises12 = require("node:readline/promises");
13530
+ var import_promises13 = require("node:readline/promises");
13530
13531
 
13531
13532
  // src/cloud/auth.ts
13532
13533
  var import_node_crypto2 = require("node:crypto");
@@ -22900,9 +22901,9 @@ var FileCommandRefused = class extends Error {
22900
22901
  };
22901
22902
  var FileTransportError = class extends Error {
22902
22903
  /**
22903
- * True when no HTTP response arrived (connection failure, timeout), so the
22904
- * outcome is UNKNOWN and one same-id retry is safe under the server's
22905
- * command-id replay. A received refusal is a known outcome: never retried.
22904
+ * True when the request did not complete: no response arrived, or an
22905
+ * idempotent read's body stalled. Reads may retry; writes reuse the same ids
22906
+ * because their outcome is unknown. A received refusal is never retried.
22906
22907
  */
22907
22908
  constructor(message, noResponse = false) {
22908
22909
  super(message);
@@ -23050,7 +23051,7 @@ async function getObject(target2, downloadPath, fetcher = fetch, options = {}) {
23050
23051
  options
23051
23052
  ));
23052
23053
  } catch {
23053
- throw new FileTransportError("the download failed before a response", true);
23054
+ throw new FileTransportError("the download did not complete", true);
23054
23055
  }
23055
23056
  if (!response.ok || body === null) {
23056
23057
  throw new FileTransportError(
@@ -23131,7 +23132,7 @@ async function listFilesAsAgent(target2, credential, workspaceId2, fetcher = fet
23131
23132
  options
23132
23133
  ));
23133
23134
  } catch {
23134
- throw new FileTransportError("file list could not reach the cloud service", true);
23135
+ throw new FileTransportError("the file list did not complete", true);
23135
23136
  }
23136
23137
  if (!response.ok) {
23137
23138
  throw new FileCommandRefused(
@@ -23176,7 +23177,7 @@ async function listFilesAsHuman(target2, accessToken, workspaceId2, fetcher = fe
23176
23177
  options
23177
23178
  ));
23178
23179
  } catch {
23179
- throw new FileTransportError("file list could not reach the cloud service", true);
23180
+ throw new FileTransportError("the file list did not complete", true);
23180
23181
  }
23181
23182
  if (!response.ok) {
23182
23183
  throw new FileCommandRefused(
@@ -23366,8 +23367,7 @@ function assertOwnedByCurrentUser(uid2) {
23366
23367
  throw new Error("credential path is not owned by the current user");
23367
23368
  }
23368
23369
  }
23369
- async function secureDirectory(path) {
23370
- let created = false;
23370
+ async function existingSecureDirectory(path) {
23371
23371
  try {
23372
23372
  const info = await (0, import_promises.lstat)(path);
23373
23373
  if (!info.isDirectory() || info.isSymbolicLink()) {
@@ -23380,20 +23380,22 @@ async function secureDirectory(path) {
23380
23380
  );
23381
23381
  }
23382
23382
  } catch (error) {
23383
- if (error.code !== "ENOENT") throw error;
23384
- await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
23385
- await (0, import_promises.chmod)(path, 448);
23386
- created = true;
23383
+ if (error.code === "ENOENT") return false;
23384
+ throw error;
23387
23385
  }
23388
- if (created) {
23389
- const info = await (0, import_promises.lstat)(path);
23390
- if (!info.isDirectory() || info.isSymbolicLink()) {
23391
- throw new Error(`credential directory is not a real directory: ${path}`);
23392
- }
23393
- assertOwnedByCurrentUser(info.uid);
23394
- if (mode(info.mode) !== 448) {
23395
- throw new Error(`credential directory could not be secured to mode 0700: ${path}`);
23396
- }
23386
+ return true;
23387
+ }
23388
+ async function secureDirectory(path) {
23389
+ if (await existingSecureDirectory(path)) return;
23390
+ await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
23391
+ await (0, import_promises.chmod)(path, 448);
23392
+ const info = await (0, import_promises.lstat)(path);
23393
+ if (!info.isDirectory() || info.isSymbolicLink()) {
23394
+ throw new Error(`credential directory is not a real directory: ${path}`);
23395
+ }
23396
+ assertOwnedByCurrentUser(info.uid);
23397
+ if (mode(info.mode) !== 448) {
23398
+ throw new Error(`credential directory could not be secured to mode 0700: ${path}`);
23397
23399
  }
23398
23400
  }
23399
23401
  async function ensureSecureStateDirectory(path) {
@@ -23555,6 +23557,20 @@ async function readSecureJsonFile(path, maxBytes) {
23555
23557
  throw error;
23556
23558
  }
23557
23559
  }
23560
+ async function readSecureJsonFileIfPresent(path, maxBytes) {
23561
+ if (!await existingSecureDirectory((0, import_node_path.dirname)(path))) return null;
23562
+ try {
23563
+ await secureCredentialFile(path);
23564
+ const raw = await (0, import_promises.readFile)(path, "utf8");
23565
+ if (Buffer.byteLength(raw, "utf8") > maxBytes) {
23566
+ throw new Error("stored record is larger than this store accepts");
23567
+ }
23568
+ return raw;
23569
+ } catch (error) {
23570
+ if (error.code === "ENOENT") return null;
23571
+ throw error;
23572
+ }
23573
+ }
23558
23574
  async function deleteSecureJsonFile(path) {
23559
23575
  await secureDirectory((0, import_node_path.dirname)(path));
23560
23576
  try {
@@ -25744,7 +25760,7 @@ var src_default = Postgres;
25744
25760
  function Postgres(a, b2) {
25745
25761
  const options = parseOptions(a, b2), subscribe = options.no_subscribe || Subscribe(Postgres, { ...options });
25746
25762
  let ending = false;
25747
- const queries = queue_default(), connecting = queue_default(), reserved = queue_default(), closed = queue_default(), ended = queue_default(), open5 = queue_default(), busy = queue_default(), full = queue_default(), queues = { connecting, reserved, closed, ended, open: open5, busy, full };
25763
+ const queries = queue_default(), connecting = queue_default(), reserved = queue_default(), closed = queue_default(), ended = queue_default(), open6 = queue_default(), busy = queue_default(), full = queue_default(), queues = { connecting, reserved, closed, ended, open: open6, busy, full };
25748
25764
  const connections = [...Array(options.max)].map(() => connection_default(options, queues, { onopen, onend, onclose }));
25749
25765
  const sql = Sql(handler);
25750
25766
  Object.assign(sql, {
@@ -25857,7 +25873,7 @@ function Postgres(a, b2) {
25857
25873
  }
25858
25874
  async function reserve() {
25859
25875
  const queue = queue_default();
25860
- const c = open5.length ? open5.shift() : await new Promise((resolve2, reject) => {
25876
+ const c = open6.length ? open6.shift() : await new Promise((resolve2, reject) => {
25861
25877
  const query = { reserve: resolve2, reject };
25862
25878
  queries.push(query);
25863
25879
  closed.length && connect(closed.shift(), query);
@@ -25930,7 +25946,7 @@ function Postgres(a, b2) {
25930
25946
  c.queue.remove(c);
25931
25947
  queue.push(c);
25932
25948
  c.queue = queue;
25933
- queue === open5 ? c.idleTimer.start() : c.idleTimer.cancel();
25949
+ queue === open6 ? c.idleTimer.start() : c.idleTimer.cancel();
25934
25950
  return c;
25935
25951
  }
25936
25952
  function json(x) {
@@ -25944,8 +25960,8 @@ function Postgres(a, b2) {
25944
25960
  function handler(query) {
25945
25961
  if (ending)
25946
25962
  return query.reject(Errors.connection("CONNECTION_ENDED", options, options));
25947
- if (open5.length)
25948
- return go(open5.shift(), query);
25963
+ if (open6.length)
25964
+ return go(open6.shift(), query);
25949
25965
  if (closed.length)
25950
25966
  return connect(closed.shift(), query);
25951
25967
  busy.length ? go(busy.shift(), query) : queries.push(query);
@@ -25990,7 +26006,7 @@ function Postgres(a, b2) {
25990
26006
  }
25991
26007
  function onopen(c) {
25992
26008
  if (queries.length === 0)
25993
- return move(c, open5);
26009
+ return move(c, open6);
25994
26010
  let max = Math.ceil(queries.length / (connecting.length + 1)), ready = true;
25995
26011
  while (ready && queries.length && max-- > 0) {
25996
26012
  const query = queries.shift();
@@ -29948,6 +29964,16 @@ var CURSOR_MAX_BYTES = 4 * 1024;
29948
29964
  var ARRIVAL_SNIPPET_MAX = 180;
29949
29965
  var ARRIVAL_WATCH_POLL_MS = 25e3;
29950
29966
  var ARRIVAL_RETRY_NOTICE_THRESHOLD_MS = 6e4;
29967
+ var EXIT_NOTIFY_ORPHANED = 74;
29968
+ var NotifyStdoutClosedError = class extends Error {
29969
+ name = "NotifyStdoutClosedError";
29970
+ code = "notify_stdout_closed";
29971
+ constructor() {
29972
+ super(
29973
+ "[notify_stdout_closed] inbox --notify lost its stdout reader and stopped before advancing its cursor. Start one fresh watcher under a live Monitor."
29974
+ );
29975
+ }
29976
+ };
29951
29977
  function createArrivalRetryNoticePolicy(thresholdMs = ARRIVAL_RETRY_NOTICE_THRESHOLD_MS) {
29952
29978
  let firstFailureAt = null;
29953
29979
  let failureEmitted = false;
@@ -30065,6 +30091,33 @@ function formatArrivalNotification(notification) {
30065
30091
  const attachmentCopy = notification.attachment_count === 0 ? "" : ` \u2014 ${notification.attachment_count} attachment${notification.attachment_count === 1 ? "" : "s"}`;
30066
30092
  return `CommonSwarm from ${notification.sender_kind} ${notification.sender}: ${notification.snippet}${attachmentCopy} \u2014 reply: ${notification.reply_command}`;
30067
30093
  }
30094
+ function notifyWriteError(error) {
30095
+ return error.code === "EPIPE" ? new NotifyStdoutClosedError() : error;
30096
+ }
30097
+ async function writeArrivalMonitorLine(line, stream2 = process.stdout) {
30098
+ await new Promise((resolve2, reject) => {
30099
+ let settled = false;
30100
+ const finish = (error) => {
30101
+ if (settled) return;
30102
+ settled = true;
30103
+ if (error) {
30104
+ setImmediate(() => stream2.off("error", onError));
30105
+ reject(notifyWriteError(error));
30106
+ } else {
30107
+ stream2.off("error", onError);
30108
+ resolve2();
30109
+ }
30110
+ };
30111
+ const onError = (error) => finish(error);
30112
+ stream2.once("error", onError);
30113
+ try {
30114
+ stream2.write(`${line}
30115
+ `, (error) => finish(error));
30116
+ } catch (error) {
30117
+ finish(error instanceof Error ? error : new Error(String(error)));
30118
+ }
30119
+ });
30120
+ }
30068
30121
  function cursorOf(signal) {
30069
30122
  return { created_at: signal.created_at, id: signal.id };
30070
30123
  }
@@ -30275,7 +30328,11 @@ function parseDeliveryReceipt(value) {
30275
30328
  "protocol",
30276
30329
  "delivery receipt returned a malformed last_error_code"
30277
30330
  );
30278
- })()
30331
+ })(),
30332
+ pending_for_main_count: Object.hasOwn(row, "pending_for_main_count") ? row.pending_for_main_count === null ? null : nonNegativeInteger(
30333
+ row.pending_for_main_count,
30334
+ "pending_for_main_count"
30335
+ ) : null
30279
30336
  };
30280
30337
  }
30281
30338
  function parseUntrackedAgent(value) {
@@ -30629,9 +30686,10 @@ function renderSignalReceiptReport(report, nowMs = Date.now()) {
30629
30686
  ].join("\n");
30630
30687
  }
30631
30688
  if (state === "queued") {
30689
+ const queueCount = receipt.pending_for_main_count;
30632
30690
  return [
30633
- `Queued for agent ${receipt.recipient_agent_principal_id}'s interactive session ${relativeAge(receipt.acked_at, nowMs)}. The agent has not seen it yet.`,
30634
- "It will appear at the agent's next prompt.",
30691
+ `Queued for agent ${receipt.recipient_agent_principal_id}'s interactive session ${relativeAge(receipt.acked_at, nowMs)}; waiting for the recipient's session hook${typeof queueCount === "number" ? ` (${queueCount} in queue)` : ""}.`,
30692
+ `Ask the agent's operator to check its listener with: ${listenerStatusCommand(report, receipt)}`,
30635
30693
  `Check again with: ${receiptCheckCommand(report)}`
30636
30694
  ].join("\n");
30637
30695
  }
@@ -30686,7 +30744,8 @@ function signalReceiptJsonPayload(report, nowMs = Date.now()) {
30686
30744
  acked_at: receipt.acked_at,
30687
30745
  attempt_count: receipt.attempt_count,
30688
30746
  lease_expiry_count: receipt.lease_expiry_count,
30689
- last_error_code: receipt.last_error_code
30747
+ last_error_code: receipt.last_error_code,
30748
+ pending_for_main_count: receipt.pending_for_main_count ?? null
30690
30749
  }
30691
30750
  )
30692
30751
  };
@@ -30799,7 +30858,7 @@ var CLAUDE_ACP_MIN_VERSION = "0.64.2";
30799
30858
  var CLAUDE_ACP_LAST_MEASURED_VERSION = "0.64.2";
30800
30859
  var CLAUDE_PERMISSION_MODE_ID = "default";
30801
30860
  var CODEX_ACP_MIN_VERSION = "1.1.9";
30802
- var CODEX_ACP_LAST_MEASURED_VERSION = "1.1.9";
30861
+ var CODEX_ACP_LAST_MEASURED_VERSION = "1.8.0";
30803
30862
  var CODEX_PERMISSION_MODE_ID = "read-only";
30804
30863
  var ACP_PROTOCOL_VERSION = 1;
30805
30864
  var OPENCODE_FORCED_PERMISSION_TOOLS = [
@@ -31041,10 +31100,12 @@ var AcpVersionBelowFloorError = class extends AcpVersionError {
31041
31100
  actual;
31042
31101
  };
31043
31102
  var AcpPermissionCanaryError = class extends AcpHostError {
31044
- constructor(message) {
31103
+ constructor(message, reasonCode = null) {
31045
31104
  super("permission_canary_failed", message);
31105
+ this.reasonCode = reasonCode;
31046
31106
  this.name = "AcpPermissionCanaryError";
31047
31107
  }
31108
+ reasonCode;
31048
31109
  };
31049
31110
  var AcpPromptsBlockedError = class extends AcpHostError {
31050
31111
  constructor() {
@@ -31513,7 +31574,8 @@ var AcpHostSession = class _AcpHostSession {
31513
31574
  }
31514
31575
  const detail = last?.reason ?? "permission-boundary canary failed: need host reject + correlated terminal tool status";
31515
31576
  throw new AcpPermissionCanaryError(
31516
- total === 1 ? detail : `${detail} (failed ${total} attempts)`
31577
+ total === 1 ? detail : `${detail} (failed ${total} attempts)`,
31578
+ last?.reasonCode ?? null
31517
31579
  );
31518
31580
  }
31519
31581
  /** Test/helper: force-enable prompts without canary (never used by production open path). */
@@ -31558,7 +31620,8 @@ var AcpHostSession = class _AcpHostSession {
31558
31620
  passed: false,
31559
31621
  sawPermissionRequest: this.canaryState.sawPermissionRequest,
31560
31622
  sawDeniedToolResult: this.canaryState.sawDeniedToolResult,
31561
- reason: err instanceof Error ? err.message : String(err)
31623
+ reason: err instanceof Error ? err.message : String(err),
31624
+ ...err instanceof AcpHostError ? { reasonCode: err.code } : {}
31562
31625
  };
31563
31626
  }
31564
31627
  }
@@ -33098,6 +33161,17 @@ async function assertCodexVersionFloor(executable, options) {
33098
33161
  });
33099
33162
  const version3 = parseCodexVersionOutput(stdout);
33100
33163
  if (!version3) {
33164
+ const codexCliVersion = parseProviderVersionOutput(
33165
+ stdout,
33166
+ /\bcodex-cli\b/i,
33167
+ false
33168
+ );
33169
+ if (codexCliVersion) {
33170
+ throw new AcpVersionError(
33171
+ "this is the Codex CLI; --codex-executable takes the codex-acp bridge (npm i -g @agentclientprotocol/codex-acp)",
33172
+ "executable_not_bridge"
33173
+ );
33174
+ }
33101
33175
  throw new AcpVersionParseError(
33102
33176
  `could not parse codex-acp version from: ${stdout.trim().slice(0, 200)}`
33103
33177
  );
@@ -33163,10 +33237,13 @@ async function openCodexAcpSession(options) {
33163
33237
  typeof pathEnv === "string" ? pathEnv : void 0
33164
33238
  );
33165
33239
  const env = buildCodexChildEnv(parentEnv);
33240
+ let providerVersion;
33166
33241
  if (!options.skipVersionCheck) {
33167
- await assertCodexVersionFloor(executable, {
33168
- env,
33169
- ...options.onVersionNotice ? { onNewerVersion: options.onVersionNotice } : {}
33242
+ providerVersion = await assertCodexVersionFloor(executable, { env });
33243
+ options.onVersionNotice?.({
33244
+ provider: "codex-acp",
33245
+ runningVersion: providerVersion,
33246
+ lastMeasuredVersion: CODEX_ACP_LAST_MEASURED_VERSION
33170
33247
  });
33171
33248
  }
33172
33249
  if (options.signal?.aborted) {
@@ -33258,7 +33335,15 @@ async function openCodexAcpSession(options) {
33258
33335
  })();
33259
33336
  return closePromise;
33260
33337
  };
33261
- return { session, child, executable, args, env, close };
33338
+ return {
33339
+ session,
33340
+ child,
33341
+ executable,
33342
+ args,
33343
+ env,
33344
+ ...providerVersion ? { providerVersion } : {},
33345
+ close
33346
+ };
33262
33347
  } catch (error) {
33263
33348
  removeAbortListener();
33264
33349
  transport.close();
@@ -35026,6 +35111,10 @@ var CodexListenerClosedDuringOpen = class extends Error {
35026
35111
  this.name = "CodexListenerClosedDuringOpen";
35027
35112
  }
35028
35113
  };
35114
+ function pathIsInsideOrEqual(ancestor, candidate) {
35115
+ const fromAncestor = (0, import_node_path14.relative)(ancestor, candidate);
35116
+ return fromAncestor === "" || fromAncestor !== ".." && !fromAncestor.startsWith(`..${import_node_path14.sep}`) && !(0, import_node_path14.isAbsolute)(fromAncestor);
35117
+ }
35029
35118
  var CodexListenerModel = class {
35030
35119
  constructor(options) {
35031
35120
  this.options = options;
@@ -35157,10 +35246,25 @@ var CodexListenerModel = class {
35157
35246
  }
35158
35247
  /** Force Codex's measured shell permission path without changing worker cwd. */
35159
35248
  async enablePromptsAfterCodexCanary(handle) {
35249
+ const configuredHome = this.options.env?.HOME;
35250
+ const home = configuredHome && (0, import_node_path14.isAbsolute)(configuredHome) ? configuredHome : (0, import_node_os9.homedir)();
35251
+ const sentinelDirectory = (0, import_node_path14.join)(home, ".cswarm", "canary");
35252
+ await (0, import_promises8.mkdir)(sentinelDirectory, { recursive: true, mode: 448 });
35253
+ await (0, import_promises8.chmod)(sentinelDirectory, 448);
35254
+ const [workerCwd, canaryDirectory] = await Promise.all([
35255
+ (0, import_promises8.realpath)(this.options.cwd),
35256
+ (0, import_promises8.realpath)(sentinelDirectory)
35257
+ ]);
35258
+ if (pathIsInsideOrEqual(workerCwd, canaryDirectory)) {
35259
+ throw new AcpPermissionCanaryError(
35260
+ `canary_path_inside_cwd: ${canaryDirectory} is inside listener cwd ${workerCwd}. Next: pass a --cwd that is not your home directory`
35261
+ );
35262
+ }
35160
35263
  const sentinelPath = (0, import_node_path14.join)(
35161
- (0, import_node_os9.tmpdir)(),
35264
+ canaryDirectory,
35162
35265
  `cswarm-codex-permission-canary-${process.pid}-${(0, import_node_crypto17.randomUUID)()}`
35163
35266
  );
35267
+ let canaryError;
35164
35268
  let sentinelCreated = false;
35165
35269
  try {
35166
35270
  await handle.session.enablePromptsAfterCanary({
@@ -35168,6 +35272,8 @@ var CodexListenerModel = class {
35168
35272
  probeText: `Use a shell command to create ${sentinelPath} with content CSWARM_CANARY_NOOP. You must use the shell. Do nothing else.`,
35169
35273
  ...this.options.onCanaryAttempt ? { onAttempt: this.options.onCanaryAttempt } : {}
35170
35274
  });
35275
+ } catch (error) {
35276
+ canaryError = error;
35171
35277
  } finally {
35172
35278
  try {
35173
35279
  await (0, import_promises8.lstat)(sentinelPath);
@@ -35177,11 +35283,39 @@ var CodexListenerModel = class {
35177
35283
  if (error.code !== "ENOENT") throw error;
35178
35284
  }
35179
35285
  }
35286
+ const observation = handle.session.canaryObservation;
35287
+ const sawPermissionRequest = observation?.sawPermissionRequest === true;
35288
+ const bridgeVersion = handle.providerVersion ?? handle.session.info?.agentVersion ?? "unknown";
35289
+ if (sentinelCreated && !sawPermissionRequest) {
35290
+ throw new AcpPermissionCanaryError(
35291
+ `canary_executed_without_permission: codex-acp ${bridgeVersion} ran the shell probe without a permission request and wrote ${sentinelPath}. Next: replace codex-acp ${bridgeVersion} with a bridge version that asks before writing ${sentinelPath}`
35292
+ );
35293
+ }
35180
35294
  if (sentinelCreated) {
35181
35295
  throw new AcpPermissionCanaryError(
35182
- "Codex bridge wrote the permission canary sentinel before denial"
35296
+ `canary_write_not_blocked: codex-acp ${bridgeVersion} wrote ${sentinelPath} even though the host rejected the canary. Next: update or reinstall the codex-acp bridge`
35183
35297
  );
35184
35298
  }
35299
+ if (canaryError instanceof AcpPermissionCanaryError) {
35300
+ if (canaryError.reasonCode === "timeout") {
35301
+ throw new AcpPermissionCanaryError(
35302
+ `canary_timeout: ${canaryError.message}. Next: retry to run a fresh bounded permission canary`,
35303
+ "timeout"
35304
+ );
35305
+ }
35306
+ if (canaryError.reasonCode !== null) {
35307
+ throw new AcpPermissionCanaryError(
35308
+ `canary_bridge_error: codex-acp ${bridgeVersion} returned ${canaryError.message}. Next: resolve the quoted bridge error, then retry`,
35309
+ canaryError.reasonCode
35310
+ );
35311
+ }
35312
+ if (!sawPermissionRequest) {
35313
+ throw new AcpPermissionCanaryError(
35314
+ `canary_no_tool_call: codex-acp ${bridgeVersion} did not request permission or create ${sentinelPath}. Next: retry to re-sample the model's shell choice`
35315
+ );
35316
+ }
35317
+ }
35318
+ if (canaryError !== void 0) throw canaryError;
35185
35319
  }
35186
35320
  };
35187
35321
 
@@ -37138,6 +37272,7 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
37138
37272
  "lastErrorDetail",
37139
37273
  "providerVersion",
37140
37274
  "providerLastMeasuredVersion",
37275
+ "cswarmVersion",
37141
37276
  "lastWorkerStderrTail",
37142
37277
  "logPath",
37143
37278
  "deliveryMode",
@@ -37197,7 +37332,7 @@ function parseStatus(raw) {
37197
37332
  const nullableUuid3 = (candidate) => candidate === null || typeof candidate === "string" && UUID_RE17.test(candidate);
37198
37333
  const nullableCount = (candidate) => candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
37199
37334
  const nullableTimestamp3 = (candidate) => candidate === null || typeof candidate === "string" && Number.isFinite(Date.parse(candidate));
37200
- if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE17.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE17.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE17.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))) || !nullableUuid3(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || !(row.lastErrorDetail === void 0 || row.lastErrorDetail === null || typeof row.lastErrorDetail === "string" && row.lastErrorDetail.length > 0 && row.lastErrorDetail.length <= 2048 && !/swm_(?:agt|inv|cap)_/i.test(row.lastErrorDetail)) || !(row.providerVersion === void 0 || row.providerVersion === null || typeof row.providerVersion === "string" && SEMVER_RE2.test(row.providerVersion)) || !(row.providerLastMeasuredVersion === void 0 || row.providerLastMeasuredVersion === null || typeof row.providerLastMeasuredVersion === "string" && SEMVER_RE2.test(row.providerLastMeasuredVersion)) || (row.providerVersion === null || row.providerVersion === void 0) !== (row.providerLastMeasuredVersion === null || row.providerLastMeasuredVersion === void 0) || !(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_path16.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 || nullableTimestamp3(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp3(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp3(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)) {
37335
+ if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE17.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE17.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE17.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))) || !nullableUuid3(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || !(row.lastErrorDetail === void 0 || row.lastErrorDetail === null || typeof row.lastErrorDetail === "string" && row.lastErrorDetail.length > 0 && row.lastErrorDetail.length <= 2048 && !/swm_(?:agt|inv|cap)_/i.test(row.lastErrorDetail)) || !(row.providerVersion === void 0 || row.providerVersion === null || typeof row.providerVersion === "string" && SEMVER_RE2.test(row.providerVersion)) || !(row.providerLastMeasuredVersion === void 0 || row.providerLastMeasuredVersion === null || typeof row.providerLastMeasuredVersion === "string" && SEMVER_RE2.test(row.providerLastMeasuredVersion)) || !(row.cswarmVersion === void 0 || row.cswarmVersion === null || typeof row.cswarmVersion === "string" && SEMVER_RE2.test(row.cswarmVersion)) || (row.providerVersion === null || row.providerVersion === void 0) !== (row.providerLastMeasuredVersion === null || row.providerLastMeasuredVersion === void 0) || !(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_path16.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 || nullableTimestamp3(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp3(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp3(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)) {
37201
37336
  throw new Error("stored listener status is malformed");
37202
37337
  }
37203
37338
  const routeMode = row.routeMode ?? "worker";
@@ -37217,6 +37352,7 @@ function parseStatus(raw) {
37217
37352
  lastWorkerStderrTail: row.lastWorkerStderrTail ?? null,
37218
37353
  providerVersion: row.providerVersion ?? null,
37219
37354
  providerLastMeasuredVersion: row.providerLastMeasuredVersion ?? null,
37355
+ ...row.cswarmVersion === void 0 ? {} : { cswarmVersion: row.cswarmVersion },
37220
37356
  routeMode,
37221
37357
  deferOverChars,
37222
37358
  pendingForMainCount: row.pendingForMainCount ?? 0,
@@ -37241,6 +37377,10 @@ async function readListenerStatus(paths) {
37241
37377
  const raw = await readSecureJsonFile(paths.statusPath, MAX_STATUS_BYTES);
37242
37378
  return raw === null ? null : parseStatus(raw);
37243
37379
  }
37380
+ async function readListenerStatusIfPresent(paths) {
37381
+ const raw = await readSecureJsonFileIfPresent(paths.statusPath, MAX_STATUS_BYTES);
37382
+ return raw === null ? null : parseStatus(raw);
37383
+ }
37244
37384
  async function appendListenerEvent(paths, event) {
37245
37385
  const allowed = /* @__PURE__ */ new Set([
37246
37386
  "ts",
@@ -37646,6 +37786,7 @@ async function runListenerSupervisor(options) {
37646
37786
  version: 1,
37647
37787
  instanceId: proposedInstanceId,
37648
37788
  provider: options.provider ?? "grok",
37789
+ ...options.cswarmVersion ? { cswarmVersion: options.cswarmVersion } : {},
37649
37790
  ...options.permissionMode ? { permissionMode: options.permissionMode } : {},
37650
37791
  profileId: options.profileId,
37651
37792
  workspaceId: options.workspaceId.toLowerCase(),
@@ -38950,6 +39091,9 @@ function newestFirst(topics) {
38950
39091
  (left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt) || left.topic.localeCompare(right.topic)
38951
39092
  );
38952
39093
  }
39094
+ function changedTopics(previous, topics) {
39095
+ return previous === null ? newestFirst(topics).slice(0, BRAIN_DIGEST_TOPIC_LIMIT) : topics.filter((topic) => previous.topicVersions[topic.topic] !== topic.version);
39096
+ }
38953
39097
  function renderBrainDigest(topicCount, topics) {
38954
39098
  if (topics.length === 0) return null;
38955
39099
  const names = newestFirst(topics).map((topic) => `${topic.topic} v${topic.version}`).join(", ");
@@ -38967,6 +39111,19 @@ var FileBrainDigestStore = class {
38967
39111
  instanceDirectory;
38968
39112
  location;
38969
39113
  principalId;
39114
+ /** Read the shared high-water without advancing it. */
39115
+ async preview(topics) {
39116
+ const raw = await readSecureJsonFileIfPresent(
39117
+ this.location,
39118
+ MAX_BRAIN_DIGEST_STATE_BYTES
39119
+ );
39120
+ const previous = raw === null ? null : parseState(raw);
39121
+ if (previous !== null && previous.principalId !== this.principalId) {
39122
+ throw new Error("stored brain digest state belongs to another principal");
39123
+ }
39124
+ currentState(this.principalId, topics);
39125
+ return renderBrainDigest(topics.length, changedTopics(previous, topics));
39126
+ }
38970
39127
  async consume(topics) {
38971
39128
  return await withFileLock(this.instanceDirectory, BRAIN_DIGEST_LOCK, async () => {
38972
39129
  const raw = await readSecureJsonFile(
@@ -38978,9 +39135,7 @@ var FileBrainDigestStore = class {
38978
39135
  throw new Error("stored brain digest state belongs to another principal");
38979
39136
  }
38980
39137
  const next = currentState(this.principalId, topics);
38981
- const changed = previous === null ? newestFirst(topics).slice(0, BRAIN_DIGEST_TOPIC_LIMIT) : topics.filter(
38982
- (topic) => previous.topicVersions[topic.topic] !== topic.version
38983
- );
39138
+ const changed = changedTopics(previous, topics);
38984
39139
  const serialized = JSON.stringify(next);
38985
39140
  if (Buffer.byteLength(serialized, "utf8") > MAX_BRAIN_DIGEST_STATE_BYTES) {
38986
39141
  throw new Error("brain digest state is larger than this store accepts");
@@ -39120,6 +39275,31 @@ var FileHookSurfaceStore = class {
39120
39275
  }
39121
39276
  instanceDirectory;
39122
39277
  path;
39278
+ /** Preview unseen hook rows without taking a write lock or advancing state. */
39279
+ async previewUnseen(items) {
39280
+ const raw = await readSecureJsonFileIfPresent(this.path, MAX_HOOK_SURFACE_BYTES);
39281
+ const seen = new Set(raw === null ? [] : parseSurface(raw).surfacedSignalIds);
39282
+ const unseen = [];
39283
+ for (const item of items) {
39284
+ const signalId = item.signalId.toLowerCase();
39285
+ if (!UUID_RE21.test(signalId) || seen.has(signalId)) continue;
39286
+ seen.add(signalId);
39287
+ unseen.push(item);
39288
+ }
39289
+ return unseen;
39290
+ }
39291
+ /** Read attendance evidence without advancing the hook high-water. */
39292
+ async evidence() {
39293
+ return await withFileLock(this.instanceDirectory, HOOK_SURFACE_LOCK, async () => {
39294
+ const raw = await readSecureJsonFile(this.path, MAX_HOOK_SURFACE_BYTES);
39295
+ if (raw === null) return { exists: false, surfacedSignalIds: [] };
39296
+ const state = parseSurface(raw);
39297
+ return {
39298
+ exists: true,
39299
+ surfacedSignalIds: state.surfacedSignalIds
39300
+ };
39301
+ }, { timeoutMs: HOOK_LOCK_TIMEOUT_MS });
39302
+ }
39123
39303
  async stage(items, droppedCount) {
39124
39304
  return await withFileLock(this.instanceDirectory, HOOK_SURFACE_LOCK, async () => {
39125
39305
  const raw = await readSecureJsonFile(this.path, MAX_HOOK_SURFACE_BYTES);
@@ -39643,6 +39823,486 @@ async function runListenerHookCheck(options = {}) {
39643
39823
  }
39644
39824
  }
39645
39825
 
39826
+ // src/listener/attendance-canary.ts
39827
+ var import_promises11 = require("node:fs/promises");
39828
+ var LOG_TAIL_BYTES = 256 * 1024;
39829
+ function agentReceipt(receipts, principalId) {
39830
+ for (const receipt of receipts) {
39831
+ if ("recipient_agent_principal_id" in receipt && receipt.recipient_agent_principal_id === principalId) {
39832
+ return receipt;
39833
+ }
39834
+ }
39835
+ return null;
39836
+ }
39837
+ async function readLogTail(path) {
39838
+ let handle;
39839
+ try {
39840
+ handle = await (0, import_promises11.open)(path, "r");
39841
+ } catch (error) {
39842
+ if (error.code === "ENOENT") return "";
39843
+ throw error;
39844
+ }
39845
+ try {
39846
+ const size2 = (await handle.stat()).size;
39847
+ const start = Math.max(0, size2 - LOG_TAIL_BYTES);
39848
+ const buffer2 = Buffer.alloc(size2 - start);
39849
+ await handle.read(buffer2, 0, buffer2.length, start);
39850
+ let text = buffer2.toString("utf8");
39851
+ if (start > 0) {
39852
+ const newline = text.indexOf("\n");
39853
+ text = newline < 0 ? "" : text.slice(newline + 1);
39854
+ }
39855
+ return text;
39856
+ } finally {
39857
+ await handle.close();
39858
+ }
39859
+ }
39860
+ async function logEvidence(path, signalId) {
39861
+ let claimedAt = null;
39862
+ let routeDecision = null;
39863
+ let routedAt = null;
39864
+ for (const line of (await readLogTail(path)).split("\n")) {
39865
+ if (line.length === 0) continue;
39866
+ let value;
39867
+ try {
39868
+ value = JSON.parse(line);
39869
+ } catch {
39870
+ continue;
39871
+ }
39872
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
39873
+ const row = value;
39874
+ if (row.signal_id !== signalId || typeof row.ts !== "string") continue;
39875
+ if (row.event === "listener_delivery_claim") claimedAt = row.ts;
39876
+ if (row.event === "listener_routing_decision" && (row.route_decision === "main" || row.route_decision === "worker")) {
39877
+ routeDecision = row.route_decision;
39878
+ routedAt = row.ts;
39879
+ }
39880
+ }
39881
+ return { claimedAt, routeDecision, routedAt };
39882
+ }
39883
+ async function runListenerAttendanceCanary(options) {
39884
+ const now = options.now ?? Date.now;
39885
+ const sleep2 = options.sleep ?? ((milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)));
39886
+ const startedAt = now();
39887
+ const deadlineMs = startedAt + options.waitMs;
39888
+ const client = new ThinCommandClient(options.target, options.fetcher, {
39889
+ signalRequestTimeoutMs: options.waitMs
39890
+ });
39891
+ const posted = await client.sendSignal({
39892
+ workspaceId: options.workspaceId,
39893
+ credential: await options.credential(),
39894
+ command: {
39895
+ kind: "post_signal",
39896
+ signal_kind: "note",
39897
+ body: "CommonSwarm listener attendance canary. No reply is needed.",
39898
+ to_user_id: null,
39899
+ to_agent_principal_id: options.principalId,
39900
+ in_reply_to: null,
39901
+ about: null,
39902
+ until_ms: 10 * 6e4
39903
+ }
39904
+ });
39905
+ const signalId = posted.response.signal.id;
39906
+ let claimedAt = null;
39907
+ let routeDecision = null;
39908
+ let routedAt = null;
39909
+ let pendingForMainCount = null;
39910
+ let surfacedAt = null;
39911
+ let observedAt = null;
39912
+ let receiptReadErrorCode = null;
39913
+ while (true) {
39914
+ const log = await logEvidence(options.paths.logPath, signalId);
39915
+ claimedAt ??= log.claimedAt;
39916
+ routeDecision ??= log.routeDecision;
39917
+ routedAt ??= log.routedAt;
39918
+ const hook = await new FileHookSurfaceStore(
39919
+ options.paths.instanceDirectory
39920
+ ).evidence();
39921
+ if (hook.surfacedSignalIds.includes(signalId)) {
39922
+ surfacedAt ??= new Date(now()).toISOString();
39923
+ }
39924
+ if (now() >= deadlineMs) break;
39925
+ try {
39926
+ const report = await readAgentDeliveryReceipts(
39927
+ options.target,
39928
+ await options.credential(),
39929
+ options.workspaceId,
39930
+ signalId,
39931
+ {
39932
+ ...options.fetcher ? { fetcher: options.fetcher } : {},
39933
+ deadlineMs,
39934
+ now
39935
+ }
39936
+ );
39937
+ receiptReadErrorCode = null;
39938
+ const receipt = agentReceipt(report.receipts, options.principalId);
39939
+ if (receipt !== null) {
39940
+ claimedAt ??= receipt.delivered_at;
39941
+ if (receipt.ack_outcome === "queued") {
39942
+ routeDecision ??= "main";
39943
+ routedAt ??= receipt.acked_at;
39944
+ pendingForMainCount = receipt.pending_for_main_count ?? null;
39945
+ }
39946
+ const state = deliveryReceiptState(receipt, now());
39947
+ if (state === "observed" || state === "replied") {
39948
+ observedAt = receipt.acked_at;
39949
+ }
39950
+ }
39951
+ } catch (error) {
39952
+ receiptReadErrorCode = error instanceof DeliveryReceiptReadError ? error.code : error instanceof SignalReadTimeoutError ? "timeout" : "transport";
39953
+ }
39954
+ const complete = claimedAt !== null && routeDecision !== null && (routeDecision === "worker" || surfacedAt !== null) && observedAt !== null;
39955
+ if (complete || now() >= deadlineMs) break;
39956
+ await sleep2(Math.min(options.pollMs ?? 250, deadlineMs - now()));
39957
+ }
39958
+ const stalledAt = claimedAt === null ? "claimed" : routeDecision === null ? "routed" : routeDecision === "main" && surfacedAt === null ? "surfaced" : observedAt === null ? "observed" : null;
39959
+ return {
39960
+ signalId,
39961
+ acceptedAt: new Date(startedAt).toISOString(),
39962
+ claimedAt,
39963
+ routeDecision,
39964
+ routedAt,
39965
+ pendingForMainCount,
39966
+ surfacedAt,
39967
+ observedAt,
39968
+ receiptReadErrorCode,
39969
+ stalledAt
39970
+ };
39971
+ }
39972
+ function renderListenerAttendanceCanary(result, workspaceId2, principalId) {
39973
+ const statusCommand = `cswarm listen status --workspace-id ${workspaceId2} --principal-id ${principalId}`;
39974
+ const route = result.routeDecision === "main" ? `queued for the interactive session${result.pendingForMainCount === null ? "" : ` (${result.pendingForMainCount} in queue)`}` : result.routeDecision === "worker" ? "sent to the worker" : "not measured";
39975
+ const lines = [
39976
+ `Canary note: ${result.signalId}.`,
39977
+ `ACCEPTED: yes at ${result.acceptedAt}.`,
39978
+ `CLAIMED: ${result.claimedAt === null ? "no" : `yes at ${result.claimedAt}`}.`,
39979
+ `QUEUED/WORKER: ${route}.`,
39980
+ `SURFACED: ${result.routeDecision === "worker" ? "not required for the worker route" : result.surfacedAt === null ? "no" : `yes at ${result.surfacedAt}`}.`,
39981
+ `OBSERVED: ${result.observedAt === null ? "no" : `yes at ${result.observedAt}`}.`
39982
+ ];
39983
+ if (result.receiptReadErrorCode !== null) {
39984
+ lines.push(`RECEIPT READ: failed (${result.receiptReadErrorCode}).`);
39985
+ }
39986
+ if (result.stalledAt === null) {
39987
+ lines.push("Canary passed: every required hop was measured.");
39988
+ } else if (result.stalledAt === "surfaced") {
39989
+ lines.push(
39990
+ `STALLED: surfaced. Next: cswarm hook install claude --principal-id ${principalId} --write, then start a fresh session. Or restart the listener with --route worker.`
39991
+ );
39992
+ } else {
39993
+ lines.push(`STALLED: ${result.stalledAt}. Next: ${statusCommand}`);
39994
+ }
39995
+ return lines.join("\n");
39996
+ }
39997
+
39998
+ // src/resume.ts
39999
+ var import_node_child_process8 = require("node:child_process");
40000
+ function execFileText(file, args) {
40001
+ return new Promise((resolve2, reject) => {
40002
+ (0, import_node_child_process8.execFile)(file, [...args], {
40003
+ encoding: "utf8",
40004
+ maxBuffer: 4 * 1024 * 1024
40005
+ }, (error, stdout) => {
40006
+ if (error) reject(error);
40007
+ else resolve2(stdout);
40008
+ });
40009
+ });
40010
+ }
40011
+ function systemProcessTable() {
40012
+ return {
40013
+ async list() {
40014
+ const output = await execFileText("ps", ["-axo", "pid=,command="]);
40015
+ return output.split("\n").flatMap((line) => {
40016
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
40017
+ if (!match) return [];
40018
+ const pid = Number(match[1]);
40019
+ return Number.isSafeInteger(pid) && pid > 0 ? [{ pid, command: match[2] }] : [];
40020
+ });
40021
+ }
40022
+ };
40023
+ }
40024
+ function lsofStdoutConsumer() {
40025
+ return {
40026
+ async inspect(pid) {
40027
+ let output;
40028
+ try {
40029
+ output = await execFileText(
40030
+ process.platform === "darwin" ? "/usr/sbin/lsof" : "lsof",
40031
+ ["-nP", "-a", "-p", String(pid), "-d", "1", "-F", "pftan"]
40032
+ );
40033
+ } catch {
40034
+ return "cannot_determine";
40035
+ }
40036
+ const lines = output.split("\n");
40037
+ const type = lines.find((line) => line.startsWith("t"))?.slice(1) ?? "";
40038
+ const names = lines.filter((line) => line.startsWith("n")).map((line) => line.slice(1));
40039
+ if (type === "unix") {
40040
+ if (names.some((name) => name === "->(none)")) return "orphaned";
40041
+ if (names.some((name) => name.startsWith("->") && name !== "->(none)")) {
40042
+ return "live_reader";
40043
+ }
40044
+ return "cannot_determine";
40045
+ }
40046
+ if (type === "PIPE" || type === "FIFO") return "cannot_determine";
40047
+ return type.length === 0 ? "cannot_determine" : "not_pipe";
40048
+ }
40049
+ };
40050
+ }
40051
+ function commandHasFlagValue(command2, flag, values2) {
40052
+ for (const value of values2) {
40053
+ const marker = `${flag} ${value}`;
40054
+ let start = command2.indexOf(marker);
40055
+ while (start !== -1) {
40056
+ const before = start === 0 ? " " : command2[start - 1];
40057
+ const afterIndex = start + marker.length;
40058
+ const after = afterIndex >= command2.length ? " " : command2[afterIndex];
40059
+ if (/\s/.test(before) && /\s/.test(after)) return true;
40060
+ start = command2.indexOf(marker, start + 1);
40061
+ }
40062
+ }
40063
+ return false;
40064
+ }
40065
+ function isNotifyCommand(command2) {
40066
+ return /(?:^|\s)inbox(?:\s|$)/.test(command2) && /(?:^|\s)--notify(?:\s|$)/.test(command2);
40067
+ }
40068
+ async function findNotifyWatchers(options) {
40069
+ const processTable = options.processTable ?? systemProcessTable();
40070
+ const stdoutConsumer = options.stdoutConsumer ?? lsofStdoutConsumer();
40071
+ const rows3 = await processTable.list();
40072
+ const matches = rows3.flatMap((row) => {
40073
+ if (!isNotifyCommand(row.command)) return [];
40074
+ const matchedBy = [];
40075
+ if (commandHasFlagValue(row.command, "--agent-token-file", options.credentialPaths)) {
40076
+ matchedBy.push("agent_token_file");
40077
+ }
40078
+ if (commandHasFlagValue(row.command, "--principal-id", [options.principalId])) {
40079
+ matchedBy.push("principal_id");
40080
+ }
40081
+ return matchedBy.length === 0 ? [] : [{ pid: row.pid, matchedBy }];
40082
+ });
40083
+ const unique = [...new Map(matches.map((row) => [row.pid, row])).values()].sort((left, right) => left.pid - right.pid);
40084
+ return await Promise.all(unique.map(async (row) => ({
40085
+ ...row,
40086
+ stdout: await stdoutConsumer.inspect(row.pid)
40087
+ })));
40088
+ }
40089
+ async function readOnlyListenerInspection(paths, adapters = {}) {
40090
+ const query = adapters.queryStatus ?? queryListenerControl;
40091
+ const read = adapters.readStatus ?? readListenerStatusIfPresent;
40092
+ try {
40093
+ return {
40094
+ checkedDirectory: paths.instanceDirectory,
40095
+ status: await query(paths, "status"),
40096
+ source: "live_process"
40097
+ };
40098
+ } catch {
40099
+ const status = await read(paths);
40100
+ return {
40101
+ checkedDirectory: paths.instanceDirectory,
40102
+ status,
40103
+ source: status === null ? "not_found" : "recorded_file"
40104
+ };
40105
+ }
40106
+ }
40107
+ async function inspectResume(options, adapters) {
40108
+ const identity = await adapters.readIdentity();
40109
+ const principalId = identity.principalId.toLowerCase();
40110
+ const paths = listenerPaths({
40111
+ profileId: options.target.profileId,
40112
+ workspaceId: options.workspaceId,
40113
+ principalId,
40114
+ ...options.stateDirectory ? { stateDirectory: options.stateDirectory } : {}
40115
+ });
40116
+ const listener = await readOnlyListenerInspection(paths, adapters);
40117
+ const watchers = await findNotifyWatchers({
40118
+ credentialPaths: options.credentialPathAliases ?? [options.credentialFile],
40119
+ principalId,
40120
+ ...adapters.processTable ? { processTable: adapters.processTable } : {},
40121
+ ...adapters.stdoutConsumer ? { stdoutConsumer: adapters.stdoutConsumer } : {}
40122
+ });
40123
+ const topics = await adapters.readBrainTopics();
40124
+ const digestStore = new FileBrainDigestStore(paths.instanceDirectory, principalId);
40125
+ const digest = await digestStore.preview(topics);
40126
+ const inbox = await adapters.readInboxCount(principalId, paths.instanceDirectory);
40127
+ return {
40128
+ identity: { ...identity, principalId },
40129
+ listener,
40130
+ watchers,
40131
+ brain: { digest, highWaterFile: digestStore.location },
40132
+ inbox,
40133
+ target: options.target,
40134
+ workspaceId: options.workspaceId,
40135
+ credentialFile: options.credentialFile,
40136
+ installedVersion: options.installedVersion
40137
+ };
40138
+ }
40139
+ function safeText(value) {
40140
+ return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").slice(0, 2e3);
40141
+ }
40142
+ function shellArg(value) {
40143
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
40144
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
40145
+ }
40146
+ function commonCommandArgs(report) {
40147
+ return [
40148
+ "--agent-token-file",
40149
+ shellArg(report.credentialFile),
40150
+ "--url",
40151
+ shellArg(report.target.url),
40152
+ "--anon-key",
40153
+ shellArg(report.target.anonKey),
40154
+ "--workspace-id",
40155
+ report.workspaceId
40156
+ ].join(" ");
40157
+ }
40158
+ function restartCommand(report, status) {
40159
+ const common = commonCommandArgs(report);
40160
+ const route = status.routeMode ?? "worker";
40161
+ const start = [
40162
+ "cswarm listen start",
40163
+ common,
40164
+ `--provider ${status.provider}`,
40165
+ `--permissions ${status.permissionMode ?? "allow"}`,
40166
+ `--route ${route}`,
40167
+ ...route === "split" && status.deferOverChars !== null && status.deferOverChars !== void 0 ? [`--defer-over ${status.deferOverChars}`] : []
40168
+ ].join(" ");
40169
+ return `cswarm listen stop ${common} && ${start}`;
40170
+ }
40171
+ function watcherStateLine(watcher) {
40172
+ const matched = watcher.matchedBy.map(
40173
+ (value) => value === "agent_token_file" ? "credential path" : "principal id"
40174
+ ).join(" and ");
40175
+ const state = watcher.stdout === "live_reader" ? "stdout has a live pipe reader" : watcher.stdout === "orphaned" ? "ORPHAN: stdout pipe has no reader" : watcher.stdout === "not_pipe" ? "stdout is not a pipe; the dead-reader check does not apply" : "stdout reader cannot be determined on this host";
40176
+ return `- PID ${watcher.pid}: ${state}; matched ${matched}.`;
40177
+ }
40178
+ function renderResume(report) {
40179
+ const lines = [
40180
+ "Identity",
40181
+ `You are ${safeText(report.identity.displayName)} (${report.identity.principalId}).`,
40182
+ "Next: use this principal for every listener, watcher, brain, and inbox check below.",
40183
+ "",
40184
+ "Listener"
40185
+ ];
40186
+ const listener = report.listener;
40187
+ const status = listener.status;
40188
+ if (status === null) {
40189
+ lines.push(
40190
+ `No listener found under ${safeText(listener.checkedDirectory)} for profile ${report.target.profileId}.`,
40191
+ `Next: start one with the original provider: cswarm listen start ${commonCommandArgs(report)} --provider <provider>`
40192
+ );
40193
+ } else {
40194
+ if (listener.source === "live_process") {
40195
+ lines.push(
40196
+ `Found under ${safeText(listener.checkedDirectory)}. State: ${status.state}, reported by running PID ${status.pid}.`
40197
+ );
40198
+ } else {
40199
+ lines.push(
40200
+ `Found a status file under ${safeText(listener.checkedDirectory)}. Recorded state: ${status.state}; the control socket did not answer, so a running listener is not established.`
40201
+ );
40202
+ }
40203
+ const runningVersion = status.cswarmVersion ?? null;
40204
+ if (listener.source !== "live_process") {
40205
+ lines.push(
40206
+ `Running listener cswarm version: cannot determine because the process did not answer; status file recorded ${runningVersion ?? "no version"}; installed CLI: ${report.installedVersion}.`,
40207
+ `Next: restart the listener because its process did not answer: ${restartCommand(report, status)}`
40208
+ );
40209
+ } else if (runningVersion === null) {
40210
+ lines.push(
40211
+ `Listener cswarm version: cannot determine from this listener; installed CLI: ${report.installedVersion}.`,
40212
+ `Next: restart it to make the running version reportable: ${restartCommand(report, status)}`
40213
+ );
40214
+ } else if (runningVersion !== report.installedVersion) {
40215
+ lines.push(
40216
+ `VERSION MISMATCH: listener runs ${runningVersion}; installed ${report.installedVersion} \u2014 restart it: ${restartCommand(report, status)}`
40217
+ );
40218
+ } else {
40219
+ lines.push(
40220
+ `Listener-reported cswarm: ${runningVersion}; installed CLI: ${report.installedVersion}.`,
40221
+ "Next: no listener restart is needed for a version change."
40222
+ );
40223
+ }
40224
+ }
40225
+ lines.push(
40226
+ "",
40227
+ "Notify watchers",
40228
+ `Checked process arguments for inbox --notify matching --agent-token-file ${safeText(report.credentialFile)} or --principal-id ${report.identity.principalId}.`
40229
+ );
40230
+ if (report.watchers.length === 0) {
40231
+ lines.push(
40232
+ "Found: 0.",
40233
+ `Next: start one watcher under a live Monitor: cswarm inbox --notify ${commonCommandArgs(report)}`
40234
+ );
40235
+ } else {
40236
+ lines.push(`Found: ${report.watchers.length}.`);
40237
+ lines.push(...report.watchers.map(watcherStateLine));
40238
+ const orphans = report.watchers.filter((watcher) => watcher.stdout === "orphaned");
40239
+ if (orphans.length > 0) {
40240
+ lines.push(
40241
+ `Next: stop only the orphan watcher${orphans.length === 1 ? "" : "s"}; CommonSwarm did not kill anything: kill ${orphans.map((watcher) => watcher.pid).join(" ")}`
40242
+ );
40243
+ } else if (report.watchers.some((watcher) => watcher.stdout === "cannot_determine")) {
40244
+ lines.push(
40245
+ "Next: verify each unknown stdout reader in the host Monitor before you start another watcher."
40246
+ );
40247
+ } else {
40248
+ lines.push("Next: keep one watcher with a live output surface; do not start a duplicate.");
40249
+ }
40250
+ }
40251
+ lines.push(
40252
+ "",
40253
+ "Brain digest",
40254
+ `Checked ${safeText(report.brain.highWaterFile)} without advancing it.`
40255
+ );
40256
+ if (report.brain.digest === null) {
40257
+ lines.push(
40258
+ "No brain topic is new or changed since this principal's digest high-water.",
40259
+ "Next: no brain read is needed now."
40260
+ );
40261
+ } else {
40262
+ lines.push(report.brain.digest, "Next: read any needed topic with the command above.");
40263
+ }
40264
+ const inboxCount = report.inbox.exact ? String(report.inbox.count) : `at least ${report.inbox.count}`;
40265
+ lines.push(
40266
+ "",
40267
+ "Unread inbox",
40268
+ `Unread directed asks and notes from the same read used by the hook: ${inboxCount}.`,
40269
+ report.inbox.count === 0 ? "Next: no inbox action is needed now." : `Next: read them without acknowledging them first: cswarm inbox ${commonCommandArgs(report)}`,
40270
+ "",
40271
+ "Read-only check complete. No cursor, brain high-water, listener status, receipt, acknowledgement, or process was changed."
40272
+ );
40273
+ return lines.join("\n");
40274
+ }
40275
+ function resumeJson(report) {
40276
+ return {
40277
+ identity: {
40278
+ display_name: report.identity.displayName,
40279
+ principal_id: report.identity.principalId
40280
+ },
40281
+ listener: {
40282
+ found: report.listener.status !== null,
40283
+ checked_directory: report.listener.checkedDirectory,
40284
+ source: report.listener.source,
40285
+ state: report.listener.status?.state ?? null,
40286
+ pid: report.listener.status?.pid ?? null,
40287
+ running_cswarm_version: report.listener.source === "live_process" ? report.listener.status?.cswarmVersion ?? null : null,
40288
+ installed_cswarm_version: report.installedVersion,
40289
+ version_mismatch: report.listener.source === "live_process" && report.listener.status?.cswarmVersion !== null && report.listener.status?.cswarmVersion !== void 0 && report.listener.status.cswarmVersion !== report.installedVersion
40290
+ },
40291
+ notify_watchers: report.watchers.map((watcher) => ({
40292
+ pid: watcher.pid,
40293
+ matched_by: watcher.matchedBy,
40294
+ stdout: watcher.stdout
40295
+ })),
40296
+ brain: {
40297
+ high_water_file: report.brain.highWaterFile,
40298
+ high_water_advanced: false,
40299
+ digest: report.brain.digest
40300
+ },
40301
+ unread_inbox: report.inbox,
40302
+ read_only: true
40303
+ };
40304
+ }
40305
+
39646
40306
  // src/cli.ts
39647
40307
  var import_meta = {};
39648
40308
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
@@ -39650,6 +40310,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
39650
40310
  "agent-token-file",
39651
40311
  "agent-token-stdin",
39652
40312
  "all-devices",
40313
+ "allow-unattended",
39653
40314
  "anon-key",
39654
40315
  "attach",
39655
40316
  "branch",
@@ -39699,6 +40360,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
39699
40360
  "since",
39700
40361
  "site",
39701
40362
  "slug",
40363
+ "state-dir",
39702
40364
  "renewal-horizon-days",
39703
40365
  "standing",
39704
40366
  "task-id",
@@ -39718,6 +40380,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
39718
40380
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
39719
40381
  "agent-token-stdin",
39720
40382
  "all-devices",
40383
+ "allow-unattended",
39721
40384
  "confirm-standing",
39722
40385
  "force-file-store",
39723
40386
  "follow",
@@ -39747,8 +40410,8 @@ var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
39747
40410
  AGENT_CREDENTIAL_MESSAGE_D088
39748
40411
  ];
39749
40412
  function packageVersion() {
39750
- if ("0.1.44".length > 0) {
39751
- return "0.1.44";
40413
+ if ("0.1.45".length > 0) {
40414
+ return "0.1.45";
39752
40415
  }
39753
40416
  try {
39754
40417
  const value = JSON.parse(
@@ -39865,6 +40528,7 @@ Usage:
39865
40528
  cswarm target clear [--json]
39866
40529
  cswarm status [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
39867
40530
  cswarm whoami ${requiredAgentCredential} [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
40531
+ cswarm resume --agent-token-file <path> [--url <url> --anon-key <key>] --workspace-id <uuid> [--json]
39868
40532
  cswarm members [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
39869
40533
  cswarm working-on "<what>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--about <ref>] [--until <dur>] [--json]
39870
40534
  cswarm note "<text>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--to <member|agent>] [--about <ref>] [--attach <path> ...] [--until <dur>] [--json] # text: 1..8000 characters
@@ -39884,7 +40548,8 @@ Usage:
39884
40548
  cswarm brain get <topic> [--version <n>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
39885
40549
  cswarm brain put <topic> [<markdown-path>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json] # without a path, reads Markdown from stdin
39886
40550
  cswarm feedback "<text>" --kind bug|idea|friction [--about <ref>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
39887
- cswarm listen start ${requiredAgentCredential} [--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]
40551
+ cswarm listen start ${requiredAgentCredential} [--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>] [--allow-unattended] [--foreground] [--json]
40552
+ cswarm listen canary ${requiredAgentCredential} [--url <url> --anon-key <key>] --workspace-id <uuid> [--state-dir <path>] [--wait <seconds>] [--json]
39888
40553
  cswarm listen status ${agentCredential2} [--url <url> --anon-key <key>] --workspace-id <uuid> [--principal-id <uuid>] [--json]
39889
40554
  cswarm listen stop ${agentCredential2} [--url <url> --anon-key <key>] --workspace-id <uuid> [--principal-id <uuid>] [--json]
39890
40555
  cswarm hook check [--principal-id <uuid> ...] [--cooldown <seconds>]
@@ -39923,6 +40588,7 @@ Credential selection for command/dogfood:
39923
40588
  One that persists or references the credential needs the complete
39924
40589
  JSON artifact, because it needs a field a bare secret does not carry:
39925
40590
  whoami reads server-proven identity -- complete or bare form
40591
+ resume reads reconnect state -- complete or bare file form
39926
40592
  members reads only -- either form
39927
40593
  working-on, note, ask, reply, feed, inbox
39928
40594
  signal command/read only -- either form
@@ -39935,6 +40601,8 @@ Credential selection for command/dogfood:
39935
40601
  command, dogfood
39936
40602
  task protocol commands -- either form
39937
40603
  listen start persists durable state, rotates -- needs expires_at
40604
+ listen canary posts one self-note and selects local state -- needs
40605
+ principal_id; it does not renew the credential
39938
40606
  listen status
39939
40607
  selects the listener profile -- complete JSON or
39940
40608
  an explicit --principal-id without a credential
@@ -39972,15 +40640,20 @@ fresh credential.
39972
40640
  listen start --route worker|main|split chooses where directed messages go. worker
39973
40641
  is the unchanged default. main queues every ask or note for the interactive session.
39974
40642
  split queues messages whose body is longer than --defer-over <chars>; the bound is
39975
- 1..10000 and an equal-length message stays on the worker path. Run cswarm hook check
40643
+ 1..10000 and an equal-length message stays on the worker path. main and split require
40644
+ a principal-scoped Claude hook or prior hook surface. --allow-unattended accepts the
40645
+ risk explicitly. Run cswarm hook check
39976
40646
  --principal-id <uuid> to surface that agent's queued messages. A bare check works only
39977
40647
  when the state directory holds one principal. hook check has its own 3s ceiling, exits 0
39978
40648
  on every outcome, and skips network checks made within --cooldown seconds (default 30).
40649
+ listen canary posts one self-addressed note, waits at most --wait seconds (default 10),
40650
+ and reports accepted, claimed, queued/worker, surfaced, and observed as separate hops.
39979
40651
  hook install claude prints principal-scoped UserPromptSubmit JSON by default. --write changes
39980
40652
  <project>/.claude/settings.local.json, which applies only to Claude Code sessions started in
39981
40653
  that project. Inside a git repository, the local file must be ignored. --user opts in to
39982
- \${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json and warns about shared hosts. --repo keeps the
39983
- repository-wide .claude/settings.json scope and also requires an ignored file. Uninstall also
40654
+ \${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json and warns that every Claude Code session reading
40655
+ that directory is affected. --repo keeps the repository-wide .claude/settings.json scope and
40656
+ also requires an ignored file. Uninstall also
39984
40657
  requires --write and uses the same scope selection.
39985
40658
 
39986
40659
  Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, new, and workspace close require a
@@ -40237,7 +40910,7 @@ async function stdinInviteLink() {
40237
40910
  return link;
40238
40911
  }
40239
40912
  async function confirmationLine(prompt) {
40240
- const reader = (0, import_promises12.createInterface)({
40913
+ const reader = (0, import_promises13.createInterface)({
40241
40914
  input: process.stdin,
40242
40915
  output: process.stderr,
40243
40916
  terminal: Boolean(process.stdin.isTTY)
@@ -42168,6 +42841,100 @@ Owner: ${ownerName} (${identity.owner_user_id}).
42168
42841
  `)
42169
42842
  );
42170
42843
  }
42844
+ async function runResume(args) {
42845
+ args.assertShape([
42846
+ ...TARGET_FLAGS,
42847
+ "workspace-id",
42848
+ "agent-token-file",
42849
+ "state-dir",
42850
+ "json"
42851
+ ], 1);
42852
+ const suppliedCredentialPath = args.optional("agent-token-file");
42853
+ if (suppliedCredentialPath === void 0) {
42854
+ throw new UsageError("cswarm resume needs --agent-token-file <path>");
42855
+ }
42856
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(suppliedCredentialPath)) {
42857
+ throw new Error("--agent-token-file must not contain control characters");
42858
+ }
42859
+ const credentialFile = (0, import_node_path21.resolve)(suppliedCredentialPath);
42860
+ const cloud = await target(args);
42861
+ const workspaceId2 = listenerUuid(
42862
+ args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
42863
+ "workspace-id"
42864
+ );
42865
+ const agent = await agentCredential(args);
42866
+ const report = await inspectResume({
42867
+ target: cloud,
42868
+ workspaceId: workspaceId2,
42869
+ credentialFile,
42870
+ credentialPathAliases: [.../* @__PURE__ */ new Set([suppliedCredentialPath, credentialFile])],
42871
+ installedVersion: CLI_BUILD_VERSION,
42872
+ ...listenerStateDirectory(args) ? { stateDirectory: listenerStateDirectory(args) } : {}
42873
+ }, {
42874
+ readIdentity: async () => {
42875
+ const directory = await readAgentSignalDirectory(
42876
+ cloud,
42877
+ agent.token,
42878
+ workspaceId2
42879
+ );
42880
+ const identity = directory.identity;
42881
+ if (identity === void 0 || identity.workspace_id !== workspaceId2) {
42882
+ throw new Error(
42883
+ "the read service authenticated this credential but did not return its identity for this workspace; resume stopped without changing state"
42884
+ );
42885
+ }
42886
+ const principal = directory.agents.find(
42887
+ (candidate) => candidate.principal_id === identity.principal_id && candidate.owner_user_id === identity.owner_user_id
42888
+ );
42889
+ if (principal === void 0) {
42890
+ throw new Error(
42891
+ "the read service authenticated this credential but returned no matching live principal; resume stopped without changing state"
42892
+ );
42893
+ }
42894
+ if (agent.principalId !== null && agent.principalId !== identity.principal_id) {
42895
+ process.stderr.write(
42896
+ `WARNING: the credential authenticated as ${sanitizeDisplayLabel(principal.name, "Unnamed agent")} (${identity.principal_id}), but its JSON metadata names ${agent.principalId}. Resume used the authenticated identity.
42897
+ `
42898
+ );
42899
+ }
42900
+ return {
42901
+ displayName: sanitizeDisplayLabel(principal.name, "Unnamed agent"),
42902
+ principalId: identity.principal_id
42903
+ };
42904
+ },
42905
+ readBrainTopics: async () => brainTopicSnapshots(await listBrainRowsAsAgent(
42906
+ cloud,
42907
+ agent.token,
42908
+ workspaceId2
42909
+ )),
42910
+ readInboxCount: async (principalId, instanceDirectory) => {
42911
+ const page = await readAgentSignalPage(
42912
+ cloud,
42913
+ { kind: "agent", token: agent.token },
42914
+ {
42915
+ workspaceId: workspaceId2,
42916
+ inbox: true,
42917
+ ascending: false,
42918
+ limit: 100,
42919
+ includeStale: false
42920
+ },
42921
+ fetch,
42922
+ { tolerateMalformedRows: true, maxMalformedRows: 3 }
42923
+ );
42924
+ const candidates = page.signals.filter(
42925
+ (signal) => (signal.kind === "ask" || signal.kind === "note") && signal.workspace_id === workspaceId2 && signal.to_agent === principalId
42926
+ ).map((signal) => ({ signalId: signal.id }));
42927
+ const unseen = await new FileHookSurfaceStore(instanceDirectory).previewUnseen(candidates);
42928
+ return {
42929
+ count: unseen.length,
42930
+ exact: page.rawCount < 100 && page.malformedRows === 0
42931
+ };
42932
+ }
42933
+ });
42934
+ if (args.has("json")) printJson(resumeJson(report));
42935
+ else process.stdout.write(`${renderResume(report)}
42936
+ `);
42937
+ }
42171
42938
  async function runSignalRead(args, inbox) {
42172
42939
  const notify = inbox && args.has("notify");
42173
42940
  args.assertShape(notify ? [
@@ -42268,15 +43035,6 @@ async function runSignalRead(args, inbox) {
42268
43035
  })}
42269
43036
  `);
42270
43037
  }
42271
- async function writeMonitorLine(line) {
42272
- await new Promise((resolve2, reject) => {
42273
- process.stdout.write(`${line}
42274
- `, (error) => {
42275
- if (error) reject(error);
42276
- else resolve2();
42277
- });
42278
- });
42279
- }
42280
43038
  async function runInboxNotifyCommand(args) {
42281
43039
  if (!hasAgentCredential(args)) {
42282
43040
  throw new Error(
@@ -42333,7 +43091,7 @@ async function runInboxNotifyCommand(args) {
42333
43091
  selected.selectedWorkspace,
42334
43092
  cloud
42335
43093
  );
42336
- await writeMonitorLine(
43094
+ await writeArrivalMonitorLine(
42337
43095
  args.has("json") ? JSON.stringify(notification) : formatArrivalNotification(notification)
42338
43096
  );
42339
43097
  },
@@ -42677,10 +43435,41 @@ function listenerHostLimits(provider) {
42677
43435
  }
42678
43436
  };
42679
43437
  }
42680
- function listenerStatusJson(status, permissionMode) {
43438
+ function listenerAttendanceState(status, evidence) {
43439
+ const routeMode = status.routeMode ?? "worker";
43440
+ const pending = status.pendingForMainCount ?? 0;
43441
+ const connected = status.state === "ready";
43442
+ const attendanceState = routeMode === "worker" ? "not_required" : pending > 0 ? "unattended" : evidence.hookSurfaceAdvanced ? "attended" : "unproven";
43443
+ const attended = attendanceState === "attended" ? true : attendanceState === "unattended" ? false : null;
43444
+ const handled = pending > 0 ? false : routeMode === "worker" && status.lastAckAt !== null ? true : null;
43445
+ return {
43446
+ connected,
43447
+ attended,
43448
+ attendanceState,
43449
+ handled,
43450
+ handledState: handled === true ? "handled" : handled === false ? "not_handled" : "not_yet_measured"
43451
+ };
43452
+ }
43453
+ function listenerAttendanceRemedy(principalId) {
43454
+ return `cswarm hook install claude --principal-id ${principalId} --write, then start a fresh session. Or restart the listener with --route worker.`;
43455
+ }
43456
+ function listenerStatusJson(status, permissionMode, evidence = {
43457
+ pendingForMainOldestAt: null,
43458
+ hookSurfaceExists: false,
43459
+ hookSurfaceAdvanced: false
43460
+ }, nowMs = Date.now()) {
42681
43461
  const mode3 = permissionMode ?? status.permissionMode;
43462
+ const attendance = listenerAttendanceState(status, evidence);
43463
+ const pending = status.pendingForMainCount ?? 0;
42682
43464
  return {
42683
43465
  ...status,
43466
+ ...attendance,
43467
+ hookSurfaceExists: evidence.hookSurfaceExists,
43468
+ hookSurfaceAdvanced: evidence.hookSurfaceAdvanced,
43469
+ pendingForMainOldestAt: evidence.pendingForMainOldestAt,
43470
+ pendingForMainOldestAgeMs: evidence.pendingForMainOldestAt === null ? null : Math.max(0, nowMs - Date.parse(evidence.pendingForMainOldestAt)),
43471
+ attendanceWarningCode: pending > 0 ? "listener_unattended_main_queue" : null,
43472
+ attendanceNextStep: pending > 0 ? listenerAttendanceRemedy(status.principalId) : null,
42684
43473
  deliveryMode: status.deliveryMode ?? null,
42685
43474
  pendingDeliveryCount: status.pendingDeliveryCount ?? null,
42686
43475
  lastTerminalDeliveryFailureCount: status.lastTerminalDeliveryFailureCount ?? null,
@@ -42702,16 +43491,25 @@ function listenerStatusJson(status, permissionMode) {
42702
43491
  host_limits: listenerHostLimits(status.provider)
42703
43492
  };
42704
43493
  }
42705
- function renderListenerStatus(status) {
43494
+ function renderListenerStatus(status, evidence = {
43495
+ pendingForMainOldestAt: null,
43496
+ hookSurfaceExists: false,
43497
+ hookSurfaceAdvanced: false
43498
+ }, nowMs = Date.now()) {
42706
43499
  const routeMode = status.routeMode ?? "worker";
42707
43500
  const pendingForMainCount = status.pendingForMainCount ?? 0;
42708
43501
  const droppedForMainCount = status.droppedForMainCount ?? 0;
43502
+ const unattendedCount = `${pendingForMainCount} ${pendingForMainCount === 1 ? "message is" : "messages are"} unattended`;
43503
+ const attendance = listenerAttendanceState(status, evidence);
42709
43504
  const lines = [
42710
- `Listener ${status.state} for agent ${status.principalId}.`,
43505
+ pendingForMainCount > 0 ? `Listener WARNING for agent ${status.principalId}: ${unattendedCount}.` : `Listener ${status.state} for agent ${status.principalId}.`,
43506
+ `CONNECTED: ${attendance.connected ? "yes" : "no"}. Transport state is ${status.state}.`,
43507
+ `ATTENDED: ${attendance.attendanceState === "attended" ? "yes. The session hook has surfaced messages on this host" : attendance.attendanceState === "unattended" ? "no. The main-session queue is not draining" : attendance.attendanceState === "not_required" ? "not required for the worker route" : "not yet proven on this host"}.`,
43508
+ `HANDLED: ${attendance.handledState === "handled" ? "yes. A delivery acknowledgement is recorded" : attendance.handledState === "not_handled" ? "no. Queued messages have not reached the session hook" : "not yet measured"}.`,
42711
43509
  `Provider: ${status.provider}; process: ${status.pid}; started: ${status.startedAt}.`,
42712
43510
  status.readyAt ? `Ready since: ${status.readyAt}.` : "Not ready yet.",
42713
- status.lastSignalId ? `Last handled signal: ${status.lastSignalId}.` : "No signal has been handled yet.",
42714
- status.lastErrorCode ? `Last status code: ${status.lastErrorCode}.` : "No listener error is recorded."
43511
+ status.lastSignalId ? pendingForMainCount > 0 ? `Last claimed and queued signal: ${status.lastSignalId}. It is not handled yet.` : routeMode === "worker" ? `Last handled signal: ${status.lastSignalId}.` : `Last listener signal: ${status.lastSignalId}. Local status does not prove its final observed receipt.` : "No signal has been handled yet.",
43512
+ status.lastErrorCode ? `Last status code: ${status.lastErrorCode}.` : "No listener process error is recorded."
42715
43513
  ];
42716
43514
  if (status.lastErrorDetail) {
42717
43515
  const [first, ...rest] = status.lastErrorDetail.split("\n");
@@ -42727,7 +43525,7 @@ function renderListenerStatus(status) {
42727
43525
  }
42728
43526
  if (status.providerVersion && status.providerLastMeasuredVersion) {
42729
43527
  lines.push(
42730
- `Provider version ${status.providerVersion} is newer than the last measured version ${status.providerLastMeasuredVersion}. It is unverified but allowed because the startup permission canary passed. Next: verify this provider release with CommonSwarm and update the last-measured version.`
43528
+ status.providerVersion === status.providerLastMeasuredVersion ? `Provider version: ${status.providerVersion} (last measured: ${status.providerLastMeasuredVersion}).` : `Provider version ${status.providerVersion} is newer than the last measured version ${status.providerLastMeasuredVersion}. It is unverified but allowed because the startup permission canary passed. Next: verify this provider release with CommonSwarm and update the last-measured version.`
42731
43529
  );
42732
43530
  }
42733
43531
  if (status.deliveryMode === "durable_claim") {
@@ -42755,8 +43553,14 @@ function renderListenerStatus(status) {
42755
43553
  }
42756
43554
  if (pendingForMainCount > 0) {
42757
43555
  lines.push(
42758
- status.state === "stopped" || status.state === "failed" ? `${pendingForMainCount} asks are stranded because this listener is not running. Restart it by piping the same agent credential into: ${listenerRestartCommand(status)}` : `${pendingForMainCount} asks waiting for this session; they surface at your next prompt, or run cswarm hook check.`
43556
+ `WARNING [listener_unattended_main_queue]: ${unattendedCount}. The oldest was queued ${evidence.pendingForMainOldestAt === null ? "an unknown time" : relativeAge(evidence.pendingForMainOldestAt, nowMs)}${evidence.pendingForMainOldestAt === null ? "" : ` (queued at ${evidence.pendingForMainOldestAt})`}.`
42759
43557
  );
43558
+ lines.push(`Next: ${listenerAttendanceRemedy(status.principalId)}`);
43559
+ if (status.state === "stopped" || status.state === "failed") {
43560
+ lines.push(
43561
+ `${pendingForMainCount} ${pendingForMainCount === 1 ? "message is" : "messages are"} also stranded because this listener is not running. Restart it by piping the same agent credential into: ${listenerRestartCommand(status)}`
43562
+ );
43563
+ }
42760
43564
  }
42761
43565
  }
42762
43566
  if (status.lastTerminalDeliveryFailureCount !== null && status.lastTerminalDeliveryFailureCount > 0) {
@@ -42776,16 +43580,30 @@ async function unsurfacedPendingMainStats(instanceDirectory, fallback) {
42776
43580
  const queue = new FilePendingMainQueue(instanceDirectory);
42777
43581
  const pending = await queue.read();
42778
43582
  const stats = await queue.stats();
42779
- const staged = await new FileHookSurfaceStore(instanceDirectory).stage(
43583
+ const surface = new FileHookSurfaceStore(instanceDirectory);
43584
+ const staged = await surface.stage(
42780
43585
  pending,
42781
43586
  stats.droppedCount
42782
43587
  );
42783
- return { count: staged.unseen.length, droppedCount: stats.droppedCount };
43588
+ const hook = await surface.evidence();
43589
+ const oldestAt = staged.unseen.reduce((oldest, item) => oldest === null || Date.parse(item.queuedAt) < Date.parse(oldest) ? item.queuedAt : oldest, null);
43590
+ return {
43591
+ count: staged.unseen.length,
43592
+ droppedCount: stats.droppedCount,
43593
+ oldestAt,
43594
+ hookSurfaceExists: hook.exists,
43595
+ hookSurfaceAdvanced: hook.surfacedSignalIds.length > 0
43596
+ };
42784
43597
  } catch {
42785
- return fallback;
43598
+ return {
43599
+ ...fallback,
43600
+ oldestAt: null,
43601
+ hookSurfaceExists: false,
43602
+ hookSurfaceAdvanced: false
43603
+ };
42786
43604
  }
42787
43605
  }
42788
- function listenerFailureMessage(code, provider) {
43606
+ function listenerFailureMessage(code, provider, detail) {
42789
43607
  if (code === "version_below_floor") {
42790
43608
  if (provider === "codex") {
42791
43609
  return "the Codex listener requires codex-acp 1.1.9 or newer; update the bridge, then retry";
@@ -42804,6 +43622,9 @@ function listenerFailureMessage(code, provider) {
42804
43622
  if (code === "version_refused") {
42805
43623
  return `the ${provider ?? "provider"} version check could not run, so startup stopped. Next: run the provider's --version command and fix that error, then retry`;
42806
43624
  }
43625
+ if (code === "executable_not_bridge" && provider === "codex") {
43626
+ return "this is the Codex CLI; --codex-executable takes the codex-acp bridge (npm i -g @agentclientprotocol/codex-acp)";
43627
+ }
42807
43628
  if (code === "executable_missing" && provider === "claude") {
42808
43629
  return "claude-agent-acp is not installed; run npm install -g @agentclientprotocol/claude-agent-acp@latest (minimum 0.64.2), then retry";
42809
43630
  }
@@ -42845,7 +43666,8 @@ function listenerFailureMessage(code, provider) {
42845
43666
  return "the Claude bridge did not complete the ACP permission canary; the startup canary ran, but no workspace signal prompt was delivered. Confirm Claude Code keychain/OAuth sign-in, then retry";
42846
43667
  }
42847
43668
  if (provider === "codex") {
42848
- return "the Codex bridge did not complete the read-only ACP permission canary; no workspace signal prompt was delivered. Confirm ChatGPT/Codex sign-in, then retry";
43669
+ const recorded = detail?.trim();
43670
+ return recorded ? `the Codex bridge did not complete the read-only ACP permission canary; no workspace signal prompt was delivered. Recorded reason: ${JSON.stringify(recorded)}` : "the Codex bridge did not complete the read-only ACP permission canary; no workspace signal prompt was delivered. The recorded reason was unavailable. Next: run cswarm listen status, then retry";
42849
43671
  }
42850
43672
  if (provider === "grok") {
42851
43673
  return "the Grok bridge did not complete the ACP permission canary; no workspace signal prompt was delivered. The local cswarm listen status output includes the final error detail; read it, then retry";
@@ -43077,6 +43899,7 @@ async function runConfiguredListener(options) {
43077
43899
  workspaceId: options.workspaceId,
43078
43900
  principalId: options.principalId,
43079
43901
  provider: options.provider,
43902
+ cswarmVersion: CLI_BUILD_VERSION,
43080
43903
  permissionMode: options.permissionMode,
43081
43904
  routeMode,
43082
43905
  deferOverChars,
@@ -43159,6 +43982,7 @@ async function runListenStart(args) {
43159
43982
  "turn-budget",
43160
43983
  "route",
43161
43984
  "defer-over",
43985
+ "allow-unattended",
43162
43986
  "foreground",
43163
43987
  "json"
43164
43988
  ], 2);
@@ -43198,6 +44022,9 @@ async function runListenStart(args) {
43198
44022
  `a listener is already ${existing.state} for agent ${principalId}`
43199
44023
  );
43200
44024
  }
44025
+ if (routing.routeMode !== "worker" && !args.has("allow-unattended") && !await listenerHasAttendanceSurface(paths.instanceDirectory, cwd, principalId)) {
44026
+ throw new ListenerUnattendedRefusedError(principalId);
44027
+ }
43201
44028
  let status;
43202
44029
  if (args.has("foreground")) {
43203
44030
  status = await runConfiguredListener({
@@ -43282,16 +44109,27 @@ async function runListenStart(args) {
43282
44109
  });
43283
44110
  } catch (error) {
43284
44111
  if (error instanceof ListenerStartupError) {
43285
- throw new Error(listenerFailureMessage(error.code, provider));
44112
+ const failedStatus = await effectiveListenerStatus(paths).catch(() => null);
44113
+ const detail = failedStatus?.lastErrorCode === error.code ? failedStatus.lastErrorDetail : null;
44114
+ throw new Error(listenerFailureMessage(error.code, provider, detail));
43286
44115
  }
43287
44116
  throw error;
43288
44117
  }
43289
44118
  }
43290
44119
  if (status.state === "failed") {
43291
44120
  throw new Error(
43292
- listenerFailureMessage(status.lastErrorCode ?? "unknown_error", provider)
44121
+ listenerFailureMessage(
44122
+ status.lastErrorCode ?? "unknown_error",
44123
+ provider,
44124
+ status.lastErrorDetail
44125
+ )
43293
44126
  );
43294
44127
  }
44128
+ let attendanceEvidence = {
44129
+ pendingForMainOldestAt: null,
44130
+ hookSurfaceExists: false,
44131
+ hookSurfaceAdvanced: false
44132
+ };
43295
44133
  if ((status.routeMode ?? "worker") !== "worker") {
43296
44134
  const recordedPending = status.pendingForMainCount ?? 0;
43297
44135
  const recordedDropped = status.droppedForMainCount ?? 0;
@@ -43304,9 +44142,14 @@ async function runListenStart(args) {
43304
44142
  pendingForMainCount: queueStats.count,
43305
44143
  droppedForMainCount: queueStats.droppedCount
43306
44144
  };
44145
+ attendanceEvidence = {
44146
+ pendingForMainOldestAt: queueStats.oldestAt,
44147
+ hookSurfaceExists: queueStats.hookSurfaceExists,
44148
+ hookSurfaceAdvanced: queueStats.hookSurfaceAdvanced
44149
+ };
43307
44150
  }
43308
44151
  if (args.has("json")) {
43309
- printJson(listenerStatusJson(status, permissionMode));
44152
+ printJson(listenerStatusJson(status, permissionMode, attendanceEvidence));
43310
44153
  return;
43311
44154
  }
43312
44155
  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.
@@ -43318,8 +44161,8 @@ async function runListenStart(args) {
43318
44161
  ` : `The Grok worker uses your selected cwd and local Grok configuration, including user and cmux hooks. ${workerAudience}
43319
44162
  `;
43320
44163
  process.stdout.write(
43321
- `${args.has("foreground") ? "Listener stopped." : "Listener is ready and will keep receiving after this command exits."}
43322
- ${renderListenerStatus(status)}
44164
+ `${args.has("foreground") ? "Listener stopped." : (status.pendingForMainCount ?? 0) > 0 ? "Listener transport is connected, but queued messages are unattended." : "Listener is ready and will keep receiving after this command exits."}
44165
+ ${renderListenerStatus(status, attendanceEvidence)}
43323
44166
  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.
43324
44167
  The short credential rotates while this process remains alive and secure local state is available. Run cswarm whoami with this credential to see whether its grant is timeboxed or standing.
43325
44168
  ` + routingNote + hostNote + `Use listen status/stop with the same agent credential, --workspace-id ${workspaceId2}, and the same Cloud target. --principal-id ${principalId} remains available when no credential is supplied.
@@ -43438,6 +44281,11 @@ async function runListenStatusOrStop(args, command2) {
43438
44281
  }
43439
44282
  return;
43440
44283
  }
44284
+ let attendanceEvidence = {
44285
+ pendingForMainOldestAt: null,
44286
+ hookSurfaceExists: false,
44287
+ hookSurfaceAdvanced: false
44288
+ };
43441
44289
  if ((status.routeMode ?? "worker") !== "worker") {
43442
44290
  const recordedPending = status.pendingForMainCount ?? 0;
43443
44291
  const recordedDropped = status.droppedForMainCount ?? 0;
@@ -43450,14 +44298,76 @@ async function runListenStatusOrStop(args, command2) {
43450
44298
  pendingForMainCount: queueStats.count,
43451
44299
  droppedForMainCount: queueStats.droppedCount
43452
44300
  };
44301
+ attendanceEvidence = {
44302
+ pendingForMainOldestAt: queueStats.oldestAt,
44303
+ hookSurfaceExists: queueStats.hookSurfaceExists,
44304
+ hookSurfaceAdvanced: queueStats.hookSurfaceAdvanced
44305
+ };
43453
44306
  }
43454
44307
  if (args.has("json")) {
43455
- printJson(listenerStatusJson(status));
44308
+ printJson(listenerStatusJson(status, void 0, attendanceEvidence));
43456
44309
  } else {
43457
- process.stdout.write(`${renderListenerStatus(status)}
44310
+ process.stdout.write(`${renderListenerStatus(status, attendanceEvidence)}
43458
44311
  `);
43459
44312
  }
43460
44313
  }
44314
+ async function runListenCanary(args) {
44315
+ args.assertShape([
44316
+ ...TARGET_FLAGS,
44317
+ ...CREDENTIAL_FLAGS,
44318
+ "workspace-id",
44319
+ "state-dir",
44320
+ "wait",
44321
+ "json"
44322
+ ], 2);
44323
+ if (!hasAgentCredential(args)) {
44324
+ throw new Error(
44325
+ "listen canary requires --agent-token-file or --agent-token-stdin; credentials are never accepted on argv"
44326
+ );
44327
+ }
44328
+ const cloud = await target(args);
44329
+ const workspaceId2 = listenerUuid(
44330
+ args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
44331
+ "workspace-id"
44332
+ );
44333
+ const agent = await agentCredential(args);
44334
+ if (agent.principalId === null) {
44335
+ throw new Error(
44336
+ "listen canary needs the complete JSON agent credential so it can address the agent and select its listener state"
44337
+ );
44338
+ }
44339
+ const principalId = listenerUuid(agent.principalId, "principal-id");
44340
+ const stateDirectory2 = listenerStateDirectory(args);
44341
+ const paths = listenerPaths({
44342
+ profileId: cloud.profileId,
44343
+ workspaceId: workspaceId2,
44344
+ principalId,
44345
+ ...stateDirectory2 ? { stateDirectory: stateDirectory2 } : {}
44346
+ });
44347
+ const waitMs = parseWaitSeconds(args.optional("wait") ?? "10") * 1e3;
44348
+ const result = await runListenerAttendanceCanary({
44349
+ target: cloud,
44350
+ workspaceId: workspaceId2,
44351
+ principalId,
44352
+ paths,
44353
+ waitMs,
44354
+ // Canary must remain read-only apart from its one self-note. It therefore
44355
+ // uses the presented token and never enters the renewal/mint path.
44356
+ credential: async () => agent.token
44357
+ });
44358
+ if (args.has("json")) {
44359
+ printJson({
44360
+ workspaceId: workspaceId2,
44361
+ principalId,
44362
+ ...result
44363
+ });
44364
+ return;
44365
+ }
44366
+ process.stdout.write(
44367
+ `${renderListenerAttendanceCanary(result, workspaceId2, principalId)}
44368
+ `
44369
+ );
44370
+ }
43461
44371
  async function runListen(args) {
43462
44372
  const command2 = args.positionals[1];
43463
44373
  if (command2 === "start") {
@@ -43468,7 +44378,11 @@ async function runListen(args) {
43468
44378
  await runListenStatusOrStop(args, command2);
43469
44379
  return;
43470
44380
  }
43471
- throw new UsageError("listen requires start, status, or stop");
44381
+ if (command2 === "canary") {
44382
+ await runListenCanary(args);
44383
+ return;
44384
+ }
44385
+ throw new UsageError("listen requires start, status, stop, or canary");
43472
44386
  }
43473
44387
  var CLAUDE_HOOK_COMMAND = "cswarm hook check";
43474
44388
  function scopedClaudeHookCommand(principalId) {
@@ -43477,6 +44391,46 @@ function scopedClaudeHookCommand(principalId) {
43477
44391
  function isCommonSwarmClaudeHook(value) {
43478
44392
  return typeof value === "string" && (value === CLAUDE_HOOK_COMMAND || /^cswarm hook check --principal-id [0-9a-f-]{36}$/.test(value));
43479
44393
  }
44394
+ var ListenerUnattendedRefusedError = class extends Error {
44395
+ code = "listen_unattended_refused";
44396
+ constructor(principalId) {
44397
+ super(
44398
+ `listen_unattended_refused: --route main and --route split need an attendance surface for agent ${principalId}. Next: cswarm hook install claude --principal-id ${principalId} --write, then start a fresh session. Or use --route worker. Use --allow-unattended only when you accept a queue that may not wake a session.`
44399
+ );
44400
+ this.name = "ListenerUnattendedRefusedError";
44401
+ }
44402
+ };
44403
+ function settingsHaveScopedClaudeHook(settings, principalId) {
44404
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks)) {
44405
+ return false;
44406
+ }
44407
+ const promptHooks = settings.hooks.UserPromptSubmit;
44408
+ if (!Array.isArray(promptHooks)) return false;
44409
+ const expected = scopedClaudeHookCommand(principalId);
44410
+ return promptHooks.some((group) => {
44411
+ if (!group || typeof group !== "object" || Array.isArray(group)) return false;
44412
+ const hooks = group.hooks;
44413
+ return Array.isArray(hooks) && hooks.some(
44414
+ (hook) => hook !== null && typeof hook === "object" && !Array.isArray(hook) && hook.type === "command" && hook.command === expected
44415
+ );
44416
+ });
44417
+ }
44418
+ async function listenerHasAttendanceSurface(instanceDirectory, cwd, principalId) {
44419
+ const surface = await new FileHookSurfaceStore(instanceDirectory).evidence();
44420
+ if (surface.exists) return true;
44421
+ const repositoryRoot = gitRepositoryRoot(cwd) ?? cwd;
44422
+ const settingsPaths = /* @__PURE__ */ new Set([
44423
+ (0, import_node_path21.join)(repositoryRoot, CLAUDE_PROJECT_SETTINGS_IGNORE_LINE),
44424
+ (0, import_node_path21.join)(repositoryRoot, CLAUDE_REPO_SETTINGS_IGNORE_LINE),
44425
+ userClaudeSettingsTarget().path
44426
+ ]);
44427
+ for (const path of settingsPaths) {
44428
+ if (settingsHaveScopedClaudeHook(readClaudeSettings(path), principalId)) {
44429
+ return true;
44430
+ }
44431
+ }
44432
+ return false;
44433
+ }
43480
44434
  function claudeUserPromptHookSnippet(principalId) {
43481
44435
  return {
43482
44436
  hooks: {
@@ -43495,7 +44449,9 @@ function claudeUserPromptHookSnippet(principalId) {
43495
44449
  }
43496
44450
  var CLAUDE_PROJECT_SETTINGS_IGNORE_LINE = ".claude/settings.local.json";
43497
44451
  var CLAUDE_REPO_SETTINGS_IGNORE_LINE = ".claude/settings.json";
43498
- var CLAUDE_USER_SCOPE_WARNING = "Warning: --user scope affects EVERY Claude Code session for this OS user and is wrong on a shared host.";
44452
+ function claudeUserScopeWarning(settingsPath) {
44453
+ return `Warning: --user scope writes settings to ${(0, import_node_path21.dirname)(settingsPath)} and applies to every Claude Code session that reads that directory.`;
44454
+ }
43499
44455
  function userClaudeSettingsTarget() {
43500
44456
  const configured = process.env.CLAUDE_CONFIG_DIR;
43501
44457
  const directory = configured && configured.length > 0 ? (0, import_node_path21.resolve)(configured) : (0, import_node_path21.join)((0, import_node_os10.homedir)(), ".claude");
@@ -43506,7 +44462,7 @@ function userClaudeSettingsTarget() {
43506
44462
  };
43507
44463
  }
43508
44464
  function gitRepositoryRoot(cwd) {
43509
- const result = (0, import_node_child_process8.spawnSync)(
44465
+ const result = (0, import_node_child_process9.spawnSync)(
43510
44466
  "git",
43511
44467
  ["-C", cwd, "rev-parse", "--show-toplevel"],
43512
44468
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -43526,12 +44482,12 @@ function projectClaudeSettingsTarget(scope, ignoreLine) {
43526
44482
  const base = root ?? process.cwd();
43527
44483
  const path = (0, import_node_path21.join)(base, ignoreLine);
43528
44484
  if (root === null) return { path, scope, projectRoot: base };
43529
- const tracked = (0, import_node_child_process8.spawnSync)(
44485
+ const tracked = (0, import_node_child_process9.spawnSync)(
43530
44486
  "git",
43531
44487
  ["-C", root, "ls-files", "--error-unmatch", "--", ignoreLine],
43532
44488
  { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }
43533
44489
  );
43534
- const ignored = (0, import_node_child_process8.spawnSync)(
44490
+ const ignored = (0, import_node_child_process9.spawnSync)(
43535
44491
  "git",
43536
44492
  ["-C", root, "check-ignore", "--quiet", "--", ignoreLine],
43537
44493
  { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }
@@ -43713,7 +44669,7 @@ async function runHook(args) {
43713
44669
  const settings = readClaudeSettings(path);
43714
44670
  const updated = command2 === "install" ? installClaudeHook(settings, principalId) : uninstallClaudeHook(settings);
43715
44671
  if (target2.scope === "user") {
43716
- process.stdout.write(`${CLAUDE_USER_SCOPE_WARNING}
44672
+ process.stdout.write(`${claudeUserScopeWarning(path)}
43717
44673
  `);
43718
44674
  }
43719
44675
  (0, import_node_fs7.mkdirSync)((0, import_node_path21.dirname)(path), { recursive: true });
@@ -44280,7 +45236,7 @@ async function runSeed(args) {
44280
45236
  if (!tokenOut || !(0, import_node_path21.isAbsolute)(tokenOut)) {
44281
45237
  throw new Error("SEED_TOKEN_OUT must be an absolute path");
44282
45238
  }
44283
- const tokenFile = await (0, import_promises11.open)(tokenOut, "wx", 384).catch((error) => {
45239
+ const tokenFile = await (0, import_promises12.open)(tokenOut, "wx", 384).catch((error) => {
44284
45240
  if (error.code === "EEXIST") {
44285
45241
  throw new Error("SEED_TOKEN_OUT already exists; refusing to overwrite it");
44286
45242
  }
@@ -44319,7 +45275,7 @@ async function runSeed(args) {
44319
45275
  tokenWritten = true;
44320
45276
  }
44321
45277
  await tokenFile.close();
44322
- if (!tokenWritten) await (0, import_promises11.unlink)(tokenOut);
45278
+ if (!tokenWritten) await (0, import_promises12.unlink)(tokenOut);
44323
45279
  process.stdout.write(`${JSON.stringify({
44324
45280
  userId: result.userId,
44325
45281
  membershipRole: result.membershipRole,
@@ -44332,7 +45288,7 @@ async function runSeed(args) {
44332
45288
  `);
44333
45289
  } catch (error) {
44334
45290
  await tokenFile.close().catch(() => void 0);
44335
- if (!tokenWritten) await (0, import_promises11.unlink)(tokenOut).catch(() => void 0);
45291
+ if (!tokenWritten) await (0, import_promises12.unlink)(tokenOut).catch(() => void 0);
44336
45292
  throw error;
44337
45293
  }
44338
45294
  }
@@ -44433,6 +45389,10 @@ async function main() {
44433
45389
  await runWhoami(args);
44434
45390
  return;
44435
45391
  }
45392
+ if (verb === "resume") {
45393
+ await runResume(args);
45394
+ return;
45395
+ }
44436
45396
  if (verb === "feedback") {
44437
45397
  await runFeedback(args);
44438
45398
  return;
@@ -44518,6 +45478,7 @@ function markRestartable(error) {
44518
45478
  return error;
44519
45479
  }
44520
45480
  function exitCodeFor(error) {
45481
+ if (error instanceof NotifyStdoutClosedError) return EXIT_NOTIFY_ORPHANED;
44521
45482
  return error instanceof Error ? restartableExit.get(error) ?? 1 : 1;
44522
45483
  }
44523
45484
  function safeParagraph(message) {
@@ -44574,6 +45535,7 @@ ${usage()}
44574
45535
  // Annotate the CommonJS export names for ESM import in node:
44575
45536
  0 && (module.exports = {
44576
45537
  EXIT_RESTARTABLE,
45538
+ ListenerUnattendedRefusedError,
44577
45539
  TURN_BUDGET_CREDENTIAL_MARGIN_MS,
44578
45540
  clampTurnBudgetToCredential,
44579
45541
  claudeUserPromptHookSnippet,