commonswarm 0.1.44 → 0.1.46

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 +2716 -302
  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,
@@ -13510,6 +13511,7 @@ __export(cli_exports, {
13510
13511
  listenerFailureMessage: () => listenerFailureMessage,
13511
13512
  listenerHostLimits: () => listenerHostLimits,
13512
13513
  listenerPermissionMode: () => listenerPermissionMode,
13514
+ listenerProviderInstallEvidence: () => listenerProviderInstallEvidence,
13513
13515
  listenerRouteConfiguration: () => listenerRouteConfiguration,
13514
13516
  listenerStatusJson: () => listenerStatusJson,
13515
13517
  renderListenerStatus: () => renderListenerStatus,
@@ -13520,13 +13522,13 @@ __export(cli_exports, {
13520
13522
  resolveTurnBudgetOrDefer: () => resolveTurnBudgetOrDefer
13521
13523
  });
13522
13524
  module.exports = __toCommonJS(cli_exports);
13523
- var import_node_crypto20 = require("node:crypto");
13524
- var import_node_child_process8 = require("node:child_process");
13525
+ var import_node_crypto21 = require("node:crypto");
13526
+ var import_node_child_process9 = require("node:child_process");
13525
13527
  var import_node_fs7 = require("node:fs");
13526
- var import_promises11 = require("node:fs/promises");
13528
+ var import_promises12 = require("node:fs/promises");
13527
13529
  var import_node_os10 = require("node:os");
13528
13530
  var import_node_path21 = require("node:path");
13529
- var import_promises12 = require("node:readline/promises");
13531
+ var import_promises13 = require("node:readline/promises");
13530
13532
 
13531
13533
  // src/cloud/auth.ts
13532
13534
  var import_node_crypto2 = require("node:crypto");
@@ -22900,9 +22902,9 @@ var FileCommandRefused = class extends Error {
22900
22902
  };
22901
22903
  var FileTransportError = class extends Error {
22902
22904
  /**
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.
22905
+ * True when the request did not complete: no response arrived, or an
22906
+ * idempotent read's body stalled. Reads may retry; writes reuse the same ids
22907
+ * because their outcome is unknown. A received refusal is never retried.
22906
22908
  */
22907
22909
  constructor(message, noResponse = false) {
22908
22910
  super(message);
@@ -23050,7 +23052,7 @@ async function getObject(target2, downloadPath, fetcher = fetch, options = {}) {
23050
23052
  options
23051
23053
  ));
23052
23054
  } catch {
23053
- throw new FileTransportError("the download failed before a response", true);
23055
+ throw new FileTransportError("the download did not complete", true);
23054
23056
  }
23055
23057
  if (!response.ok || body === null) {
23056
23058
  throw new FileTransportError(
@@ -23131,7 +23133,7 @@ async function listFilesAsAgent(target2, credential, workspaceId2, fetcher = fet
23131
23133
  options
23132
23134
  ));
23133
23135
  } catch {
23134
- throw new FileTransportError("file list could not reach the cloud service", true);
23136
+ throw new FileTransportError("the file list did not complete", true);
23135
23137
  }
23136
23138
  if (!response.ok) {
23137
23139
  throw new FileCommandRefused(
@@ -23176,7 +23178,7 @@ async function listFilesAsHuman(target2, accessToken, workspaceId2, fetcher = fe
23176
23178
  options
23177
23179
  ));
23178
23180
  } catch {
23179
- throw new FileTransportError("file list could not reach the cloud service", true);
23181
+ throw new FileTransportError("the file list did not complete", true);
23180
23182
  }
23181
23183
  if (!response.ok) {
23182
23184
  throw new FileCommandRefused(
@@ -23366,8 +23368,7 @@ function assertOwnedByCurrentUser(uid2) {
23366
23368
  throw new Error("credential path is not owned by the current user");
23367
23369
  }
23368
23370
  }
23369
- async function secureDirectory(path) {
23370
- let created = false;
23371
+ async function existingSecureDirectory(path) {
23371
23372
  try {
23372
23373
  const info = await (0, import_promises.lstat)(path);
23373
23374
  if (!info.isDirectory() || info.isSymbolicLink()) {
@@ -23380,20 +23381,22 @@ async function secureDirectory(path) {
23380
23381
  );
23381
23382
  }
23382
23383
  } 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;
23384
+ if (error.code === "ENOENT") return false;
23385
+ throw error;
23387
23386
  }
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
- }
23387
+ return true;
23388
+ }
23389
+ async function secureDirectory(path) {
23390
+ if (await existingSecureDirectory(path)) return;
23391
+ await (0, import_promises.mkdir)(path, { recursive: true, mode: 448 });
23392
+ await (0, import_promises.chmod)(path, 448);
23393
+ const info = await (0, import_promises.lstat)(path);
23394
+ if (!info.isDirectory() || info.isSymbolicLink()) {
23395
+ throw new Error(`credential directory is not a real directory: ${path}`);
23396
+ }
23397
+ assertOwnedByCurrentUser(info.uid);
23398
+ if (mode(info.mode) !== 448) {
23399
+ throw new Error(`credential directory could not be secured to mode 0700: ${path}`);
23397
23400
  }
23398
23401
  }
23399
23402
  async function ensureSecureStateDirectory(path) {
@@ -23555,6 +23558,20 @@ async function readSecureJsonFile(path, maxBytes) {
23555
23558
  throw error;
23556
23559
  }
23557
23560
  }
23561
+ async function readSecureJsonFileIfPresent(path, maxBytes) {
23562
+ if (!await existingSecureDirectory((0, import_node_path.dirname)(path))) return null;
23563
+ try {
23564
+ await secureCredentialFile(path);
23565
+ const raw = await (0, import_promises.readFile)(path, "utf8");
23566
+ if (Buffer.byteLength(raw, "utf8") > maxBytes) {
23567
+ throw new Error("stored record is larger than this store accepts");
23568
+ }
23569
+ return raw;
23570
+ } catch (error) {
23571
+ if (error.code === "ENOENT") return null;
23572
+ throw error;
23573
+ }
23574
+ }
23558
23575
  async function deleteSecureJsonFile(path) {
23559
23576
  await secureDirectory((0, import_node_path.dirname)(path));
23560
23577
  try {
@@ -23832,8 +23849,7 @@ function parseStoredCurrentTarget(raw) {
23832
23849
  throw new Error("stored current target is malformed");
23833
23850
  }
23834
23851
  const record = value;
23835
- const keys = Object.keys(record).sort();
23836
- if (keys.length !== 3 || keys[0] !== "anonKey" || keys[1] !== "url" || keys[2] !== "version" || record.version !== 1 || typeof record.url !== "string" || typeof record.anonKey !== "string") {
23852
+ if (record.version !== 1 || typeof record.url !== "string" || typeof record.anonKey !== "string") {
23837
23853
  throw new Error("stored current target is malformed");
23838
23854
  }
23839
23855
  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();
@@ -28734,10 +28750,19 @@ var SIGNAL_BODY_DISPLAY_MAX = 8e3;
28734
28750
  var SIGNAL_ABOUT_DISPLAY_MAX = 500;
28735
28751
  var SIGNAL_READ_TIMEOUT_MS = 3e4;
28736
28752
  var SignalReadTimeoutError = class extends Error {
28737
- constructor(message = "signal read timed out") {
28753
+ constructor(message = "signal read timed out", phase = "response") {
28738
28754
  super(message);
28755
+ this.phase = phase;
28739
28756
  this.name = "SignalReadTimeoutError";
28740
28757
  }
28758
+ phase;
28759
+ };
28760
+ var SignalHostPortsExhaustedError = class extends Error {
28761
+ code = "EADDRNOTAVAIL";
28762
+ constructor() {
28763
+ super("the host could not allocate an outbound source port");
28764
+ this.name = "SignalHostPortsExhaustedError";
28765
+ }
28741
28766
  };
28742
28767
  var SIGNAL_WAIT_MIN_SECONDS = 1;
28743
28768
  var SIGNAL_WAIT_MAX_SECONDS = 300;
@@ -28776,9 +28801,17 @@ var plainHttpRetryAfterMs = /* @__PURE__ */ new WeakMap();
28776
28801
  var plainHttpStatus = /* @__PURE__ */ new WeakMap();
28777
28802
  var plainHttpEnvelope = /* @__PURE__ */ new WeakMap();
28778
28803
  var plainTransportErrors = /* @__PURE__ */ new WeakSet();
28779
- function plainTransportError() {
28804
+ var plainTransportFailureCodes = /* @__PURE__ */ new WeakMap();
28805
+ var plainMalformedErrors = /* @__PURE__ */ new WeakSet();
28806
+ function plainTransportError(failureCode2 = "no_response") {
28780
28807
  const error = new Error("signal read could not reach the cloud service");
28781
28808
  plainTransportErrors.add(error);
28809
+ plainTransportFailureCodes.set(error, failureCode2);
28810
+ return error;
28811
+ }
28812
+ function plainMalformedError(message) {
28813
+ const error = new Error(message);
28814
+ plainMalformedErrors.add(error);
28782
28815
  return error;
28783
28816
  }
28784
28817
  function checkedUuid2(value, field) {
@@ -28983,7 +29016,62 @@ function followErrorEnvelope(error) {
28983
29016
  return EMPTY_SERVER_ERROR_ENVELOPE;
28984
29017
  }
28985
29018
  function isTransportFollowMessage(error) {
28986
- return error instanceof SignalTransportError || error instanceof Error && plainTransportErrors.has(error);
29019
+ return error instanceof SignalTransportError || error instanceof SignalHostPortsExhaustedError || error instanceof Error && plainTransportErrors.has(error);
29020
+ }
29021
+ function safeConstructorName(error) {
29022
+ if (error === null || typeof error !== "object") return typeof error;
29023
+ const name = error.constructor?.name;
29024
+ if (typeof name !== "string" || name.length === 0) return "Unknown";
29025
+ return name.replace(/[^A-Za-z0-9_$-]+/g, "_").slice(0, 96) || "Unknown";
29026
+ }
29027
+ function classifySignalReadFailure(error) {
29028
+ if (error instanceof SignalHostPortsExhaustedError || error !== null && typeof error === "object" && error.code === "EADDRNOTAVAIL") {
29029
+ return {
29030
+ code: "host_ports_exhausted",
29031
+ httpStatus: null,
29032
+ errorConstructor: null
29033
+ };
29034
+ }
29035
+ const http = followHttpDetails(error);
29036
+ if (http !== null) {
29037
+ return {
29038
+ code: "http_status",
29039
+ httpStatus: http.status,
29040
+ errorConstructor: null
29041
+ };
29042
+ }
29043
+ if (error instanceof SignalReadTimeoutError) {
29044
+ return {
29045
+ code: error.phase === "body" ? "body_timeout" : "no_response",
29046
+ httpStatus: null,
29047
+ errorConstructor: null
29048
+ };
29049
+ }
29050
+ if (error instanceof Error && plainTransportErrors.has(error)) {
29051
+ return {
29052
+ code: plainTransportFailureCodes.get(error) ?? "no_response",
29053
+ httpStatus: null,
29054
+ errorConstructor: null
29055
+ };
29056
+ }
29057
+ if (error instanceof SignalTransportError) {
29058
+ return { code: "no_response", httpStatus: null, errorConstructor: null };
29059
+ }
29060
+ if (error instanceof SignalMalformedError || error instanceof Error && plainMalformedErrors.has(error)) {
29061
+ return {
29062
+ code: "malformed_response",
29063
+ httpStatus: null,
29064
+ errorConstructor: null
29065
+ };
29066
+ }
29067
+ if (error instanceof Error && error.name === "AbortError") {
29068
+ return { code: "aborted", httpStatus: null, errorConstructor: null };
29069
+ }
29070
+ return {
29071
+ code: "unclassified",
29072
+ httpStatus: null,
29073
+ errorConstructor: safeConstructorName(error)
29074
+ };
28987
29075
  }
28988
29076
  function isRestartableReadError(error) {
28989
29077
  if (error instanceof SignalReadTimeoutError) return true;
@@ -29035,6 +29123,7 @@ async function fetchSignalRead(fetcher, input, init, timeoutMs = SIGNAL_READ_TIM
29035
29123
  }
29036
29124
  const deadlineController = new AbortController();
29037
29125
  let timedOut = false;
29126
+ let responseReceived = false;
29038
29127
  const signal = init.signal ? AbortSignal.any([init.signal, deadlineController.signal]) : deadlineController.signal;
29039
29128
  let onAbort = () => {
29040
29129
  };
@@ -29062,22 +29151,31 @@ async function fetchSignalRead(fetcher, input, init, timeoutMs = SIGNAL_READ_TIM
29062
29151
  signal
29063
29152
  });
29064
29153
  } catch (error) {
29065
- if (signal.aborted || timedOut || error?.name === "AbortError") {
29154
+ if (signal.aborted || timedOut) {
29066
29155
  return "timeout";
29067
29156
  }
29157
+ if (error instanceof Error && error.name === "AbortError") throw error;
29158
+ if (error !== null && typeof error === "object" && error.code === "EADDRNOTAVAIL") {
29159
+ throw new SignalHostPortsExhaustedError();
29160
+ }
29068
29161
  return null;
29069
29162
  }
29163
+ responseReceived = true;
29070
29164
  if (signal.aborted || timedOut) return "timeout";
29071
29165
  try {
29072
29166
  return { response, body: await response.json() };
29073
- } catch {
29167
+ } catch (error) {
29074
29168
  if (signal.aborted || timedOut) return "timeout";
29169
+ if (error instanceof Error && error.name === "AbortError") throw error;
29075
29170
  return { response, body: null };
29076
29171
  }
29077
29172
  })();
29078
29173
  const raced = await Promise.race([read, aborted]);
29079
29174
  if (raced === "timeout") {
29080
- throw new SignalReadTimeoutError();
29175
+ throw new SignalReadTimeoutError(
29176
+ "signal read timed out",
29177
+ responseReceived ? "body" : "response"
29178
+ );
29081
29179
  }
29082
29180
  return raced;
29083
29181
  } finally {
@@ -29106,7 +29204,9 @@ async function fetchSignalReadRetrying(fetcher, input, init, timeoutMs = SIGNAL_
29106
29204
  function mapReadFailure(error, waitBound) {
29107
29205
  if (error instanceof SignalReadTimeoutError) {
29108
29206
  if (waitBound) throw error;
29109
- throw plainTransportError();
29207
+ throw plainTransportError(
29208
+ error.phase === "body" ? "body_timeout" : "no_response"
29209
+ );
29110
29210
  }
29111
29211
  throw error;
29112
29212
  }
@@ -29162,7 +29262,7 @@ async function humanSignals(target2, credential, query, options) {
29162
29262
  throwSignalHttp(response, body);
29163
29263
  }
29164
29264
  if (!Array.isArray(body)) {
29165
- throw new Error("signal read returned malformed JSON");
29265
+ throw plainMalformedError("signal read returned malformed JSON");
29166
29266
  }
29167
29267
  const parsed = body.map((value) => parseSignalRecord(value));
29168
29268
  return sortSignals(rowsAfterCursor(parsed, query.after), ascending);
@@ -29230,7 +29330,7 @@ async function agentSignalPage(target2, credential, query, options, allowLegacyC
29230
29330
  const { response, body } = result;
29231
29331
  if (!response.ok) throwSignalHttp(response, body);
29232
29332
  if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.signals)) {
29233
- throw new Error("signal read returned malformed JSON");
29333
+ throw plainMalformedError("signal read returned malformed JSON");
29234
29334
  }
29235
29335
  const capabilities = signalReadCapabilities(
29236
29336
  body.capabilities
@@ -29720,6 +29820,7 @@ function resolveRefusalToleranceMs(raw, warn = () => {
29720
29820
  }
29721
29821
  function isRetryableFollowError(error) {
29722
29822
  if (serverRefusedRetry(followErrorEnvelope(error))) return false;
29823
+ if (error instanceof SignalHostPortsExhaustedError) return true;
29723
29824
  if (error instanceof SignalReadTimeoutError) return true;
29724
29825
  if (isTransportFollowMessage(error)) return true;
29725
29826
  const http = followHttpDetails(error);
@@ -29948,6 +30049,16 @@ var CURSOR_MAX_BYTES = 4 * 1024;
29948
30049
  var ARRIVAL_SNIPPET_MAX = 180;
29949
30050
  var ARRIVAL_WATCH_POLL_MS = 25e3;
29950
30051
  var ARRIVAL_RETRY_NOTICE_THRESHOLD_MS = 6e4;
30052
+ var EXIT_NOTIFY_ORPHANED = 74;
30053
+ var NotifyStdoutClosedError = class extends Error {
30054
+ name = "NotifyStdoutClosedError";
30055
+ code = "notify_stdout_closed";
30056
+ constructor() {
30057
+ super(
30058
+ "[notify_stdout_closed] inbox --notify lost its stdout reader and stopped before advancing its cursor. Start one fresh watcher under a live Monitor."
30059
+ );
30060
+ }
30061
+ };
29951
30062
  function createArrivalRetryNoticePolicy(thresholdMs = ARRIVAL_RETRY_NOTICE_THRESHOLD_MS) {
29952
30063
  let firstFailureAt = null;
29953
30064
  let failureEmitted = false;
@@ -30000,9 +30111,8 @@ function parseCursor(raw, workspaceId2, principalId) {
30000
30111
  throw new Error("stored arrival cursor is malformed");
30001
30112
  }
30002
30113
  const row = value;
30003
- const keys = Object.keys(row).sort();
30004
30114
  const cursor = row.cursor;
30005
- if (keys.join(",") !== "cursor,principal_id,version,workspace_id" || row.version !== 1 || row.workspace_id !== workspaceId2.toLowerCase() || row.principal_id !== principalId.toLowerCase() || !(cursor === null || typeof cursor === "object" && !Array.isArray(cursor) && Object.keys(cursor).sort().join(",") === "created_at,id" && typeof cursor.created_at === "string" && Number.isFinite(Date.parse(cursor.created_at)) && typeof cursor.id === "string" && UUID_RE10.test(cursor.id))) {
30115
+ if (row.version !== 1 || row.workspace_id !== workspaceId2.toLowerCase() || row.principal_id !== principalId.toLowerCase() || !(cursor === null || typeof cursor === "object" && !Array.isArray(cursor) && typeof cursor.created_at === "string" && Number.isFinite(Date.parse(cursor.created_at)) && typeof cursor.id === "string" && UUID_RE10.test(cursor.id))) {
30006
30116
  throw new Error("stored arrival cursor is malformed");
30007
30117
  }
30008
30118
  if (cursor === null) return null;
@@ -30065,6 +30175,33 @@ function formatArrivalNotification(notification) {
30065
30175
  const attachmentCopy = notification.attachment_count === 0 ? "" : ` \u2014 ${notification.attachment_count} attachment${notification.attachment_count === 1 ? "" : "s"}`;
30066
30176
  return `CommonSwarm from ${notification.sender_kind} ${notification.sender}: ${notification.snippet}${attachmentCopy} \u2014 reply: ${notification.reply_command}`;
30067
30177
  }
30178
+ function notifyWriteError(error) {
30179
+ return error.code === "EPIPE" ? new NotifyStdoutClosedError() : error;
30180
+ }
30181
+ async function writeArrivalMonitorLine(line, stream2 = process.stdout) {
30182
+ await new Promise((resolve2, reject) => {
30183
+ let settled = false;
30184
+ const finish = (error) => {
30185
+ if (settled) return;
30186
+ settled = true;
30187
+ if (error) {
30188
+ setImmediate(() => stream2.off("error", onError));
30189
+ reject(notifyWriteError(error));
30190
+ } else {
30191
+ stream2.off("error", onError);
30192
+ resolve2();
30193
+ }
30194
+ };
30195
+ const onError = (error) => finish(error);
30196
+ stream2.once("error", onError);
30197
+ try {
30198
+ stream2.write(`${line}
30199
+ `, (error) => finish(error));
30200
+ } catch (error) {
30201
+ finish(error instanceof Error ? error : new Error(String(error)));
30202
+ }
30203
+ });
30204
+ }
30068
30205
  function cursorOf(signal) {
30069
30206
  return { created_at: signal.created_at, id: signal.id };
30070
30207
  }
@@ -30275,7 +30412,11 @@ function parseDeliveryReceipt(value) {
30275
30412
  "protocol",
30276
30413
  "delivery receipt returned a malformed last_error_code"
30277
30414
  );
30278
- })()
30415
+ })(),
30416
+ pending_for_main_count: Object.hasOwn(row, "pending_for_main_count") ? row.pending_for_main_count === null ? null : nonNegativeInteger(
30417
+ row.pending_for_main_count,
30418
+ "pending_for_main_count"
30419
+ ) : null
30279
30420
  };
30280
30421
  }
30281
30422
  function parseUntrackedAgent(value) {
@@ -30629,9 +30770,10 @@ function renderSignalReceiptReport(report, nowMs = Date.now()) {
30629
30770
  ].join("\n");
30630
30771
  }
30631
30772
  if (state === "queued") {
30773
+ const queueCount = receipt.pending_for_main_count;
30632
30774
  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.",
30775
+ `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)` : ""}.`,
30776
+ `Ask the agent's operator to check its listener with: ${listenerStatusCommand(report, receipt)}`,
30635
30777
  `Check again with: ${receiptCheckCommand(report)}`
30636
30778
  ].join("\n");
30637
30779
  }
@@ -30686,7 +30828,8 @@ function signalReceiptJsonPayload(report, nowMs = Date.now()) {
30686
30828
  acked_at: receipt.acked_at,
30687
30829
  attempt_count: receipt.attempt_count,
30688
30830
  lease_expiry_count: receipt.lease_expiry_count,
30689
- last_error_code: receipt.last_error_code
30831
+ last_error_code: receipt.last_error_code,
30832
+ pending_for_main_count: receipt.pending_for_main_count ?? null
30690
30833
  }
30691
30834
  )
30692
30835
  };
@@ -30696,11 +30839,7 @@ function signalReceiptJsonPayload(report, nowMs = Date.now()) {
30696
30839
  var import_node_child_process3 = require("node:child_process");
30697
30840
  var import_node_crypto12 = require("node:crypto");
30698
30841
 
30699
- // src/host/stderr-tail.ts
30700
- var RING_CAPACITY_BYTES = 4096;
30701
- var TAIL_MAX_CHARS = 2048;
30702
- var STDERR_EXIT_GRACE_MS = 100;
30703
- var STDERR_READABLE_END_GRACE_MS = STDERR_EXIT_GRACE_MS + 50;
30842
+ // src/host/credential-redaction.ts
30704
30843
  var EXOTIC_SEPARATORS = "\\u00a0\\u1680\\u2000-\\u200d\\u2028\\u2029\\u202a-\\u202e\\u2060\\u2066-\\u2069\\u202f\\u205f\\u3000\\ufeff";
30705
30844
  var SEPARATOR_CLASS_SOURCE = "\\t\\n\\x0b\\f\\r " + EXOTIC_SEPARATORS;
30706
30845
  var ANSI_ESCAPE_GLOBAL_RE2 = new RegExp("\\u001b\\[[0-?]*[ -\\/]*[@-~]", "g");
@@ -30712,8 +30851,17 @@ var CREDENTIAL_PREFIX_RE = new RegExp(
30712
30851
  `swm_(?:agt|inv|cap)_[^${SEPARATOR_CLASS_SOURCE}]*`,
30713
30852
  "gi"
30714
30853
  );
30854
+ function redactCredentialText(value) {
30855
+ return value.replace(ANSI_ESCAPE_GLOBAL_RE2, "").replace(CONTROL_AND_SEPARATOR_STRIP_RE, "").replace(CREDENTIAL_PREFIX_RE, "[redacted-credential]");
30856
+ }
30857
+
30858
+ // src/host/stderr-tail.ts
30859
+ var RING_CAPACITY_BYTES = 4096;
30860
+ var TAIL_MAX_CHARS = 2048;
30861
+ var STDERR_EXIT_GRACE_MS = 100;
30862
+ var STDERR_READABLE_END_GRACE_MS = STDERR_EXIT_GRACE_MS + 50;
30715
30863
  function sanitizeStderrTail(raw) {
30716
- return raw.replace(ANSI_ESCAPE_GLOBAL_RE2, "").replace(CONTROL_AND_SEPARATOR_STRIP_RE, "").replace(CREDENTIAL_PREFIX_RE, "[redacted-credential]").slice(-TAIL_MAX_CHARS).trim();
30864
+ return redactCredentialText(raw).slice(-TAIL_MAX_CHARS).trim();
30717
30865
  }
30718
30866
  function attachStderrTailRing(stderr) {
30719
30867
  const chunks = [];
@@ -30799,7 +30947,7 @@ var CLAUDE_ACP_MIN_VERSION = "0.64.2";
30799
30947
  var CLAUDE_ACP_LAST_MEASURED_VERSION = "0.64.2";
30800
30948
  var CLAUDE_PERMISSION_MODE_ID = "default";
30801
30949
  var CODEX_ACP_MIN_VERSION = "1.1.9";
30802
- var CODEX_ACP_LAST_MEASURED_VERSION = "1.1.9";
30950
+ var CODEX_ACP_LAST_MEASURED_VERSION = "1.8.0";
30803
30951
  var CODEX_PERMISSION_MODE_ID = "read-only";
30804
30952
  var ACP_PROTOCOL_VERSION = 1;
30805
30953
  var OPENCODE_FORCED_PERMISSION_TOOLS = [
@@ -30925,7 +31073,7 @@ function permissionDecisionToResult(decision) {
30925
31073
  var SECRET_VALUE_RE = /(?:(?:api[_-]?key|token|secret|password|authorization|bearer)\s*[:=]\s*)(["']?)([^\s"'\\]{8,})\1/gi;
30926
31074
  var JWT_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g;
30927
31075
  function redactString(value) {
30928
- return value.replace(SECRET_VALUE_RE, (_m, q) => `redacted=${q}***${q}`).replace(JWT_RE, "[redacted-jwt]");
31076
+ return redactCredentialText(value).replace(SECRET_VALUE_RE, (_m, q) => `redacted=${q}***${q}`).replace(JWT_RE, "[redacted-jwt]");
30929
31077
  }
30930
31078
  function redactUnknown(value, depth = 0) {
30931
31079
  if (depth > 6) return "[truncated]";
@@ -31041,10 +31189,14 @@ var AcpVersionBelowFloorError = class extends AcpVersionError {
31041
31189
  actual;
31042
31190
  };
31043
31191
  var AcpPermissionCanaryError = class extends AcpHostError {
31044
- constructor(message) {
31192
+ constructor(message, reasonCode = null, minimumRequiredVersion = null) {
31045
31193
  super("permission_canary_failed", message);
31194
+ this.reasonCode = reasonCode;
31195
+ this.minimumRequiredVersion = minimumRequiredVersion;
31046
31196
  this.name = "AcpPermissionCanaryError";
31047
31197
  }
31198
+ reasonCode;
31199
+ minimumRequiredVersion;
31048
31200
  };
31049
31201
  var AcpPromptsBlockedError = class extends AcpHostError {
31050
31202
  constructor() {
@@ -31513,7 +31665,8 @@ var AcpHostSession = class _AcpHostSession {
31513
31665
  }
31514
31666
  const detail = last?.reason ?? "permission-boundary canary failed: need host reject + correlated terminal tool status";
31515
31667
  throw new AcpPermissionCanaryError(
31516
- total === 1 ? detail : `${detail} (failed ${total} attempts)`
31668
+ total === 1 ? detail : `${detail} (failed ${total} attempts)`,
31669
+ last?.reasonCode ?? null
31517
31670
  );
31518
31671
  }
31519
31672
  /** Test/helper: force-enable prompts without canary (never used by production open path). */
@@ -31558,7 +31711,8 @@ var AcpHostSession = class _AcpHostSession {
31558
31711
  passed: false,
31559
31712
  sawPermissionRequest: this.canaryState.sawPermissionRequest,
31560
31713
  sawDeniedToolResult: this.canaryState.sawDeniedToolResult,
31561
- reason: err instanceof Error ? err.message : String(err)
31714
+ reason: err instanceof Error ? err.message : String(err),
31715
+ ...err instanceof AcpHostError ? { reasonCode: err.code } : {}
31562
31716
  };
31563
31717
  }
31564
31718
  }
@@ -32635,6 +32789,7 @@ async function openOpenCodeAcpSession(options) {
32635
32789
 
32636
32790
  // src/host/claude.ts
32637
32791
  var import_node_child_process4 = require("node:child_process");
32792
+ var import_node_module = require("node:module");
32638
32793
  var import_node_fs4 = require("node:fs");
32639
32794
  var import_node_path7 = require("node:path");
32640
32795
  var CHILD_EXIT_WAIT_MS2 = 3e3;
@@ -32745,6 +32900,51 @@ function parseClaudeVersionOutput(stdout) {
32745
32900
  function parseClaudeCodeVersionOutput(stdout) {
32746
32901
  return parseProviderVersionOutput(stdout, /\bClaude Code\b/i, false);
32747
32902
  }
32903
+ function semanticVersion(value) {
32904
+ if (typeof value !== "string") return null;
32905
+ return parseProviderVersionOutput(`${value}
32906
+ `, /\bnever-a-product-name\b/i);
32907
+ }
32908
+ function readPackageAtOrAbove(entrypoint, expectedName) {
32909
+ let directory = (0, import_node_path7.dirname)(entrypoint);
32910
+ for (let depth = 0; depth < 5; depth += 1) {
32911
+ const path = (0, import_node_path7.join)(directory, "package.json");
32912
+ try {
32913
+ const row = JSON.parse((0, import_node_fs4.readFileSync)(path, "utf8"));
32914
+ if (row && typeof row === "object" && !Array.isArray(row) && row.name === expectedName) {
32915
+ return { path, row };
32916
+ }
32917
+ } catch {
32918
+ }
32919
+ const parent = (0, import_node_path7.dirname)(directory);
32920
+ if (parent === directory) break;
32921
+ directory = parent;
32922
+ }
32923
+ return null;
32924
+ }
32925
+ function measureClaudeBundleVersions(executable) {
32926
+ const adapter = readPackageAtOrAbove(
32927
+ executable,
32928
+ "@agentclientprotocol/claude-agent-acp"
32929
+ );
32930
+ if (!adapter) return { agentSdkVersion: null, claudeCodeVersion: null };
32931
+ try {
32932
+ const sdkEntrypoint = (0, import_node_module.createRequire)(adapter.path).resolve(
32933
+ "@anthropic-ai/claude-agent-sdk"
32934
+ );
32935
+ const sdk = readPackageAtOrAbove(
32936
+ sdkEntrypoint,
32937
+ "@anthropic-ai/claude-agent-sdk"
32938
+ );
32939
+ if (!sdk) return { agentSdkVersion: null, claudeCodeVersion: null };
32940
+ return {
32941
+ agentSdkVersion: semanticVersion(sdk.row.version),
32942
+ claudeCodeVersion: semanticVersion(sdk.row.claudeCodeVersion)
32943
+ };
32944
+ } catch {
32945
+ return { agentSdkVersion: null, claudeCodeVersion: null };
32946
+ }
32947
+ }
32748
32948
  async function readClaudeVersionOutput(executable, options) {
32749
32949
  const timeoutMs = options?.timeoutMs ?? ACP_VERSION_CHECK_TIMEOUT_MS;
32750
32950
  const env = options?.env ?? sanitizeChildEnv(process.env);
@@ -32768,24 +32968,21 @@ async function readClaudeVersionOutput(executable, options) {
32768
32968
  );
32769
32969
  });
32770
32970
  }
32771
- async function assertClaudeVersionFloor(executable, options) {
32772
- const minimumVersion = options?.minimumVersion ?? CLAUDE_ACP_MIN_VERSION;
32773
- const lastMeasuredVersion = options?.lastMeasuredVersion ?? CLAUDE_ACP_LAST_MEASURED_VERSION;
32774
- const stdout = await readClaudeVersionOutput(executable, options);
32775
- const version3 = parseClaudeVersionOutput(stdout);
32776
- if (!version3) {
32777
- throw new AcpVersionParseError(
32778
- `could not parse claude-agent-acp version from: ${stdout.trim().slice(0, 200)}`
32779
- );
32780
- }
32781
- assertProviderVersionFloor({
32782
- provider: "claude-agent-acp",
32783
- version: version3,
32784
- minimumVersion,
32785
- lastMeasuredVersion,
32786
- ...options?.onNewerVersion ? { onNewerVersion: options.onNewerVersion } : {}
32787
- });
32788
- return version3;
32971
+ async function inspectClaudeBridgeExecutable(executable = "claude-agent-acp", options) {
32972
+ const resolved = resolveClaudeExecutable(
32973
+ executable,
32974
+ options?.pathEnv,
32975
+ options?.platform
32976
+ );
32977
+ const output = await readClaudeVersionOutput(resolved, options);
32978
+ const bundle = measureClaudeBundleVersions(resolved);
32979
+ return {
32980
+ executable: resolved,
32981
+ providerVersion: parseClaudeVersionOutput(output),
32982
+ lastMeasuredVersion: CLAUDE_ACP_LAST_MEASURED_VERSION,
32983
+ bundledAgentSdkVersion: bundle.agentSdkVersion,
32984
+ bundledClaudeCodeVersion: bundle.claudeCodeVersion
32985
+ };
32789
32986
  }
32790
32987
  function buildClaudeAcpArgs() {
32791
32988
  return [];
@@ -32850,29 +33047,57 @@ async function openClaudeAcpSession(options) {
32850
33047
  let executable;
32851
33048
  let env = baseEnv;
32852
33049
  let claudeCodeExecutable;
33050
+ let providerVersion;
33051
+ let bundleVersions = {
33052
+ agentSdkVersion: null,
33053
+ claudeCodeVersion: null
33054
+ };
33055
+ const reportRuntime = (resolved, version3) => {
33056
+ bundleVersions = measureClaudeBundleVersions(resolved);
33057
+ options.onRuntimeNotice?.({
33058
+ executable: resolved,
33059
+ providerVersion: version3,
33060
+ lastMeasuredVersion: CLAUDE_ACP_LAST_MEASURED_VERSION,
33061
+ bundledAgentSdkVersion: bundleVersions.agentSdkVersion,
33062
+ bundledClaudeCodeVersion: bundleVersions.claudeCodeVersion
33063
+ });
33064
+ };
33065
+ const admitBridgeVersion = (resolved, version3) => {
33066
+ providerVersion = version3;
33067
+ reportRuntime(resolved, version3);
33068
+ assertProviderVersionFloor({
33069
+ provider: "claude-agent-acp",
33070
+ version: version3,
33071
+ minimumVersion: CLAUDE_ACP_MIN_VERSION,
33072
+ lastMeasuredVersion: CLAUDE_ACP_LAST_MEASURED_VERSION,
33073
+ ...options.onVersionNotice ? { onNewerVersion: options.onVersionNotice } : {}
33074
+ });
33075
+ };
32853
33076
  if (requestedExecutable) {
32854
33077
  const output = await readClaudeVersionOutput(requestedExecutable, {
32855
33078
  env: baseEnv
32856
33079
  });
32857
33080
  const bridgeVersion = parseClaudeVersionOutput(output);
32858
33081
  if (bridgeVersion) {
32859
- assertProviderVersionFloor({
32860
- provider: "claude-agent-acp",
32861
- version: bridgeVersion,
32862
- minimumVersion: CLAUDE_ACP_MIN_VERSION,
32863
- lastMeasuredVersion: CLAUDE_ACP_LAST_MEASURED_VERSION,
32864
- ...options.onVersionNotice ? { onNewerVersion: options.onVersionNotice } : {}
32865
- });
32866
33082
  executable = requestedExecutable;
33083
+ admitBridgeVersion(executable, bridgeVersion);
32867
33084
  } else if (parseClaudeCodeVersionOutput(output)) {
32868
33085
  claudeCodeExecutable = requestedExecutable;
32869
33086
  executable = resolvePackagedClaudeBridge(resolvedPathEnv);
32870
33087
  env = buildClaudeChildEnv(parentEnv, claudeCodeExecutable);
32871
- await assertClaudeVersionFloor(executable, {
32872
- env,
32873
- ...options.onVersionNotice ? { onNewerVersion: options.onVersionNotice } : {}
33088
+ const bridgeOutput = await readClaudeVersionOutput(executable, {
33089
+ env
32874
33090
  });
33091
+ const packagedVersion = parseClaudeVersionOutput(bridgeOutput);
33092
+ if (!packagedVersion) {
33093
+ reportRuntime(executable, null);
33094
+ throw new AcpVersionParseError(
33095
+ `could not parse claude-agent-acp version from: ${bridgeOutput.trim().slice(0, 200)}`
33096
+ );
33097
+ }
33098
+ admitBridgeVersion(executable, packagedVersion);
32875
33099
  } else {
33100
+ reportRuntime(requestedExecutable, null);
32876
33101
  throw new AcpVersionError(
32877
33102
  `could not identify Claude executable from: ${output.trim().slice(0, 200)}`
32878
33103
  );
@@ -32883,10 +33108,19 @@ async function openClaudeAcpSession(options) {
32883
33108
  resolvedPathEnv
32884
33109
  );
32885
33110
  if (!options.skipVersionCheck) {
32886
- await assertClaudeVersionFloor(executable, {
32887
- env: baseEnv,
32888
- ...options.onVersionNotice ? { onNewerVersion: options.onVersionNotice } : {}
33111
+ const output = await readClaudeVersionOutput(executable, {
33112
+ env: baseEnv
32889
33113
  });
33114
+ const version3 = parseClaudeVersionOutput(output);
33115
+ if (!version3) {
33116
+ reportRuntime(executable, null);
33117
+ throw new AcpVersionParseError(
33118
+ `could not parse claude-agent-acp version from: ${output.trim().slice(0, 200)}`
33119
+ );
33120
+ }
33121
+ admitBridgeVersion(executable, version3);
33122
+ } else {
33123
+ reportRuntime(executable, null);
32890
33124
  }
32891
33125
  }
32892
33126
  if (options.signal?.aborted) {
@@ -32978,7 +33212,17 @@ async function openClaudeAcpSession(options) {
32978
33212
  })();
32979
33213
  return closePromise;
32980
33214
  };
32981
- return { session, child, executable, args, env, close };
33215
+ return {
33216
+ session,
33217
+ child,
33218
+ executable,
33219
+ args,
33220
+ env,
33221
+ ...providerVersion ? { providerVersion } : {},
33222
+ ...bundleVersions.agentSdkVersion ? { bundledAgentSdkVersion: bundleVersions.agentSdkVersion } : {},
33223
+ ...bundleVersions.claudeCodeVersion ? { bundledClaudeCodeVersion: bundleVersions.claudeCodeVersion } : {},
33224
+ close
33225
+ };
32982
33226
  } catch (error) {
32983
33227
  removeAbortListener();
32984
33228
  transport.close();
@@ -33098,6 +33342,17 @@ async function assertCodexVersionFloor(executable, options) {
33098
33342
  });
33099
33343
  const version3 = parseCodexVersionOutput(stdout);
33100
33344
  if (!version3) {
33345
+ const codexCliVersion = parseProviderVersionOutput(
33346
+ stdout,
33347
+ /\bcodex-cli\b/i,
33348
+ false
33349
+ );
33350
+ if (codexCliVersion) {
33351
+ throw new AcpVersionError(
33352
+ "this is the Codex CLI; --codex-executable takes the codex-acp bridge (npm i -g @agentclientprotocol/codex-acp)",
33353
+ "executable_not_bridge"
33354
+ );
33355
+ }
33101
33356
  throw new AcpVersionParseError(
33102
33357
  `could not parse codex-acp version from: ${stdout.trim().slice(0, 200)}`
33103
33358
  );
@@ -33163,10 +33418,13 @@ async function openCodexAcpSession(options) {
33163
33418
  typeof pathEnv === "string" ? pathEnv : void 0
33164
33419
  );
33165
33420
  const env = buildCodexChildEnv(parentEnv);
33421
+ let providerVersion;
33166
33422
  if (!options.skipVersionCheck) {
33167
- await assertCodexVersionFloor(executable, {
33168
- env,
33169
- ...options.onVersionNotice ? { onNewerVersion: options.onVersionNotice } : {}
33423
+ providerVersion = await assertCodexVersionFloor(executable, { env });
33424
+ options.onVersionNotice?.({
33425
+ provider: "codex-acp",
33426
+ runningVersion: providerVersion,
33427
+ lastMeasuredVersion: CODEX_ACP_LAST_MEASURED_VERSION
33170
33428
  });
33171
33429
  }
33172
33430
  if (options.signal?.aborted) {
@@ -33258,7 +33516,15 @@ async function openCodexAcpSession(options) {
33258
33516
  })();
33259
33517
  return closePromise;
33260
33518
  };
33261
- return { session, child, executable, args, env, close };
33519
+ return {
33520
+ session,
33521
+ child,
33522
+ executable,
33523
+ args,
33524
+ env,
33525
+ ...providerVersion ? { providerVersion } : {},
33526
+ close
33527
+ };
33262
33528
  } catch (error) {
33263
33529
  removeAbortListener();
33264
33530
  transport.close();
@@ -33808,6 +34074,18 @@ var V1_EFFECT_KEYS = /* @__PURE__ */ new Set([
33808
34074
  "updatedAt"
33809
34075
  ]);
33810
34076
  var V2_EFFECT_KEYS = /* @__PURE__ */ new Set([...V1_EFFECT_KEYS, "signalKind"]);
34077
+ var EFFECT_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
34078
+ "lease_id",
34079
+ "leaseId",
34080
+ "listenerBearer",
34081
+ "bearer",
34082
+ "token",
34083
+ "prompt",
34084
+ "ackCommandId",
34085
+ "ack_command_id",
34086
+ "claimCommandId",
34087
+ "claim_command_id"
34088
+ ]);
33811
34089
  function defaultListenerStateDirectory() {
33812
34090
  return process.env.XDG_STATE_HOME ? (0, import_node_path9.join)(process.env.XDG_STATE_HOME, "cswarm", "listeners") : (0, import_node_path9.join)((0, import_node_os6.homedir)(), ".cswarm", "listeners");
33813
34091
  }
@@ -33826,9 +34104,9 @@ function integer(value) {
33826
34104
  function nullableString2(value, max) {
33827
34105
  return value === null || typeof value === "string" && value.length <= max;
33828
34106
  }
33829
- function rejectUnknownKeys(row, allowed) {
34107
+ function rejectSensitiveKeys(row) {
33830
34108
  for (const key2 of Object.keys(row)) {
33831
- if (!allowed.has(key2)) {
34109
+ if (EFFECT_SENSITIVE_KEYS.has(key2)) {
33832
34110
  throw new Error("stored listener effect is malformed");
33833
34111
  }
33834
34112
  }
@@ -33844,17 +34122,16 @@ function parseListenerEffectRecord(raw, expectedId) {
33844
34122
  throw new Error("stored listener effect is malformed");
33845
34123
  }
33846
34124
  const row = value;
34125
+ rejectSensitiveKeys(row);
33847
34126
  if (typeof row.version !== "number" || row.version !== 1 && row.version !== 2 || typeof row.signalId !== "string" || row.signalId.toLowerCase() !== expectedId || !UUID_RE13.test(row.signalId)) {
33848
34127
  throw new Error("stored listener effect is malformed");
33849
34128
  }
33850
34129
  if (row.version === 1) {
33851
- rejectUnknownKeys(row, V1_EFFECT_KEYS);
33852
34130
  if ("signalKind" in row || row.state === "observed" || row.state === "routed_main") {
33853
34131
  throw new Error("stored listener effect is malformed");
33854
34132
  }
33855
34133
  return upcastV1Ask(row);
33856
34134
  }
33857
- rejectUnknownKeys(row, V2_EFFECT_KEYS);
33858
34135
  return parseV2Record(row);
33859
34136
  }
33860
34137
  function upcastV1Ask(row) {
@@ -34307,6 +34584,7 @@ var GrokListenerModel = class {
34307
34584
  ...this.options.effort ? { effort: this.options.effort } : {},
34308
34585
  ...this.options.env ? { env: this.options.env } : {},
34309
34586
  ...this.options.onVersionNotice ? { onVersionNotice: this.options.onVersionNotice } : {},
34587
+ ...this.options.events ? { events: this.options.events } : {},
34310
34588
  ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
34311
34589
  clientName: "cswarm-listener"
34312
34590
  });
@@ -34747,6 +35025,7 @@ var OpenCodeListenerModel = class {
34747
35025
  ...this.options.model ? { model: this.options.model } : {},
34748
35026
  ...this.options.env ? { env: this.options.env } : {},
34749
35027
  ...this.options.onVersionNotice ? { onVersionNotice: this.options.onVersionNotice } : {},
35028
+ ...this.options.events ? { events: this.options.events } : {},
34750
35029
  ...this.options.allowMissingAuth === true ? { allowMissingAuth: true } : {},
34751
35030
  ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
34752
35031
  clientName: "cswarm-listener"
@@ -34850,6 +35129,35 @@ var import_node_crypto16 = require("node:crypto");
34850
35129
  var import_promises7 = require("node:fs/promises");
34851
35130
  var import_node_os8 = require("node:os");
34852
35131
  var import_node_path13 = require("node:path");
35132
+ var CLAUDE_CODE_VERSION_REQUIRED_RE = /\bClaude Code (\d+\.\d+\.\d+) does not support this model; version (\d+\.\d+\.\d+) or newer is required\b/;
35133
+ var CLAUDE_AUTH_FAILURE_RE = /\b(?:authentication failed|authentication required|not authenticated|OAuth (?:sign-in|login|token)|keychain\/OAuth|please (?:log|sign) in)\b/i;
35134
+ var CLAUDE_CANARY_TIMEOUT_RE = /^ACP request timed out: session\/prompt(?: \(failed \d+ attempts\))?$/;
35135
+ function classifyClaudeCanaryFailure(detail, typedReasonCode) {
35136
+ const recorded = detail?.trim() ?? "";
35137
+ const demanded = CLAUDE_CODE_VERSION_REQUIRED_RE.exec(recorded);
35138
+ if (demanded?.[2]) {
35139
+ return {
35140
+ code: "claude_bridge_version_required",
35141
+ minimumRequiredVersion: demanded[2]
35142
+ };
35143
+ }
35144
+ if (typedReasonCode === "claude_canary_timeout" || typedReasonCode === "timeout" || (typedReasonCode === null || typedReasonCode === void 0) && CLAUDE_CANARY_TIMEOUT_RE.test(recorded)) {
35145
+ return { code: "claude_canary_timeout", minimumRequiredVersion: null };
35146
+ }
35147
+ if (typedReasonCode === "claude_canary_auth_failed") {
35148
+ return { code: "claude_canary_auth_failed", minimumRequiredVersion: null };
35149
+ }
35150
+ if (typedReasonCode === "claude_bridge_version_required") {
35151
+ return {
35152
+ code: "claude_bridge_version_required",
35153
+ minimumRequiredVersion: demanded?.[2] ?? null
35154
+ };
35155
+ }
35156
+ if ((typedReasonCode === "rpc_error" || typedReasonCode === null || typedReasonCode === void 0) && CLAUDE_AUTH_FAILURE_RE.test(recorded)) {
35157
+ return { code: "claude_canary_auth_failed", minimumRequiredVersion: null };
35158
+ }
35159
+ return { code: "claude_canary_unknown", minimumRequiredVersion: null };
35160
+ }
34853
35161
  var ClaudeListenerClosedDuringOpen = class extends Error {
34854
35162
  constructor() {
34855
35163
  super("listener model closed while the Claude worker was opening");
@@ -34963,6 +35271,8 @@ var ClaudeListenerModel = class {
34963
35271
  ...this.options.executable ? { executable: this.options.executable } : {},
34964
35272
  ...this.options.env ? { env: this.options.env } : {},
34965
35273
  ...this.options.onVersionNotice ? { onVersionNotice: this.options.onVersionNotice } : {},
35274
+ ...this.options.onRuntimeNotice ? { onRuntimeNotice: this.options.onRuntimeNotice } : {},
35275
+ ...this.options.events ? { events: this.options.events } : {},
34966
35276
  signal: controller.signal,
34967
35277
  ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
34968
35278
  clientName: "cswarm-listener"
@@ -34991,6 +35301,7 @@ var ClaudeListenerModel = class {
34991
35301
  (0, import_node_os8.tmpdir)(),
34992
35302
  `cswarm-claude-permission-canary-${process.pid}-${(0, import_node_crypto16.randomUUID)()}`
34993
35303
  );
35304
+ let canaryError;
34994
35305
  let sentinelCreated = false;
34995
35306
  try {
34996
35307
  await handle.session.enablePromptsAfterCanary({
@@ -34998,6 +35309,8 @@ var ClaudeListenerModel = class {
34998
35309
  probeText: `Create the file ${sentinelPath} using the Write tool with content CSWARM_CANARY_NOOP. You must use the Write tool. Do nothing else.`,
34999
35310
  ...this.options.onCanaryAttempt ? { onAttempt: this.options.onCanaryAttempt } : {}
35000
35311
  });
35312
+ } catch (error) {
35313
+ canaryError = error;
35001
35314
  } finally {
35002
35315
  try {
35003
35316
  await (0, import_promises7.lstat)(sentinelPath);
@@ -35009,9 +35322,22 @@ var ClaudeListenerModel = class {
35009
35322
  }
35010
35323
  if (sentinelCreated) {
35011
35324
  throw new AcpPermissionCanaryError(
35012
- "Claude bridge wrote the permission canary sentinel before denial"
35325
+ "Claude bridge wrote the permission canary sentinel before denial",
35326
+ "claude_canary_write_not_blocked"
35327
+ );
35328
+ }
35329
+ if (canaryError instanceof AcpPermissionCanaryError) {
35330
+ const shape = classifyClaudeCanaryFailure(
35331
+ canaryError.message,
35332
+ canaryError.reasonCode
35333
+ );
35334
+ throw new AcpPermissionCanaryError(
35335
+ canaryError.message,
35336
+ shape.code,
35337
+ shape.minimumRequiredVersion
35013
35338
  );
35014
35339
  }
35340
+ if (canaryError !== void 0) throw canaryError;
35015
35341
  }
35016
35342
  };
35017
35343
 
@@ -35026,6 +35352,10 @@ var CodexListenerClosedDuringOpen = class extends Error {
35026
35352
  this.name = "CodexListenerClosedDuringOpen";
35027
35353
  }
35028
35354
  };
35355
+ function pathIsInsideOrEqual(ancestor, candidate) {
35356
+ const fromAncestor = (0, import_node_path14.relative)(ancestor, candidate);
35357
+ return fromAncestor === "" || fromAncestor !== ".." && !fromAncestor.startsWith(`..${import_node_path14.sep}`) && !(0, import_node_path14.isAbsolute)(fromAncestor);
35358
+ }
35029
35359
  var CodexListenerModel = class {
35030
35360
  constructor(options) {
35031
35361
  this.options = options;
@@ -35133,6 +35463,7 @@ var CodexListenerModel = class {
35133
35463
  ...this.options.executable ? { executable: this.options.executable } : {},
35134
35464
  ...this.options.env ? { env: this.options.env } : {},
35135
35465
  ...this.options.onVersionNotice ? { onVersionNotice: this.options.onVersionNotice } : {},
35466
+ ...this.options.events ? { events: this.options.events } : {},
35136
35467
  signal: controller.signal,
35137
35468
  ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
35138
35469
  clientName: "cswarm-listener"
@@ -35157,10 +35488,25 @@ var CodexListenerModel = class {
35157
35488
  }
35158
35489
  /** Force Codex's measured shell permission path without changing worker cwd. */
35159
35490
  async enablePromptsAfterCodexCanary(handle) {
35491
+ const configuredHome = this.options.env?.HOME;
35492
+ const home = configuredHome && (0, import_node_path14.isAbsolute)(configuredHome) ? configuredHome : (0, import_node_os9.homedir)();
35493
+ const sentinelDirectory = (0, import_node_path14.join)(home, ".cswarm", "canary");
35494
+ await (0, import_promises8.mkdir)(sentinelDirectory, { recursive: true, mode: 448 });
35495
+ await (0, import_promises8.chmod)(sentinelDirectory, 448);
35496
+ const [workerCwd, canaryDirectory] = await Promise.all([
35497
+ (0, import_promises8.realpath)(this.options.cwd),
35498
+ (0, import_promises8.realpath)(sentinelDirectory)
35499
+ ]);
35500
+ if (pathIsInsideOrEqual(workerCwd, canaryDirectory)) {
35501
+ throw new AcpPermissionCanaryError(
35502
+ `canary_path_inside_cwd: ${canaryDirectory} is inside listener cwd ${workerCwd}. Next: pass a --cwd that is not your home directory`
35503
+ );
35504
+ }
35160
35505
  const sentinelPath = (0, import_node_path14.join)(
35161
- (0, import_node_os9.tmpdir)(),
35506
+ canaryDirectory,
35162
35507
  `cswarm-codex-permission-canary-${process.pid}-${(0, import_node_crypto17.randomUUID)()}`
35163
35508
  );
35509
+ let canaryError;
35164
35510
  let sentinelCreated = false;
35165
35511
  try {
35166
35512
  await handle.session.enablePromptsAfterCanary({
@@ -35168,6 +35514,8 @@ var CodexListenerModel = class {
35168
35514
  probeText: `Use a shell command to create ${sentinelPath} with content CSWARM_CANARY_NOOP. You must use the shell. Do nothing else.`,
35169
35515
  ...this.options.onCanaryAttempt ? { onAttempt: this.options.onCanaryAttempt } : {}
35170
35516
  });
35517
+ } catch (error) {
35518
+ canaryError = error;
35171
35519
  } finally {
35172
35520
  try {
35173
35521
  await (0, import_promises8.lstat)(sentinelPath);
@@ -35177,11 +35525,39 @@ var CodexListenerModel = class {
35177
35525
  if (error.code !== "ENOENT") throw error;
35178
35526
  }
35179
35527
  }
35528
+ const observation = handle.session.canaryObservation;
35529
+ const sawPermissionRequest = observation?.sawPermissionRequest === true;
35530
+ const bridgeVersion = handle.providerVersion ?? handle.session.info?.agentVersion ?? "unknown";
35531
+ if (sentinelCreated && !sawPermissionRequest) {
35532
+ throw new AcpPermissionCanaryError(
35533
+ `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}`
35534
+ );
35535
+ }
35180
35536
  if (sentinelCreated) {
35181
35537
  throw new AcpPermissionCanaryError(
35182
- "Codex bridge wrote the permission canary sentinel before denial"
35538
+ `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
35539
  );
35184
35540
  }
35541
+ if (canaryError instanceof AcpPermissionCanaryError) {
35542
+ if (canaryError.reasonCode === "timeout") {
35543
+ throw new AcpPermissionCanaryError(
35544
+ `canary_timeout: ${canaryError.message}. Next: retry to run a fresh bounded permission canary`,
35545
+ "timeout"
35546
+ );
35547
+ }
35548
+ if (canaryError.reasonCode !== null) {
35549
+ throw new AcpPermissionCanaryError(
35550
+ `canary_bridge_error: codex-acp ${bridgeVersion} returned ${canaryError.message}. Next: resolve the quoted bridge error, then retry`,
35551
+ canaryError.reasonCode
35552
+ );
35553
+ }
35554
+ if (!sawPermissionRequest) {
35555
+ throw new AcpPermissionCanaryError(
35556
+ `canary_no_tool_call: codex-acp ${bridgeVersion} did not request permission or create ${sentinelPath}. Next: retry to re-sample the model's shell choice`
35557
+ );
35558
+ }
35559
+ }
35560
+ if (canaryError !== void 0) throw canaryError;
35185
35561
  }
35186
35562
  };
35187
35563
 
@@ -35775,6 +36151,21 @@ var UUID_RE15 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-
35775
36151
  var MAX_QUEUE_BYTES = 1024 * 1024;
35776
36152
  var QUEUE_FILE = "pending-for-main.json";
35777
36153
  var QUEUE_LOCK = "pending-for-main";
36154
+ var QUEUE_KEYS = /* @__PURE__ */ new Set(["version", "entries", "droppedCount"]);
36155
+ var ENTRY_KEYS = /* @__PURE__ */ new Set([
36156
+ "signalId",
36157
+ "workspaceId",
36158
+ "principalId",
36159
+ "fromId",
36160
+ "fromKind",
36161
+ "kind",
36162
+ "senderName",
36163
+ "body",
36164
+ "attachmentCount",
36165
+ "createdAt",
36166
+ "queuedAt",
36167
+ "observationPending"
36168
+ ]);
35778
36169
  var LISTENER_MAIN_QUEUE_MAX = 200;
35779
36170
  var LISTENER_DEFER_OVER_MIN = 1;
35780
36171
  var LISTENER_DEFER_OVER_MAX = 1e4;
@@ -35805,26 +36196,12 @@ function decideListenerRoute(route, threshold, bodyLength) {
35805
36196
  function checkedTimestamp2(value) {
35806
36197
  return typeof value === "string" && Number.isFinite(Date.parse(value));
35807
36198
  }
35808
- function parseEntry(value) {
36199
+ function parseEntry(value, rejectUnknownKeys) {
35809
36200
  if (!value || typeof value !== "object" || Array.isArray(value)) {
35810
36201
  throw new Error("stored pending-for-main entry is malformed");
35811
36202
  }
35812
36203
  const row = value;
35813
- const allowed = /* @__PURE__ */ new Set([
35814
- "signalId",
35815
- "workspaceId",
35816
- "principalId",
35817
- "fromId",
35818
- "fromKind",
35819
- "kind",
35820
- "senderName",
35821
- "body",
35822
- "attachmentCount",
35823
- "createdAt",
35824
- "queuedAt",
35825
- "observationPending"
35826
- ]);
35827
- if (Object.keys(row).some((key2) => !allowed.has(key2))) {
36204
+ if (rejectUnknownKeys && Object.keys(row).some((key2) => !ENTRY_KEYS.has(key2))) {
35828
36205
  throw new Error("stored pending-for-main entry is malformed");
35829
36206
  }
35830
36207
  if (typeof row.signalId !== "string" || !UUID_RE15.test(row.signalId) || typeof row.workspaceId !== "string" || !UUID_RE15.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE15.test(row.principalId) || typeof row.fromId !== "string" || !UUID_RE15.test(row.fromId) || row.fromKind !== "user" && row.fromKind !== "agent" || !(row.kind === void 0 || row.kind === "ask" || row.kind === "note") || !(row.senderName === null || typeof row.senderName === "string" && row.senderName.length <= 200) || typeof row.body !== "string" || row.body.length < 1 || !(row.attachmentCount === void 0 || typeof row.attachmentCount === "number" && Number.isSafeInteger(row.attachmentCount) && row.attachmentCount >= 1 && row.attachmentCount <= 8) || !checkedTimestamp2(row.createdAt) || !checkedTimestamp2(row.queuedAt) || !(row.observationPending === void 0 || row.observationPending === true)) {
@@ -35845,7 +36222,7 @@ function parseEntry(value) {
35845
36222
  ...row.observationPending === true ? { observationPending: true } : {}
35846
36223
  };
35847
36224
  }
35848
- function parseFile(raw) {
36225
+ function parseFile(raw, rejectUnknownKeys = false) {
35849
36226
  let value;
35850
36227
  try {
35851
36228
  value = JSON.parse(raw);
@@ -35856,12 +36233,10 @@ function parseFile(raw) {
35856
36233
  throw new Error("stored pending-for-main queue is malformed");
35857
36234
  }
35858
36235
  const row = value;
35859
- if (Object.keys(row).some(
35860
- (key2) => key2 !== "version" && key2 !== "entries" && key2 !== "droppedCount"
35861
- ) || row.version !== 1 || !Array.isArray(row.entries) || row.entries.length > LISTENER_MAIN_QUEUE_MAX || !(row.droppedCount === void 0 || typeof row.droppedCount === "number" && Number.isSafeInteger(row.droppedCount) && row.droppedCount >= 0)) {
36236
+ if (rejectUnknownKeys && Object.keys(row).some((key2) => !QUEUE_KEYS.has(key2)) || row.version !== 1 || !Array.isArray(row.entries) || row.entries.length > LISTENER_MAIN_QUEUE_MAX || !(row.droppedCount === void 0 || typeof row.droppedCount === "number" && Number.isSafeInteger(row.droppedCount) && row.droppedCount >= 0)) {
35862
36237
  throw new Error("stored pending-for-main queue is malformed");
35863
36238
  }
35864
- const entries = row.entries.map(parseEntry);
36239
+ const entries = row.entries.map((entry) => parseEntry(entry, rejectUnknownKeys));
35865
36240
  if (new Set(entries.map((entry) => entry.signalId)).size !== entries.length) {
35866
36241
  throw new Error("stored pending-for-main queue repeats a signal");
35867
36242
  }
@@ -35882,7 +36257,7 @@ var FilePendingMainQueue = class {
35882
36257
  return raw === null ? { version: 1, entries: [], droppedCount: 0 } : parseFile(raw);
35883
36258
  }
35884
36259
  async writeUnlocked(file) {
35885
- const canonical = parseFile(JSON.stringify(file));
36260
+ const canonical = parseFile(JSON.stringify(file), true);
35886
36261
  await writeSecureJsonFile(this.path, JSON.stringify(canonical));
35887
36262
  }
35888
36263
  async read() {
@@ -35896,7 +36271,7 @@ var FilePendingMainQueue = class {
35896
36271
  return { count: file.entries.length, droppedCount: file.droppedCount };
35897
36272
  }
35898
36273
  async enqueue(entry) {
35899
- const checked = parseEntry(entry);
36274
+ const checked = parseEntry(entry, true);
35900
36275
  return await withFileLock(this.directory, QUEUE_LOCK, async () => {
35901
36276
  const file = await this.readUnlocked();
35902
36277
  if (file.entries.some((item) => item.signalId === checked.signalId)) {
@@ -35955,7 +36330,7 @@ function pendingMainEntry(signal, principalId, provenance, now, options = {}) {
35955
36330
  createdAt: signal.created_at,
35956
36331
  queuedAt: new Date(now).toISOString(),
35957
36332
  ...options.observationPending ? { observationPending: true } : {}
35958
- });
36333
+ }, true);
35959
36334
  }
35960
36335
 
35961
36336
  // src/listener/runtime.ts
@@ -35968,6 +36343,7 @@ var LISTENER_REPLY_ONLY_MINIMUM_MS = SIGNAL_REQUEST_TIMEOUT_MS + LISTENER_ACK_ON
35968
36343
  var LISTENER_PROMPT_START_MINIMUM_MS = SIGNAL_READ_TIMEOUT_MS + ACP_DEFAULT_REQUEST_TIMEOUT_MS + LISTENER_REPLY_ONLY_MINIMUM_MS;
35969
36344
  var LISTENER_DELIVERY_RETRY_INITIAL_MS = 500;
35970
36345
  var LISTENER_DELIVERY_RETRY_MAX_MS = 3e4;
36346
+ var LISTENER_HOST_PORTS_PROBE_MS = 6e4;
35971
36347
  var UUID_RE16 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
35972
36348
  var ListenerCapabilityError = class extends Error {
35973
36349
  code;
@@ -36402,6 +36778,8 @@ async function runListenerRuntime(options) {
36402
36778
  let ready = false;
36403
36779
  let deliveryMode = null;
36404
36780
  let readAttempt = 0;
36781
+ let readEpisodeStartedAtMs = null;
36782
+ let readEpisodeAttempts = 0;
36405
36783
  const onAbort = () => {
36406
36784
  options.model.cancel();
36407
36785
  };
@@ -36499,6 +36877,18 @@ async function runListenerRuntime(options) {
36499
36877
  }
36500
36878
  });
36501
36879
  requireCapabilities(page);
36880
+ if (ready && readEpisodeStartedAtMs !== null) {
36881
+ const recoveredAtMs = now();
36882
+ options.onEvent?.({
36883
+ type: "read_recovered",
36884
+ attempts: readEpisodeAttempts,
36885
+ durationMs: Math.max(0, recoveredAtMs - readEpisodeStartedAtMs),
36886
+ startedAt: new Date(readEpisodeStartedAtMs).toISOString(),
36887
+ ts: new Date(recoveredAtMs).toISOString()
36888
+ });
36889
+ readEpisodeStartedAtMs = null;
36890
+ readEpisodeAttempts = 0;
36891
+ }
36502
36892
  const nextMode = classifyDeliveryMode(page, durableConfigured);
36503
36893
  if (nextMode !== deliveryMode) {
36504
36894
  deliveryMode = nextMode;
@@ -36519,19 +36909,25 @@ async function runListenerRuntime(options) {
36519
36909
  stop = { reason: "credential", error: asError2(error) };
36520
36910
  break;
36521
36911
  }
36522
- if (isAbort2(error)) {
36523
- stop = { reason: "cancelled" };
36524
- break;
36525
- }
36526
- if (isRetryableFollowError(error)) {
36912
+ const failure = classifySignalReadFailure(error);
36913
+ if (isRetryableFollowError(error) || failure.code === "aborted" || failure.code === "host_ports_exhausted") {
36527
36914
  readAttempt += 1;
36528
- const delayMs = nextFollowBackoffMs(readAttempt, null, random);
36915
+ const delayMs = failure.code === "host_ports_exhausted" ? LISTENER_HOST_PORTS_PROBE_MS : nextFollowBackoffMs(readAttempt, null, random);
36529
36916
  if (ready) {
36917
+ const failedAtMs = now();
36918
+ if (readEpisodeStartedAtMs === null) {
36919
+ readEpisodeStartedAtMs = failedAtMs;
36920
+ readEpisodeAttempts = 0;
36921
+ }
36922
+ readEpisodeAttempts += 1;
36530
36923
  options.onEvent?.({
36531
36924
  type: "read_retry",
36532
36925
  attempt: readAttempt,
36926
+ episodeAttempt: readEpisodeAttempts,
36927
+ episodeStartedAt: new Date(readEpisodeStartedAtMs).toISOString(),
36928
+ failure,
36533
36929
  delayMs,
36534
- ts: eventTime(now)
36930
+ ts: new Date(failedAtMs).toISOString()
36535
36931
  });
36536
36932
  }
36537
36933
  await sleep2(delayMs, abort);
@@ -36554,6 +36950,7 @@ async function runListenerRuntime(options) {
36554
36950
  type: "ready",
36555
36951
  workspaceId: options.workspaceId,
36556
36952
  principalId: options.principalId,
36953
+ cadenceMs: pollMs,
36557
36954
  ts: eventTime(now)
36558
36955
  });
36559
36956
  if (options.declareModel !== void 0) {
@@ -37083,27 +37480,275 @@ async function runListenerRuntime(options) {
37083
37480
  return stop ?? { reason: "cancelled" };
37084
37481
  }
37085
37482
 
37086
- // src/listener/control.ts
37087
- var import_node_net = require("node:net");
37088
- var import_promises9 = require("node:fs/promises");
37089
- var import_node_path16 = require("node:path");
37090
- var UUID_RE17 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
37091
- var SEMVER_RE2 = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
37092
- var MAX_STATUS_BYTES = 16 * 1024;
37093
- var MAX_CONTROL_BYTES = 8 * 1024;
37094
- var CONTROL_TIMEOUT_MS = 2e3;
37095
- var START_LOCK_WAIT_MS = 2e3;
37096
- var START_LOCK_STALE_MS = 1e4;
37097
- var ListenerAlreadyRunningError = class extends Error {
37098
- constructor() {
37099
- super("a listener is already running for this agent principal");
37100
- this.name = "ListenerAlreadyRunningError";
37101
- }
37102
- };
37103
- function listenerPaths(options) {
37104
- const root = options.stateDirectory ?? defaultListenerStateDirectory();
37105
- if (!(0, import_node_path16.isAbsolute)(root)) {
37106
- throw new Error("listener state directory must be absolute");
37483
+ // src/listener/read-health.ts
37484
+ var HOUR_MS = 60 * 6e4;
37485
+ var MINUTE_MS = 6e4;
37486
+ var HEALTH_WINDOW_MS = 24 * HOUR_MS;
37487
+ var LISTENER_READ_RETRY_HOUR_CAP = 25;
37488
+ var LISTENER_READ_RETRY_MINUTE_CAP = 61;
37489
+ var LISTENER_CLAIM_HOUR_CAP = 25;
37490
+ var LISTENER_THROUGHPUT_LAPSE_RATIO = 0.5;
37491
+ var FAILURE_CODES = /* @__PURE__ */ new Set([
37492
+ "http_status",
37493
+ "no_response",
37494
+ "body_timeout",
37495
+ "malformed_response",
37496
+ "aborted",
37497
+ "host_ports_exhausted",
37498
+ "unclassified"
37499
+ ]);
37500
+ function emptyListenerReadHealth() {
37501
+ return {
37502
+ currentEpisodeStartedAt: null,
37503
+ currentEpisodeAttempts: 0,
37504
+ currentReasonCode: null,
37505
+ currentHttpStatus: null,
37506
+ currentErrorConstructor: null,
37507
+ retryHours: [],
37508
+ retryMinutes: [],
37509
+ claimCadenceMs: null,
37510
+ claimHours: []
37511
+ };
37512
+ }
37513
+ function bucketStart(ts, sizeMs) {
37514
+ const time = Date.parse(ts);
37515
+ return new Date(Math.floor(time / sizeMs) * sizeMs).toISOString();
37516
+ }
37517
+ function trimNewest(rows3, cap) {
37518
+ return rows3.sort((left, right) => Date.parse(left.hourStart) - Date.parse(right.hourStart)).slice(-cap);
37519
+ }
37520
+ function recordRetryHour(rows3, ts, episodeStarted) {
37521
+ const hourStart = bucketStart(ts, HOUR_MS);
37522
+ const next = rows3.map((row) => ({ ...row }));
37523
+ const existing = next.find((row) => row.hourStart === hourStart);
37524
+ if (existing) {
37525
+ existing.retries += 1;
37526
+ if (episodeStarted) existing.episodes += 1;
37527
+ } else {
37528
+ next.push({
37529
+ hourStart,
37530
+ retries: 1,
37531
+ episodes: episodeStarted ? 1 : 0,
37532
+ longestEpisodeAttempts: 0,
37533
+ longestEpisodeDurationMs: 0
37534
+ });
37535
+ }
37536
+ return trimNewest(next, LISTENER_READ_RETRY_HOUR_CAP);
37537
+ }
37538
+ function recordRetryMinute(rows3, ts) {
37539
+ const minuteStart = bucketStart(ts, MINUTE_MS);
37540
+ const next = rows3.map((row) => ({ ...row }));
37541
+ const existing = next.find((row) => row.minuteStart === minuteStart);
37542
+ if (existing) {
37543
+ existing.retries += 1;
37544
+ } else {
37545
+ next.push({ minuteStart, retries: 1 });
37546
+ }
37547
+ return next.sort(
37548
+ (left, right) => Date.parse(left.minuteStart) - Date.parse(right.minuteStart)
37549
+ ).slice(-LISTENER_READ_RETRY_MINUTE_CAP);
37550
+ }
37551
+ function recordListenerReadRetry(health, input) {
37552
+ return {
37553
+ ...health,
37554
+ currentEpisodeStartedAt: input.episodeStartedAt,
37555
+ currentEpisodeAttempts: input.episodeAttempt,
37556
+ currentReasonCode: input.failure.code,
37557
+ currentHttpStatus: input.failure.httpStatus,
37558
+ currentErrorConstructor: input.failure.errorConstructor,
37559
+ retryHours: recordRetryHour(
37560
+ health.retryHours,
37561
+ input.ts,
37562
+ input.episodeAttempt === 1
37563
+ ),
37564
+ retryMinutes: recordRetryMinute(health.retryMinutes, input.ts)
37565
+ };
37566
+ }
37567
+ function recordListenerReadRecovery(health, input) {
37568
+ const hourStart = bucketStart(input.startedAt, HOUR_MS);
37569
+ const retryHours = health.retryHours.map((row) => ({ ...row }));
37570
+ const hour = retryHours.find((row) => row.hourStart === hourStart);
37571
+ if (hour) {
37572
+ if (input.durationMs > hour.longestEpisodeDurationMs || input.durationMs === hour.longestEpisodeDurationMs && input.attempts > hour.longestEpisodeAttempts) {
37573
+ hour.longestEpisodeAttempts = input.attempts;
37574
+ hour.longestEpisodeDurationMs = input.durationMs;
37575
+ }
37576
+ }
37577
+ return {
37578
+ ...health,
37579
+ currentEpisodeStartedAt: null,
37580
+ currentEpisodeAttempts: 0,
37581
+ currentReasonCode: null,
37582
+ currentHttpStatus: null,
37583
+ currentErrorConstructor: null,
37584
+ retryHours: trimNewest(retryHours, LISTENER_READ_RETRY_HOUR_CAP)
37585
+ };
37586
+ }
37587
+ function recordListenerClaimCadence(health, cadenceMs) {
37588
+ return { ...health, claimCadenceMs: cadenceMs };
37589
+ }
37590
+ function recordListenerClaim(health, ts) {
37591
+ const hourStart = bucketStart(ts, HOUR_MS);
37592
+ const claimHours = health.claimHours.map((row) => ({ ...row }));
37593
+ const hour = claimHours.find((row) => row.hourStart === hourStart);
37594
+ if (hour) hour.claims += 1;
37595
+ else claimHours.push({ hourStart, claims: 1 });
37596
+ return {
37597
+ ...health,
37598
+ claimHours: trimNewest(claimHours, LISTENER_CLAIM_HOUR_CAP)
37599
+ };
37600
+ }
37601
+ function hasExpectedKeys(value, expected, rejectUnknownKeys) {
37602
+ const actual = Object.keys(value);
37603
+ const allowed = new Set(expected);
37604
+ return expected.every((key2) => actual.includes(key2)) && (!rejectUnknownKeys || actual.every((key2) => allowed.has(key2)));
37605
+ }
37606
+ function validTimestamp(value) {
37607
+ return typeof value === "string" && Number.isFinite(Date.parse(value));
37608
+ }
37609
+ function validCount(value) {
37610
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
37611
+ }
37612
+ function parseListenerReadHealth(value, rejectUnknownKeys = false) {
37613
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
37614
+ const row = value;
37615
+ if (!hasExpectedKeys(row, [
37616
+ "currentEpisodeStartedAt",
37617
+ "currentEpisodeAttempts",
37618
+ "currentReasonCode",
37619
+ "currentHttpStatus",
37620
+ "currentErrorConstructor",
37621
+ "retryHours",
37622
+ "retryMinutes",
37623
+ "claimCadenceMs",
37624
+ "claimHours"
37625
+ ], rejectUnknownKeys)) return null;
37626
+ if (!(row.currentEpisodeStartedAt === null || validTimestamp(row.currentEpisodeStartedAt)) || !validCount(row.currentEpisodeAttempts) || !(row.currentReasonCode === null || typeof row.currentReasonCode === "string" && FAILURE_CODES.has(row.currentReasonCode)) || !(row.currentHttpStatus === null || typeof row.currentHttpStatus === "number" && Number.isSafeInteger(row.currentHttpStatus) && row.currentHttpStatus >= 100 && row.currentHttpStatus <= 599) || !(row.currentErrorConstructor === null || typeof row.currentErrorConstructor === "string" && /^[A-Za-z0-9_$-]{1,96}$/.test(row.currentErrorConstructor)) || !(row.claimCadenceMs === null || typeof row.claimCadenceMs === "number" && Number.isSafeInteger(row.claimCadenceMs) && row.claimCadenceMs >= 1) || !Array.isArray(row.retryHours) || row.retryHours.length > LISTENER_READ_RETRY_HOUR_CAP || !Array.isArray(row.retryMinutes) || row.retryMinutes.length > LISTENER_READ_RETRY_MINUTE_CAP || !Array.isArray(row.claimHours) || row.claimHours.length > LISTENER_CLAIM_HOUR_CAP) return null;
37627
+ if (row.currentEpisodeStartedAt === null !== (row.currentEpisodeAttempts === 0) || row.currentEpisodeStartedAt === null !== (row.currentReasonCode === null) || row.currentReasonCode === "http_status" !== (row.currentHttpStatus !== null) || row.currentReasonCode === "unclassified" !== (row.currentErrorConstructor !== null)) return null;
37628
+ for (const value2 of row.retryHours) {
37629
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
37630
+ const hour = value2;
37631
+ if (!hasExpectedKeys(hour, [
37632
+ "hourStart",
37633
+ "retries",
37634
+ "episodes",
37635
+ "longestEpisodeAttempts",
37636
+ "longestEpisodeDurationMs"
37637
+ ], rejectUnknownKeys) || !validTimestamp(hour.hourStart) || !validCount(hour.retries) || !validCount(hour.episodes) || !validCount(hour.longestEpisodeAttempts) || !validCount(hour.longestEpisodeDurationMs)) return null;
37638
+ }
37639
+ for (const value2 of row.retryMinutes) {
37640
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
37641
+ const minute = value2;
37642
+ if (!hasExpectedKeys(minute, ["minuteStart", "retries"], rejectUnknownKeys) || !validTimestamp(minute.minuteStart) || !validCount(minute.retries)) return null;
37643
+ }
37644
+ for (const value2 of row.claimHours) {
37645
+ if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) return null;
37646
+ const hour = value2;
37647
+ if (!hasExpectedKeys(hour, ["hourStart", "claims"], rejectUnknownKeys) || !validTimestamp(hour.hourStart) || !validCount(hour.claims)) return null;
37648
+ }
37649
+ return {
37650
+ currentEpisodeStartedAt: row.currentEpisodeStartedAt,
37651
+ currentEpisodeAttempts: row.currentEpisodeAttempts,
37652
+ currentReasonCode: row.currentReasonCode,
37653
+ currentHttpStatus: row.currentHttpStatus,
37654
+ currentErrorConstructor: row.currentErrorConstructor,
37655
+ retryHours: row.retryHours.map((hour) => ({
37656
+ hourStart: hour.hourStart,
37657
+ retries: hour.retries,
37658
+ episodes: hour.episodes,
37659
+ longestEpisodeAttempts: hour.longestEpisodeAttempts,
37660
+ longestEpisodeDurationMs: hour.longestEpisodeDurationMs
37661
+ })),
37662
+ retryMinutes: row.retryMinutes.map((minute) => ({
37663
+ minuteStart: minute.minuteStart,
37664
+ retries: minute.retries
37665
+ })),
37666
+ claimCadenceMs: row.claimCadenceMs,
37667
+ claimHours: row.claimHours.map((hour) => ({
37668
+ hourStart: hour.hourStart,
37669
+ claims: hour.claims
37670
+ }))
37671
+ };
37672
+ }
37673
+ function summarizeListenerReadHealth(health, readyAt, nowMs) {
37674
+ const windowStart = nowMs - HEALTH_WINDOW_MS;
37675
+ const retryHours = health.retryHours.filter(
37676
+ (row) => Date.parse(row.hourStart) + HOUR_MS > windowStart && Date.parse(row.hourStart) <= nowMs
37677
+ );
37678
+ let episodesLast24h = retryHours.reduce((sum, row) => sum + row.episodes, 0);
37679
+ let longestEpisodeAttemptsLast24h = 0;
37680
+ let longestEpisodeDurationMsLast24h = 0;
37681
+ for (const row of retryHours) {
37682
+ if (row.longestEpisodeDurationMs > longestEpisodeDurationMsLast24h || row.longestEpisodeDurationMs === longestEpisodeDurationMsLast24h && row.longestEpisodeAttempts > longestEpisodeAttemptsLast24h) {
37683
+ longestEpisodeAttemptsLast24h = row.longestEpisodeAttempts;
37684
+ longestEpisodeDurationMsLast24h = row.longestEpisodeDurationMs;
37685
+ }
37686
+ }
37687
+ const currentStartedMs = health.currentEpisodeStartedAt === null ? null : Date.parse(health.currentEpisodeStartedAt);
37688
+ const currentEpisodeDurationMs = currentStartedMs === null ? null : Math.max(0, nowMs - currentStartedMs);
37689
+ if (currentStartedMs !== null && currentStartedMs >= windowStart && currentEpisodeDurationMs !== null && (currentEpisodeDurationMs > longestEpisodeDurationMsLast24h || currentEpisodeDurationMs === longestEpisodeDurationMsLast24h && health.currentEpisodeAttempts > longestEpisodeAttemptsLast24h)) {
37690
+ longestEpisodeAttemptsLast24h = health.currentEpisodeAttempts;
37691
+ longestEpisodeDurationMsLast24h = currentEpisodeDurationMs;
37692
+ }
37693
+ const rollingMinuteStart = Math.floor((nowMs - HOUR_MS) / MINUTE_MS) * MINUTE_MS;
37694
+ const retriesLastHour = health.retryMinutes.reduce((sum, row) => Date.parse(row.minuteStart) >= rollingMinuteStart && Date.parse(row.minuteStart) <= nowMs ? sum + row.retries : sum, 0);
37695
+ const claimThroughputHours = [];
37696
+ if (health.claimCadenceMs !== null && readyAt !== null) {
37697
+ const readyMs = Date.parse(readyAt);
37698
+ const firstFullHour = Math.ceil(readyMs / HOUR_MS) * HOUR_MS;
37699
+ const currentHour = Math.floor(nowMs / HOUR_MS) * HOUR_MS;
37700
+ const first = Math.max(firstFullHour, currentHour - HEALTH_WINDOW_MS);
37701
+ const claimsByHour = new Map(
37702
+ health.claimHours.map((row) => [row.hourStart, row.claims])
37703
+ );
37704
+ const expectedClaims = HOUR_MS / health.claimCadenceMs;
37705
+ for (let hour = first; hour < currentHour; hour += HOUR_MS) {
37706
+ const hourStart = new Date(hour).toISOString();
37707
+ const claims = claimsByHour.get(hourStart) ?? 0;
37708
+ claimThroughputHours.push({
37709
+ hourStart,
37710
+ claims,
37711
+ expectedClaims,
37712
+ ratio: claims / expectedClaims
37713
+ });
37714
+ }
37715
+ }
37716
+ const throughputLapseHours = claimThroughputHours.filter(
37717
+ (hour) => hour.ratio < LISTENER_THROUGHPUT_LAPSE_RATIO
37718
+ );
37719
+ return {
37720
+ currentEpisodeDurationMs,
37721
+ episodesLast24h,
37722
+ longestEpisodeAttemptsLast24h,
37723
+ longestEpisodeDurationMsLast24h,
37724
+ retriesLastHour,
37725
+ retryHours,
37726
+ claimThroughputHours,
37727
+ throughputLapseHours
37728
+ };
37729
+ }
37730
+
37731
+ // src/listener/control.ts
37732
+ var import_node_net = require("node:net");
37733
+ var import_promises9 = require("node:fs/promises");
37734
+ var import_node_path16 = require("node:path");
37735
+ var UUID_RE17 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
37736
+ var SEMVER_RE2 = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
37737
+ var MAX_STATUS_BYTES = 32 * 1024;
37738
+ var MAX_CONTROL_BYTES = 8 * 1024;
37739
+ var CONTROL_TIMEOUT_MS = 2e3;
37740
+ var START_LOCK_WAIT_MS = 2e3;
37741
+ var START_LOCK_STALE_MS = 1e4;
37742
+ var ListenerAlreadyRunningError = class extends Error {
37743
+ constructor() {
37744
+ super("a listener is already running for this agent principal");
37745
+ this.name = "ListenerAlreadyRunningError";
37746
+ }
37747
+ };
37748
+ function listenerPaths(options) {
37749
+ const root = options.stateDirectory ?? defaultListenerStateDirectory();
37750
+ if (!(0, import_node_path16.isAbsolute)(root)) {
37751
+ throw new Error("listener state directory must be absolute");
37107
37752
  }
37108
37753
  const key2 = listenerInstanceKey(options);
37109
37754
  const instanceDirectory = (0, import_node_path16.join)(root, key2);
@@ -37136,8 +37781,14 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
37136
37781
  "lastSignalId",
37137
37782
  "lastErrorCode",
37138
37783
  "lastErrorDetail",
37784
+ "lastErrorReasonCode",
37785
+ "providerExecutable",
37139
37786
  "providerVersion",
37140
37787
  "providerLastMeasuredVersion",
37788
+ "providerBundledAgentSdkVersion",
37789
+ "providerBundledClaudeCodeVersion",
37790
+ "providerMinimumRequiredVersion",
37791
+ "cswarmVersion",
37141
37792
  "lastWorkerStderrTail",
37142
37793
  "logPath",
37143
37794
  "deliveryMode",
@@ -37149,7 +37800,10 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
37149
37800
  "routeMode",
37150
37801
  "deferOverChars",
37151
37802
  "pendingForMainCount",
37152
- "droppedForMainCount"
37803
+ "droppedForMainCount",
37804
+ "readHealth",
37805
+ "connectionsOpened",
37806
+ "connectionReuseRatio"
37153
37807
  ]);
37154
37808
  var STATUS_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
37155
37809
  "leaseId",
@@ -37175,7 +37829,7 @@ var STATUS_DELIVERY_KEYS = [
37175
37829
  "lastClaimAt",
37176
37830
  "lastAckAt"
37177
37831
  ];
37178
- function parseStatus(raw) {
37832
+ function parseStatus(raw, rejectUnknownKeys = false) {
37179
37833
  let value;
37180
37834
  try {
37181
37835
  value = JSON.parse(raw);
@@ -37190,14 +37844,15 @@ function parseStatus(raw) {
37190
37844
  if (STATUS_SENSITIVE_KEYS.has(key2)) {
37191
37845
  throw new Error("stored listener status contains a forbidden field");
37192
37846
  }
37193
- if (!STATUS_ALLOWED_KEYS.has(key2)) {
37847
+ if (rejectUnknownKeys && !STATUS_ALLOWED_KEYS.has(key2)) {
37194
37848
  throw new Error("stored listener status is malformed");
37195
37849
  }
37196
37850
  }
37197
37851
  const nullableUuid3 = (candidate) => candidate === null || typeof candidate === "string" && UUID_RE17.test(candidate);
37198
37852
  const nullableCount = (candidate) => candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
37199
37853
  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)) {
37854
+ const readHealth = row.readHealth === void 0 ? void 0 : parseListenerReadHealth(row.readHealth, rejectUnknownKeys);
37855
+ 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.lastErrorReasonCode === void 0 || row.lastErrorReasonCode === null || typeof row.lastErrorReasonCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorReasonCode)) || !(row.providerExecutable === void 0 || row.providerExecutable === null || typeof row.providerExecutable === "string" && (0, import_node_path16.isAbsolute)(row.providerExecutable)) || !(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.providerBundledAgentSdkVersion === void 0 || row.providerBundledAgentSdkVersion === null || typeof row.providerBundledAgentSdkVersion === "string" && SEMVER_RE2.test(row.providerBundledAgentSdkVersion)) || !(row.providerBundledClaudeCodeVersion === void 0 || row.providerBundledClaudeCodeVersion === null || typeof row.providerBundledClaudeCodeVersion === "string" && SEMVER_RE2.test(row.providerBundledClaudeCodeVersion)) || !(row.providerMinimumRequiredVersion === void 0 || row.providerMinimumRequiredVersion === null || typeof row.providerMinimumRequiredVersion === "string" && SEMVER_RE2.test(row.providerMinimumRequiredVersion)) || !(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) || readHealth === null || !(row.connectionsOpened === void 0 || typeof row.connectionsOpened === "number" && Number.isSafeInteger(row.connectionsOpened) && row.connectionsOpened >= 0) || !(row.connectionReuseRatio === void 0 || typeof row.connectionReuseRatio === "number" && Number.isFinite(row.connectionReuseRatio) && row.connectionReuseRatio >= 0)) {
37201
37856
  throw new Error("stored listener status is malformed");
37202
37857
  }
37203
37858
  const routeMode = row.routeMode ?? "worker";
@@ -37205,8 +37860,11 @@ function parseStatus(raw) {
37205
37860
  if (routeMode === "split" && deferOverChars === null || routeMode !== "split" && deferOverChars !== null) {
37206
37861
  throw new Error("stored listener status routing fields are malformed");
37207
37862
  }
37863
+ const knownRow = Object.fromEntries(
37864
+ Object.entries(row).filter(([key2]) => STATUS_ALLOWED_KEYS.has(key2))
37865
+ );
37208
37866
  return {
37209
- ...row,
37867
+ ...knownRow,
37210
37868
  deliveryMode: row.deliveryMode ?? null,
37211
37869
  pendingDeliveryCount: row.pendingDeliveryCount ?? null,
37212
37870
  lastTerminalDeliveryFailureCount: row.lastTerminalDeliveryFailureCount ?? null,
@@ -37217,14 +37875,19 @@ function parseStatus(raw) {
37217
37875
  lastWorkerStderrTail: row.lastWorkerStderrTail ?? null,
37218
37876
  providerVersion: row.providerVersion ?? null,
37219
37877
  providerLastMeasuredVersion: row.providerLastMeasuredVersion ?? null,
37878
+ ...row.cswarmVersion === void 0 ? {} : { cswarmVersion: row.cswarmVersion },
37220
37879
  routeMode,
37221
37880
  deferOverChars,
37222
37881
  pendingForMainCount: row.pendingForMainCount ?? 0,
37223
- droppedForMainCount: row.droppedForMainCount ?? 0
37882
+ droppedForMainCount: row.droppedForMainCount ?? 0,
37883
+ ...readHealth === void 0 ? {} : { readHealth }
37224
37884
  };
37225
37885
  }
37226
37886
  async function writeListenerStatus(paths, status) {
37227
37887
  const serialized = JSON.stringify(status);
37888
+ if (Buffer.byteLength(serialized, "utf8") > MAX_STATUS_BYTES) {
37889
+ throw new Error("listener status is too large");
37890
+ }
37228
37891
  const parsed = JSON.parse(serialized);
37229
37892
  for (const key2 of STATUS_DELIVERY_KEYS) {
37230
37893
  if (!(key2 in parsed)) {
@@ -37234,13 +37897,17 @@ async function writeListenerStatus(paths, status) {
37234
37897
  if (!("lastErrorDetail" in parsed)) {
37235
37898
  throw new Error("listener status is missing local error detail metadata");
37236
37899
  }
37237
- parseStatus(serialized);
37900
+ parseStatus(serialized, true);
37238
37901
  await writeSecureJsonFile(paths.statusPath, serialized);
37239
37902
  }
37240
37903
  async function readListenerStatus(paths) {
37241
37904
  const raw = await readSecureJsonFile(paths.statusPath, MAX_STATUS_BYTES);
37242
37905
  return raw === null ? null : parseStatus(raw);
37243
37906
  }
37907
+ async function readListenerStatusIfPresent(paths) {
37908
+ const raw = await readSecureJsonFileIfPresent(paths.statusPath, MAX_STATUS_BYTES);
37909
+ return raw === null ? null : parseStatus(raw);
37910
+ }
37244
37911
  async function appendListenerEvent(paths, event) {
37245
37912
  const allowed = /* @__PURE__ */ new Set([
37246
37913
  "ts",
@@ -37255,6 +37922,12 @@ async function appendListenerEvent(paths, event) {
37255
37922
  "passed",
37256
37923
  "reason",
37257
37924
  "delay_ms",
37925
+ "reason_code",
37926
+ "http_status",
37927
+ "error_constructor",
37928
+ "episode_attempt",
37929
+ "attempts",
37930
+ "duration_ms",
37258
37931
  "index",
37259
37932
  "delivery_mode",
37260
37933
  "pending_delivery_count",
@@ -37293,6 +37966,29 @@ async function appendListenerEvent(paths, event) {
37293
37966
  if ((key2 === "attempt" || key2 === "total") && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 1)) {
37294
37967
  throw new Error("listener event attempt count is not allowed");
37295
37968
  }
37969
+ if ((key2 === "episode_attempt" || key2 === "attempts") && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 1)) {
37970
+ throw new Error("listener event episode count is not allowed");
37971
+ }
37972
+ if (key2 === "duration_ms" && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) {
37973
+ throw new Error("listener event episode duration is not allowed");
37974
+ }
37975
+ if (key2 === "http_status" && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 100 && value <= 599)) {
37976
+ throw new Error("listener event HTTP status is not allowed");
37977
+ }
37978
+ if (key2 === "reason_code" && !(typeof value === "string" && [
37979
+ "http_status",
37980
+ "no_response",
37981
+ "body_timeout",
37982
+ "malformed_response",
37983
+ "aborted",
37984
+ "host_ports_exhausted",
37985
+ "unclassified"
37986
+ ].includes(value))) {
37987
+ throw new Error("listener event read reason code is not allowed");
37988
+ }
37989
+ if (key2 === "error_constructor" && !(typeof value === "string" && /^[A-Za-z0-9_$-]{1,96}$/.test(value))) {
37990
+ throw new Error("listener event error constructor is not allowed");
37991
+ }
37296
37992
  if (key2 === "passed" && typeof value !== "boolean") {
37297
37993
  throw new Error("listener event canary result is not allowed");
37298
37994
  }
@@ -37336,6 +38032,12 @@ async function appendListenerEvent(paths, event) {
37336
38032
  if (event.event === "listener_canary_attempt" && (typeof event.attempt !== "number" || typeof event.total !== "number" || event.attempt > event.total || typeof event.passed !== "boolean" || !(event.reason === null || typeof event.reason === "string"))) {
37337
38033
  throw new Error("listener canary attempt event is incomplete");
37338
38034
  }
38035
+ if (event.event === "listener_read_retry" && (typeof event.reason_code !== "string" || typeof event.episode_attempt !== "number" || event.reason_code === "http_status" !== (typeof event.http_status === "number") || event.reason_code === "unclassified" !== (typeof event.error_constructor === "string"))) {
38036
+ throw new Error("listener read retry event is incomplete");
38037
+ }
38038
+ if (event.event === "listener_read_recovered" && (typeof event.attempts !== "number" || typeof event.duration_ms !== "number")) {
38039
+ throw new Error("listener read recovery event is incomplete");
38040
+ }
37339
38041
  await ensureSecureStateDirectory(paths.instanceDirectory);
37340
38042
  const serialized = `${JSON.stringify(event)}
37341
38043
  `;
@@ -37620,6 +38322,28 @@ function localDiagnostic(message, maxChars) {
37620
38322
  function safeErrorDetail(error) {
37621
38323
  return localDiagnostic(error.message, 2048);
37622
38324
  }
38325
+ function providerStatusFields(notice) {
38326
+ if (!notice) return {};
38327
+ return {
38328
+ providerExecutable: notice.executable ?? null,
38329
+ providerVersion: notice.runningVersion,
38330
+ providerLastMeasuredVersion: notice.runningVersion === null ? null : notice.lastMeasuredVersion,
38331
+ providerBundledAgentSdkVersion: notice.bundledAgentSdkVersion ?? null,
38332
+ providerBundledClaudeCodeVersion: notice.bundledClaudeCodeVersion ?? null
38333
+ };
38334
+ }
38335
+ function providerFailureFields(error) {
38336
+ if (!(error instanceof AcpPermissionCanaryError)) {
38337
+ return {
38338
+ lastErrorReasonCode: null,
38339
+ providerMinimumRequiredVersion: null
38340
+ };
38341
+ }
38342
+ return {
38343
+ lastErrorReasonCode: error.reasonCode,
38344
+ providerMinimumRequiredVersion: error.minimumRequiredVersion
38345
+ };
38346
+ }
37623
38347
  var TAIL_SERIALIZED_BUDGET_BYTES = 3e3;
37624
38348
  function fitWorkerStderrTailForLog(tail) {
37625
38349
  let fitted = tail.trim();
@@ -37646,6 +38370,7 @@ async function runListenerSupervisor(options) {
37646
38370
  version: 1,
37647
38371
  instanceId: proposedInstanceId,
37648
38372
  provider: options.provider ?? "grok",
38373
+ ...options.cswarmVersion ? { cswarmVersion: options.cswarmVersion } : {},
37649
38374
  ...options.permissionMode ? { permissionMode: options.permissionMode } : {},
37650
38375
  profileId: options.profileId,
37651
38376
  workspaceId: options.workspaceId.toLowerCase(),
@@ -37659,8 +38384,13 @@ async function runListenerSupervisor(options) {
37659
38384
  lastSignalId: null,
37660
38385
  lastErrorCode: null,
37661
38386
  lastErrorDetail: null,
38387
+ lastErrorReasonCode: null,
38388
+ providerExecutable: null,
37662
38389
  providerVersion: null,
37663
38390
  providerLastMeasuredVersion: null,
38391
+ providerBundledAgentSdkVersion: null,
38392
+ providerBundledClaudeCodeVersion: null,
38393
+ providerMinimumRequiredVersion: null,
37664
38394
  lastWorkerStderrTail: null,
37665
38395
  deliveryMode: null,
37666
38396
  pendingDeliveryCount: null,
@@ -37672,14 +38402,27 @@ async function runListenerSupervisor(options) {
37672
38402
  deferOverChars: options.deferOverChars ?? null,
37673
38403
  pendingForMainCount: 0,
37674
38404
  droppedForMainCount: 0,
38405
+ readHealth: emptyListenerReadHealth(),
38406
+ connectionsOpened: 0,
38407
+ connectionReuseRatio: 0,
37675
38408
  logPath: options.paths.logPath
37676
38409
  };
37677
38410
  let writes = Promise.resolve();
37678
38411
  const chain = (work) => {
37679
38412
  writes = writes.then(work).catch(() => void 0);
37680
38413
  };
38414
+ const statusSnapshot = () => {
38415
+ const metrics = options.getConnectionMetrics?.();
38416
+ return {
38417
+ ...structuredClone(status),
38418
+ ...metrics ? {
38419
+ connectionsOpened: metrics.connectionsOpened,
38420
+ connectionReuseRatio: metrics.connectionReuseRatio
38421
+ } : {}
38422
+ };
38423
+ };
37681
38424
  const persist = () => {
37682
- const snapshot = structuredClone(status);
38425
+ const snapshot = statusSnapshot();
37683
38426
  chain(() => writeListenerStatus(options.paths, snapshot));
37684
38427
  };
37685
38428
  const log = (event) => {
@@ -37697,7 +38440,7 @@ async function runListenerSupervisor(options) {
37697
38440
  const prepare = options.prepare;
37698
38441
  const control = await startListenerControlServer({
37699
38442
  paths: options.paths,
37700
- status: () => structuredClone(status),
38443
+ status: statusSnapshot,
37701
38444
  stop: () => {
37702
38445
  if (status.state === "stopped" || status.state === "failed") return;
37703
38446
  transition("stopping");
@@ -37735,9 +38478,16 @@ async function runListenerSupervisor(options) {
37735
38478
  readyAt: event.ts,
37736
38479
  lastErrorCode: null,
37737
38480
  lastErrorDetail: null,
38481
+ lastErrorReasonCode: null,
37738
38482
  lastWorkerStderrTail: null,
37739
- providerVersion: versionNotice?.runningVersion ?? null,
37740
- providerLastMeasuredVersion: versionNotice?.lastMeasuredVersion ?? null
38483
+ providerMinimumRequiredVersion: null,
38484
+ ...providerStatusFields(versionNotice),
38485
+ ...event.cadenceMs === void 0 ? {} : {
38486
+ readHealth: recordListenerClaimCadence(
38487
+ status.readHealth ?? emptyListenerReadHealth(),
38488
+ event.cadenceMs
38489
+ )
38490
+ }
37741
38491
  });
37742
38492
  log({ ts: event.ts, event: "listener_ready" });
37743
38493
  return;
@@ -37779,14 +38529,54 @@ async function runListenerSupervisor(options) {
37779
38529
  return;
37780
38530
  }
37781
38531
  if (event.type === "read_retry") {
38532
+ status = {
38533
+ ...status,
38534
+ readHealth: recordListenerReadRetry(
38535
+ status.readHealth ?? emptyListenerReadHealth(),
38536
+ {
38537
+ ts: event.ts,
38538
+ episodeStartedAt: event.episodeStartedAt,
38539
+ episodeAttempt: event.episodeAttempt,
38540
+ failure: event.failure
38541
+ }
38542
+ ),
38543
+ updatedAt: event.ts
38544
+ };
38545
+ persist();
37782
38546
  log({
37783
38547
  ts: event.ts,
37784
38548
  event: "listener_read_retry",
37785
38549
  attempt: event.attempt,
38550
+ episode_attempt: event.episodeAttempt,
38551
+ reason_code: event.failure.code,
38552
+ ...event.failure.httpStatus === null ? {} : { http_status: event.failure.httpStatus },
38553
+ ...event.failure.errorConstructor === null ? {} : { error_constructor: event.failure.errorConstructor },
37786
38554
  delay_ms: event.delayMs
37787
38555
  });
37788
38556
  return;
37789
38557
  }
38558
+ if (event.type === "read_recovered") {
38559
+ status = {
38560
+ ...status,
38561
+ readHealth: recordListenerReadRecovery(
38562
+ status.readHealth ?? emptyListenerReadHealth(),
38563
+ {
38564
+ startedAt: event.startedAt,
38565
+ attempts: event.attempts,
38566
+ durationMs: event.durationMs
38567
+ }
38568
+ ),
38569
+ updatedAt: event.ts
38570
+ };
38571
+ persist();
38572
+ log({
38573
+ ts: event.ts,
38574
+ event: "listener_read_recovered",
38575
+ attempts: event.attempts,
38576
+ duration_ms: event.durationMs
38577
+ });
38578
+ return;
38579
+ }
37790
38580
  if (event.type === "malformed_row") {
37791
38581
  log({
37792
38582
  ts: event.ts,
@@ -37823,6 +38613,10 @@ async function runListenerSupervisor(options) {
37823
38613
  if (event.type === "delivery_claim") {
37824
38614
  status = {
37825
38615
  ...status,
38616
+ readHealth: recordListenerClaim(
38617
+ status.readHealth ?? emptyListenerReadHealth(),
38618
+ event.ts
38619
+ ),
37826
38620
  pendingDeliveryCount: event.pendingDeliveryCount,
37827
38621
  lastClaimAt: event.ts,
37828
38622
  updatedAt: event.ts
@@ -37947,9 +38741,9 @@ async function runListenerSupervisor(options) {
37947
38741
  readyAt: null,
37948
38742
  lastErrorCode: restartCode,
37949
38743
  lastErrorDetail: safeErrorDetail(stop.error),
38744
+ ...providerFailureFields(stop.error),
37950
38745
  lastWorkerStderrTail: restartStderrTail,
37951
- providerVersion: null,
37952
- providerLastMeasuredVersion: null
38746
+ ...providerStatusFields(options.getProviderVersionNotice?.() ?? null)
37953
38747
  });
37954
38748
  await restartSleep(delayMs, controller.signal);
37955
38749
  if (controller.signal.aborted) {
@@ -37963,7 +38757,9 @@ async function runListenerSupervisor(options) {
37963
38757
  stoppedAt,
37964
38758
  lastErrorCode: null,
37965
38759
  lastErrorDetail: null,
37966
- lastWorkerStderrTail: null
38760
+ lastErrorReasonCode: null,
38761
+ lastWorkerStderrTail: null,
38762
+ providerMinimumRequiredVersion: null
37967
38763
  });
37968
38764
  log({ ts: stoppedAt, event: "listener_stopped" });
37969
38765
  } else {
@@ -37973,6 +38769,8 @@ async function runListenerSupervisor(options) {
37973
38769
  stoppedAt,
37974
38770
  lastErrorCode: code,
37975
38771
  lastErrorDetail: safeErrorDetail(stop.error),
38772
+ ...providerFailureFields(stop.error),
38773
+ ...providerStatusFields(options.getProviderVersionNotice?.() ?? null),
37976
38774
  lastWorkerStderrTail: failedStderrTail
37977
38775
  });
37978
38776
  log({
@@ -37997,6 +38795,10 @@ async function runListenerSupervisor(options) {
37997
38795
  lastErrorDetail: safeErrorDetail(
37998
38796
  error instanceof Error ? error : new Error(String(error))
37999
38797
  ),
38798
+ ...providerFailureFields(
38799
+ error instanceof Error ? error : new Error(String(error))
38800
+ ),
38801
+ ...providerStatusFields(options.getProviderVersionNotice?.() ?? null),
38000
38802
  lastWorkerStderrTail: failedStderrTail
38001
38803
  });
38002
38804
  log({
@@ -38009,7 +38811,7 @@ async function runListenerSupervisor(options) {
38009
38811
  await writes.catch(() => void 0);
38010
38812
  await control.close().catch(() => void 0);
38011
38813
  }
38012
- return status;
38814
+ return statusSnapshot();
38013
38815
  }
38014
38816
  async function effectiveListenerStatus(paths) {
38015
38817
  try {
@@ -38242,7 +39044,7 @@ function assertPlainObject(input, allowedKeys, requiredKeys, errMessage = "deliv
38242
39044
  throw new Error(errMessage);
38243
39045
  }
38244
39046
  }
38245
- function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
39047
+ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId, rejectUnknownKeys = false) {
38246
39048
  if (Buffer.byteLength(raw, "utf8") > MAX_JOURNAL_BYTES) {
38247
39049
  throw new Error("stored delivery journal is malformed");
38248
39050
  }
@@ -38263,7 +39065,7 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
38263
39065
  throw new Error("stored delivery journal is malformed");
38264
39066
  }
38265
39067
  const ownProps = Object.getOwnPropertyNames(value);
38266
- if (ownProps.length !== ALLOWED_TOP_KEYS.size || !ownProps.every((p) => ALLOWED_TOP_KEYS.has(p))) {
39068
+ if ([...ALLOWED_TOP_KEYS].some((key2) => !ownProps.includes(key2)) || rejectUnknownKeys && !ownProps.every((key2) => ALLOWED_TOP_KEYS.has(key2))) {
38267
39069
  throw new Error("stored delivery journal is malformed");
38268
39070
  }
38269
39071
  for (const prop of ownProps) {
@@ -38297,8 +39099,16 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
38297
39099
  if (!isValidIsoTimestamp(row.updatedAt)) {
38298
39100
  throw new Error("stored delivery journal is malformed");
38299
39101
  }
39102
+ const base = {
39103
+ version: 1,
39104
+ workspaceId: row.workspaceId,
39105
+ principalId: row.principalId,
39106
+ listenerInstanceId: row.listenerInstanceId,
39107
+ nextClaimOrdinal: row.nextClaimOrdinal,
39108
+ updatedAt: row.updatedAt
39109
+ };
38300
39110
  if (row.active === null) {
38301
- return row;
39111
+ return { ...base, active: null };
38302
39112
  }
38303
39113
  if (typeof row.active !== "object" || row.active === null || Array.isArray(row.active)) {
38304
39114
  throw new Error("stored delivery journal is malformed");
@@ -38311,8 +39121,10 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
38311
39121
  throw new Error("stored delivery journal is malformed");
38312
39122
  }
38313
39123
  const activeProps = Object.getOwnPropertyNames(row.active);
38314
- const legacyWithoutFingerprint = activeProps.length === ALLOWED_ACTIVE_KEYS.size - 1 && !activeProps.includes("signalFingerprint");
38315
- if (!legacyWithoutFingerprint && activeProps.length !== ALLOWED_ACTIVE_KEYS.size || !activeProps.every((p) => ALLOWED_ACTIVE_KEYS.has(p))) {
39124
+ const requiredActiveKeys = [...ALLOWED_ACTIVE_KEYS].filter(
39125
+ (key2) => key2 !== "signalFingerprint"
39126
+ );
39127
+ if (requiredActiveKeys.some((key2) => !activeProps.includes(key2)) || rejectUnknownKeys && !activeProps.every((key2) => ALLOWED_ACTIVE_KEYS.has(key2))) {
38316
39128
  throw new Error("stored delivery journal is malformed");
38317
39129
  }
38318
39130
  for (const prop of activeProps) {
@@ -38380,7 +39192,7 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
38380
39192
  throw new Error("stored delivery journal is malformed");
38381
39193
  }
38382
39194
  const ackProps = Object.getOwnPropertyNames(active.ack);
38383
- if (ackProps.length !== ALLOWED_ACK_KEYS.size || !ackProps.every((p) => ALLOWED_ACK_KEYS.has(p))) {
39195
+ if ([...ALLOWED_ACK_KEYS].some((key2) => !ackProps.includes(key2)) || rejectUnknownKeys && !ackProps.every((key2) => ALLOWED_ACK_KEYS.has(key2))) {
38384
39196
  throw new Error("stored delivery journal is malformed");
38385
39197
  }
38386
39198
  for (const prop of ackProps) {
@@ -38389,32 +39201,52 @@ function parseJournalRecord(raw, expectedWorkspaceId, expectedPrincipalId) {
38389
39201
  throw new Error("stored delivery journal is malformed");
38390
39202
  }
38391
39203
  }
38392
- const ack = active.ack;
38393
- if (typeof ack.commandId !== "string" || !COMMAND_ID_RE3.test(ack.commandId)) {
39204
+ const ack2 = active.ack;
39205
+ if (typeof ack2.commandId !== "string" || !COMMAND_ID_RE3.test(ack2.commandId)) {
38394
39206
  throw new Error("stored delivery journal is malformed");
38395
39207
  }
38396
39208
  const expectedAckCmdId = ackCommandId(active.leaseId);
38397
- if (ack.commandId !== expectedAckCmdId) {
39209
+ if (ack2.commandId !== expectedAckCmdId) {
38398
39210
  throw new Error("stored delivery journal is malformed");
38399
39211
  }
38400
- if (typeof ack.outcome !== "string" || !ALLOWED_OUTCOMES.has(ack.outcome)) {
39212
+ if (typeof ack2.outcome !== "string" || !ALLOWED_OUTCOMES.has(ack2.outcome)) {
38401
39213
  throw new Error("stored delivery journal is malformed");
38402
39214
  }
38403
- if (ack.outcome === "failed_terminal") {
38404
- if (typeof ack.lastErrorCode !== "string" || !ALLOWED_ERROR_CODES.has(ack.lastErrorCode)) {
39215
+ if (ack2.outcome === "failed_terminal") {
39216
+ if (typeof ack2.lastErrorCode !== "string" || !ALLOWED_ERROR_CODES.has(ack2.lastErrorCode)) {
38405
39217
  throw new Error("stored delivery journal is malformed");
38406
39218
  }
38407
39219
  } else {
38408
- if (ack.lastErrorCode !== null) {
39220
+ if (ack2.lastErrorCode !== null) {
38409
39221
  throw new Error("stored delivery journal is malformed");
38410
39222
  }
38411
39223
  }
38412
- if (!isValidIsoTimestamp(ack.preparedAt)) {
39224
+ if (!isValidIsoTimestamp(ack2.preparedAt)) {
38413
39225
  throw new Error("stored delivery journal is malformed");
38414
39226
  }
38415
39227
  }
38416
39228
  }
38417
- return row;
39229
+ const ack = active.ack;
39230
+ return {
39231
+ ...base,
39232
+ active: {
39233
+ phase: active.phase,
39234
+ claimOrdinal: active.claimOrdinal,
39235
+ claimCommandId: active.claimCommandId,
39236
+ claimCreatedAt: active.claimCreatedAt,
39237
+ claimLastAttemptAt: active.claimLastAttemptAt,
39238
+ signalId: active.signalId,
39239
+ leaseId: active.leaseId,
39240
+ leasedUntil: active.leasedUntil,
39241
+ ...active.signalFingerprint === void 0 ? {} : { signalFingerprint: active.signalFingerprint },
39242
+ ack: ack === null ? null : {
39243
+ commandId: ack.commandId,
39244
+ outcome: ack.outcome,
39245
+ lastErrorCode: ack.lastErrorCode,
39246
+ preparedAt: ack.preparedAt
39247
+ }
39248
+ }
39249
+ };
38418
39250
  }
38419
39251
  var FileListenerDeliveryJournal = class {
38420
39252
  instanceDirectory;
@@ -38478,7 +39310,8 @@ var FileListenerDeliveryJournal = class {
38478
39310
  parseJournalRecord(
38479
39311
  serialized,
38480
39312
  this.options.workspaceId,
38481
- this.options.principalId
39313
+ this.options.principalId,
39314
+ true
38482
39315
  );
38483
39316
  await writeSecureJsonFile(this.journalPath, serialized);
38484
39317
  }
@@ -38728,7 +39561,8 @@ async function openListenerDeliveryJournal(options) {
38728
39561
  parseJournalRecord(
38729
39562
  serialized2,
38730
39563
  workspaceIdSnapshot,
38731
- principalIdSnapshot
39564
+ principalIdSnapshot,
39565
+ true
38732
39566
  );
38733
39567
  await writeSecureJsonFile(journal.journalPath, serialized2);
38734
39568
  return {
@@ -38762,7 +39596,8 @@ async function openListenerDeliveryJournal(options) {
38762
39596
  parseJournalRecord(
38763
39597
  serialized,
38764
39598
  workspaceIdSnapshot,
38765
- principalIdSnapshot
39599
+ principalIdSnapshot,
39600
+ true
38766
39601
  );
38767
39602
  await writeSecureJsonFile(journal.journalPath, serialized);
38768
39603
  return {
@@ -38918,7 +39753,7 @@ function parseState(raw) {
38918
39753
  }
38919
39754
  const row = value;
38920
39755
  const topicVersions = row.topicVersions;
38921
- if (Object.keys(row).sort().join(",") !== "principalId,topicVersions,version" || row.version !== 1 || typeof row.principalId !== "string" || !UUID_RE20.test(row.principalId) || !topicVersions || typeof topicVersions !== "object" || Array.isArray(topicVersions) || Object.keys(topicVersions).length > MAX_BRAIN_DIGEST_TOPICS) {
39756
+ if (row.version !== 1 || typeof row.principalId !== "string" || !UUID_RE20.test(row.principalId) || !topicVersions || typeof topicVersions !== "object" || Array.isArray(topicVersions) || Object.keys(topicVersions).length > MAX_BRAIN_DIGEST_TOPICS) {
38922
39757
  throw new Error("stored brain digest state is malformed");
38923
39758
  }
38924
39759
  for (const [topic, version3] of Object.entries(topicVersions)) {
@@ -38950,6 +39785,9 @@ function newestFirst(topics) {
38950
39785
  (left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt) || left.topic.localeCompare(right.topic)
38951
39786
  );
38952
39787
  }
39788
+ function changedTopics(previous, topics) {
39789
+ return previous === null ? newestFirst(topics).slice(0, BRAIN_DIGEST_TOPIC_LIMIT) : topics.filter((topic) => previous.topicVersions[topic.topic] !== topic.version);
39790
+ }
38953
39791
  function renderBrainDigest(topicCount, topics) {
38954
39792
  if (topics.length === 0) return null;
38955
39793
  const names = newestFirst(topics).map((topic) => `${topic.topic} v${topic.version}`).join(", ");
@@ -38967,6 +39805,19 @@ var FileBrainDigestStore = class {
38967
39805
  instanceDirectory;
38968
39806
  location;
38969
39807
  principalId;
39808
+ /** Read the shared high-water without advancing it. */
39809
+ async preview(topics) {
39810
+ const raw = await readSecureJsonFileIfPresent(
39811
+ this.location,
39812
+ MAX_BRAIN_DIGEST_STATE_BYTES
39813
+ );
39814
+ const previous = raw === null ? null : parseState(raw);
39815
+ if (previous !== null && previous.principalId !== this.principalId) {
39816
+ throw new Error("stored brain digest state belongs to another principal");
39817
+ }
39818
+ currentState(this.principalId, topics);
39819
+ return renderBrainDigest(topics.length, changedTopics(previous, topics));
39820
+ }
38970
39821
  async consume(topics) {
38971
39822
  return await withFileLock(this.instanceDirectory, BRAIN_DIGEST_LOCK, async () => {
38972
39823
  const raw = await readSecureJsonFile(
@@ -38978,9 +39829,7 @@ var FileBrainDigestStore = class {
38978
39829
  throw new Error("stored brain digest state belongs to another principal");
38979
39830
  }
38980
39831
  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
- );
39832
+ const changed = changedTopics(previous, topics);
38984
39833
  const serialized = JSON.stringify(next);
38985
39834
  if (Buffer.byteLength(serialized, "utf8") > MAX_BRAIN_DIGEST_STATE_BYTES) {
38986
39835
  throw new Error("brain digest state is larger than this store accepts");
@@ -39016,7 +39865,26 @@ function exactKeys2(row, keys) {
39016
39865
  const expected = new Set(keys);
39017
39866
  return Object.keys(row).length === expected.size && Object.keys(row).every((key2) => expected.has(key2));
39018
39867
  }
39019
- function parseListenerCredential(raw) {
39868
+ function hasRequiredKeys(row, keys) {
39869
+ return keys.every((key2) => Object.hasOwn(row, key2));
39870
+ }
39871
+ var LISTENER_CREDENTIAL_KEYS = [
39872
+ "version",
39873
+ "profileId",
39874
+ "targetUrl",
39875
+ "anonKey",
39876
+ "workspaceId",
39877
+ "principalId",
39878
+ "credential",
39879
+ "updatedAt"
39880
+ ];
39881
+ var HOOK_SURFACE_KEYS = /* @__PURE__ */ new Set([
39882
+ "version",
39883
+ "surfacedSignalIds",
39884
+ "reportedDroppedCount",
39885
+ "credentialFailureReported"
39886
+ ]);
39887
+ function parseListenerCredential(raw, rejectUnknownKeys = false) {
39020
39888
  let value;
39021
39889
  try {
39022
39890
  value = JSON.parse(raw);
@@ -39027,16 +39895,7 @@ function parseListenerCredential(raw) {
39027
39895
  throw new Error("stored listener hook credential is malformed");
39028
39896
  }
39029
39897
  const row = value;
39030
- if (!exactKeys2(row, [
39031
- "version",
39032
- "profileId",
39033
- "targetUrl",
39034
- "anonKey",
39035
- "workspaceId",
39036
- "principalId",
39037
- "credential",
39038
- "updatedAt"
39039
- ]) || row.version !== 1 || typeof row.profileId !== "string" || !/^[0-9a-f]{24}$/.test(row.profileId) || typeof row.targetUrl !== "string" || typeof row.anonKey !== "string" || row.anonKey.length < 1 || row.anonKey.length > 4096 || typeof row.workspaceId !== "string" || !UUID_RE21.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE21.test(row.principalId) || typeof row.credential !== "string" || !TOKEN_RE.test(row.credential) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt))) {
39898
+ if (!(rejectUnknownKeys ? exactKeys2(row, LISTENER_CREDENTIAL_KEYS) : hasRequiredKeys(row, LISTENER_CREDENTIAL_KEYS)) || row.version !== 1 || typeof row.profileId !== "string" || !/^[0-9a-f]{24}$/.test(row.profileId) || typeof row.targetUrl !== "string" || typeof row.anonKey !== "string" || row.anonKey.length < 1 || row.anonKey.length > 4096 || typeof row.workspaceId !== "string" || !UUID_RE21.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE21.test(row.principalId) || typeof row.credential !== "string" || !TOKEN_RE.test(row.credential) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt))) {
39040
39899
  throw new Error("stored listener hook credential is malformed");
39041
39900
  }
39042
39901
  const target2 = cloudTarget(row.targetUrl, row.anonKey);
@@ -39067,7 +39926,7 @@ async function writeListenerCredentialState(instanceDirectory, input) {
39067
39926
  principalId: input.principalId,
39068
39927
  credential: input.credential,
39069
39928
  updatedAt: new Date(input.now ?? Date.now()).toISOString()
39070
- }));
39929
+ }), true);
39071
39930
  await writeSecureJsonFile(
39072
39931
  (0, import_node_path20.join)(instanceDirectory, LISTENER_CREDENTIAL_FILE),
39073
39932
  JSON.stringify(record)
@@ -39083,7 +39942,7 @@ async function readListenerCredentialState(instanceDirectory) {
39083
39942
  );
39084
39943
  return raw === null ? null : parseListenerCredential(raw);
39085
39944
  }
39086
- function parseSurface(raw) {
39945
+ function parseSurface(raw, rejectUnknownKeys = false) {
39087
39946
  let value;
39088
39947
  try {
39089
39948
  value = JSON.parse(raw);
@@ -39094,9 +39953,7 @@ function parseSurface(raw) {
39094
39953
  throw new Error("stored listener hook surface state is malformed");
39095
39954
  }
39096
39955
  const row = value;
39097
- if (Object.keys(row).some(
39098
- (key2) => key2 !== "version" && key2 !== "surfacedSignalIds" && key2 !== "reportedDroppedCount" && key2 !== "credentialFailureReported"
39099
- ) || row.version !== 1 || !Array.isArray(row.surfacedSignalIds) || row.surfacedSignalIds.length > HOOK_SURFACED_IDS_MAX || row.surfacedSignalIds.some((id) => typeof id !== "string" || !UUID_RE21.test(id)) || !(row.reportedDroppedCount === void 0 || typeof row.reportedDroppedCount === "number" && Number.isSafeInteger(row.reportedDroppedCount) && row.reportedDroppedCount >= 0) || !(row.credentialFailureReported === void 0 || typeof row.credentialFailureReported === "boolean")) {
39956
+ if (rejectUnknownKeys && Object.keys(row).some((key2) => !HOOK_SURFACE_KEYS.has(key2)) || row.version !== 1 || !Array.isArray(row.surfacedSignalIds) || row.surfacedSignalIds.length > HOOK_SURFACED_IDS_MAX || row.surfacedSignalIds.some((id) => typeof id !== "string" || !UUID_RE21.test(id)) || !(row.reportedDroppedCount === void 0 || typeof row.reportedDroppedCount === "number" && Number.isSafeInteger(row.reportedDroppedCount) && row.reportedDroppedCount >= 0) || !(row.credentialFailureReported === void 0 || typeof row.credentialFailureReported === "boolean")) {
39100
39957
  throw new Error("stored listener hook surface state is malformed");
39101
39958
  }
39102
39959
  const ids = row.surfacedSignalIds.map((id) => String(id).toLowerCase());
@@ -39120,6 +39977,31 @@ var FileHookSurfaceStore = class {
39120
39977
  }
39121
39978
  instanceDirectory;
39122
39979
  path;
39980
+ /** Preview unseen hook rows without taking a write lock or advancing state. */
39981
+ async previewUnseen(items) {
39982
+ const raw = await readSecureJsonFileIfPresent(this.path, MAX_HOOK_SURFACE_BYTES);
39983
+ const seen = new Set(raw === null ? [] : parseSurface(raw).surfacedSignalIds);
39984
+ const unseen = [];
39985
+ for (const item of items) {
39986
+ const signalId = item.signalId.toLowerCase();
39987
+ if (!UUID_RE21.test(signalId) || seen.has(signalId)) continue;
39988
+ seen.add(signalId);
39989
+ unseen.push(item);
39990
+ }
39991
+ return unseen;
39992
+ }
39993
+ /** Read attendance evidence without advancing the hook high-water. */
39994
+ async evidence() {
39995
+ return await withFileLock(this.instanceDirectory, HOOK_SURFACE_LOCK, async () => {
39996
+ const raw = await readSecureJsonFile(this.path, MAX_HOOK_SURFACE_BYTES);
39997
+ if (raw === null) return { exists: false, surfacedSignalIds: [] };
39998
+ const state = parseSurface(raw);
39999
+ return {
40000
+ exists: true,
40001
+ surfacedSignalIds: state.surfacedSignalIds
40002
+ };
40003
+ }, { timeoutMs: HOOK_LOCK_TIMEOUT_MS });
40004
+ }
39123
40005
  async stage(items, droppedCount) {
39124
40006
  return await withFileLock(this.instanceDirectory, HOOK_SURFACE_LOCK, async () => {
39125
40007
  const raw = await readSecureJsonFile(this.path, MAX_HOOK_SURFACE_BYTES);
@@ -39165,7 +40047,7 @@ var FileHookSurfaceStore = class {
39165
40047
  surfacedSignalIds: [...seen].slice(-HOOK_SURFACED_IDS_MAX),
39166
40048
  reportedDroppedCount: options.droppedCount ?? state.reportedDroppedCount,
39167
40049
  credentialFailureReported: options.credentialFailureReported ?? state.credentialFailureReported
39168
- })))
40050
+ }), true))
39169
40051
  );
39170
40052
  }, { timeoutMs: HOOK_LOCK_TIMEOUT_MS });
39171
40053
  }
@@ -39188,7 +40070,7 @@ function parseGlobalState(raw) {
39188
40070
  throw new Error("stored hook cooldown state is malformed");
39189
40071
  }
39190
40072
  const row = value;
39191
- if (!exactKeys2(row, ["version", "lastCheckAt"]) || row.version !== 1 || typeof row.lastCheckAt !== "number" || !Number.isSafeInteger(row.lastCheckAt) || row.lastCheckAt < 0) {
40073
+ if (!hasRequiredKeys(row, ["version", "lastCheckAt"]) || row.version !== 1 || typeof row.lastCheckAt !== "number" || !Number.isSafeInteger(row.lastCheckAt) || row.lastCheckAt < 0) {
39192
40074
  throw new Error("stored hook cooldown state is malformed");
39193
40075
  }
39194
40076
  return { version: 1, lastCheckAt: row.lastCheckAt };
@@ -39643,6 +40525,950 @@ async function runListenerHookCheck(options = {}) {
39643
40525
  }
39644
40526
  }
39645
40527
 
40528
+ // src/listener/attendance-canary.ts
40529
+ var import_promises11 = require("node:fs/promises");
40530
+ var LOG_TAIL_BYTES = 256 * 1024;
40531
+ function agentReceipt(receipts, principalId) {
40532
+ for (const receipt of receipts) {
40533
+ if ("recipient_agent_principal_id" in receipt && receipt.recipient_agent_principal_id === principalId) {
40534
+ return receipt;
40535
+ }
40536
+ }
40537
+ return null;
40538
+ }
40539
+ async function readLogTail(path) {
40540
+ let handle;
40541
+ try {
40542
+ handle = await (0, import_promises11.open)(path, "r");
40543
+ } catch (error) {
40544
+ if (error.code === "ENOENT") return "";
40545
+ throw error;
40546
+ }
40547
+ try {
40548
+ const size2 = (await handle.stat()).size;
40549
+ const start = Math.max(0, size2 - LOG_TAIL_BYTES);
40550
+ const buffer2 = Buffer.alloc(size2 - start);
40551
+ await handle.read(buffer2, 0, buffer2.length, start);
40552
+ let text = buffer2.toString("utf8");
40553
+ if (start > 0) {
40554
+ const newline = text.indexOf("\n");
40555
+ text = newline < 0 ? "" : text.slice(newline + 1);
40556
+ }
40557
+ return text;
40558
+ } finally {
40559
+ await handle.close();
40560
+ }
40561
+ }
40562
+ async function logEvidence(path, signalId) {
40563
+ let claimedAt = null;
40564
+ let routeDecision = null;
40565
+ let routedAt = null;
40566
+ for (const line of (await readLogTail(path)).split("\n")) {
40567
+ if (line.length === 0) continue;
40568
+ let value;
40569
+ try {
40570
+ value = JSON.parse(line);
40571
+ } catch {
40572
+ continue;
40573
+ }
40574
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
40575
+ const row = value;
40576
+ if (row.signal_id !== signalId || typeof row.ts !== "string") continue;
40577
+ if (row.event === "listener_delivery_claim") claimedAt = row.ts;
40578
+ if (row.event === "listener_routing_decision" && (row.route_decision === "main" || row.route_decision === "worker")) {
40579
+ routeDecision = row.route_decision;
40580
+ routedAt = row.ts;
40581
+ }
40582
+ }
40583
+ return { claimedAt, routeDecision, routedAt };
40584
+ }
40585
+ async function runListenerAttendanceCanary(options) {
40586
+ const now = options.now ?? Date.now;
40587
+ const sleep2 = options.sleep ?? ((milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)));
40588
+ const startedAt = now();
40589
+ const deadlineMs = startedAt + options.waitMs;
40590
+ const client = new ThinCommandClient(options.target, options.fetcher, {
40591
+ signalRequestTimeoutMs: options.waitMs
40592
+ });
40593
+ const posted = await client.sendSignal({
40594
+ workspaceId: options.workspaceId,
40595
+ credential: await options.credential(),
40596
+ command: {
40597
+ kind: "post_signal",
40598
+ signal_kind: "note",
40599
+ body: "CommonSwarm listener attendance canary. No reply is needed.",
40600
+ to_user_id: null,
40601
+ to_agent_principal_id: options.principalId,
40602
+ in_reply_to: null,
40603
+ about: null,
40604
+ until_ms: 10 * 6e4
40605
+ }
40606
+ });
40607
+ const signalId = posted.response.signal.id;
40608
+ let claimedAt = null;
40609
+ let routeDecision = null;
40610
+ let routedAt = null;
40611
+ let pendingForMainCount = null;
40612
+ let surfacedAt = null;
40613
+ let observedAt = null;
40614
+ let receiptReadErrorCode = null;
40615
+ while (true) {
40616
+ const log = await logEvidence(options.paths.logPath, signalId);
40617
+ claimedAt ??= log.claimedAt;
40618
+ routeDecision ??= log.routeDecision;
40619
+ routedAt ??= log.routedAt;
40620
+ const hook = await new FileHookSurfaceStore(
40621
+ options.paths.instanceDirectory
40622
+ ).evidence();
40623
+ if (hook.surfacedSignalIds.includes(signalId)) {
40624
+ surfacedAt ??= new Date(now()).toISOString();
40625
+ }
40626
+ if (now() >= deadlineMs) break;
40627
+ try {
40628
+ const report = await readAgentDeliveryReceipts(
40629
+ options.target,
40630
+ await options.credential(),
40631
+ options.workspaceId,
40632
+ signalId,
40633
+ {
40634
+ ...options.fetcher ? { fetcher: options.fetcher } : {},
40635
+ deadlineMs,
40636
+ now
40637
+ }
40638
+ );
40639
+ receiptReadErrorCode = null;
40640
+ const receipt = agentReceipt(report.receipts, options.principalId);
40641
+ if (receipt !== null) {
40642
+ claimedAt ??= receipt.delivered_at;
40643
+ if (receipt.ack_outcome === "queued") {
40644
+ routeDecision ??= "main";
40645
+ routedAt ??= receipt.acked_at;
40646
+ pendingForMainCount = receipt.pending_for_main_count ?? null;
40647
+ }
40648
+ const state = deliveryReceiptState(receipt, now());
40649
+ if (state === "observed" || state === "replied") {
40650
+ observedAt = receipt.acked_at;
40651
+ }
40652
+ }
40653
+ } catch (error) {
40654
+ receiptReadErrorCode = error instanceof DeliveryReceiptReadError ? error.code : error instanceof SignalReadTimeoutError ? "timeout" : "transport";
40655
+ }
40656
+ const complete = claimedAt !== null && routeDecision !== null && (routeDecision === "worker" || surfacedAt !== null) && observedAt !== null;
40657
+ if (complete || now() >= deadlineMs) break;
40658
+ await sleep2(Math.min(options.pollMs ?? 250, deadlineMs - now()));
40659
+ }
40660
+ const stalledAt = claimedAt === null ? "claimed" : routeDecision === null ? "routed" : routeDecision === "main" && surfacedAt === null ? "surfaced" : observedAt === null ? "observed" : null;
40661
+ return {
40662
+ signalId,
40663
+ acceptedAt: new Date(startedAt).toISOString(),
40664
+ claimedAt,
40665
+ routeDecision,
40666
+ routedAt,
40667
+ pendingForMainCount,
40668
+ surfacedAt,
40669
+ observedAt,
40670
+ receiptReadErrorCode,
40671
+ stalledAt
40672
+ };
40673
+ }
40674
+ function renderListenerAttendanceCanary(result, workspaceId2, principalId) {
40675
+ const statusCommand = `cswarm listen status --workspace-id ${workspaceId2} --principal-id ${principalId}`;
40676
+ 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";
40677
+ const lines = [
40678
+ `Canary note: ${result.signalId}.`,
40679
+ `ACCEPTED: yes at ${result.acceptedAt}.`,
40680
+ `CLAIMED: ${result.claimedAt === null ? "no" : `yes at ${result.claimedAt}`}.`,
40681
+ `QUEUED/WORKER: ${route}.`,
40682
+ `SURFACED: ${result.routeDecision === "worker" ? "not required for the worker route" : result.surfacedAt === null ? "no" : `yes at ${result.surfacedAt}`}.`,
40683
+ `OBSERVED: ${result.observedAt === null ? "no" : `yes at ${result.observedAt}`}.`
40684
+ ];
40685
+ if (result.receiptReadErrorCode !== null) {
40686
+ lines.push(`RECEIPT READ: failed (${result.receiptReadErrorCode}).`);
40687
+ }
40688
+ if (result.stalledAt === null) {
40689
+ lines.push("Canary passed: every required hop was measured.");
40690
+ } else if (result.stalledAt === "surfaced") {
40691
+ lines.push(
40692
+ `STALLED: surfaced. Next: cswarm hook install claude --principal-id ${principalId} --write, then start a fresh session. Or restart the listener with --route worker.`
40693
+ );
40694
+ } else {
40695
+ lines.push(`STALLED: ${result.stalledAt}. Next: ${statusCommand}`);
40696
+ }
40697
+ return lines.join("\n");
40698
+ }
40699
+
40700
+ // src/listener/activity.ts
40701
+ var import_node_crypto20 = require("node:crypto");
40702
+ var ACTIVITY_FRAME_INTERVAL_MS = 750;
40703
+ var ACTIVITY_HEARTBEAT_MS = 15e3;
40704
+ var ACTIVITY_TOOL_TITLE_MAX = 160;
40705
+ var ACTIVITY_REQUEST_TIMEOUT_MS = 5e3;
40706
+ var SYSTEM_CLOCK = {
40707
+ now: Date.now,
40708
+ setTimer: (callback, delayMs) => setTimeout(callback, delayMs),
40709
+ clearTimer: (timer2) => clearTimeout(timer2)
40710
+ };
40711
+ var TERMINAL_TOOL_STATUSES = /* @__PURE__ */ new Set([
40712
+ "cancelled",
40713
+ "completed",
40714
+ "done",
40715
+ "failed",
40716
+ "rejected"
40717
+ ]);
40718
+ var AgentActivityEndpointTransport = class {
40719
+ constructor(target2, credentialSession, fetcher) {
40720
+ this.target = target2;
40721
+ this.credentialSession = credentialSession;
40722
+ this.fetcher = fetcher ?? fetch;
40723
+ }
40724
+ target;
40725
+ credentialSession;
40726
+ fetcher;
40727
+ async publish(frame) {
40728
+ const credential = await this.credentialSession.bearer();
40729
+ const response = await this.fetcher(
40730
+ `${this.target.url}/functions/v1/activity`,
40731
+ {
40732
+ method: "POST",
40733
+ headers: {
40734
+ authorization: `Bearer ${credential}`,
40735
+ apikey: this.target.anonKey,
40736
+ "content-type": "application/json"
40737
+ },
40738
+ body: JSON.stringify({
40739
+ version: frame.version,
40740
+ workspace_id: frame.workspaceId,
40741
+ stream_id: frame.streamId,
40742
+ sequence: frame.sequence,
40743
+ phase: frame.phase,
40744
+ signal_id: frame.signalId,
40745
+ tool_title: frame.toolTitle,
40746
+ elapsed_ms: frame.elapsedMs
40747
+ }),
40748
+ signal: AbortSignal.timeout(ACTIVITY_REQUEST_TIMEOUT_MS)
40749
+ }
40750
+ );
40751
+ await response.body?.cancel();
40752
+ if (!response.ok) {
40753
+ throw new Error(`activity publish failed (${response.status})`);
40754
+ }
40755
+ }
40756
+ };
40757
+ var ListenerActivityController = class {
40758
+ constructor(options) {
40759
+ this.options = options;
40760
+ this.clock = options.clock ?? SYSTEM_CLOCK;
40761
+ this.streamId = options.streamId ?? (0, import_node_crypto20.randomUUID)();
40762
+ this.events = { update: (update) => this.onSessionUpdate(update) };
40763
+ }
40764
+ options;
40765
+ events;
40766
+ clock;
40767
+ streamId;
40768
+ sequence = 0;
40769
+ phase = "idle";
40770
+ signalId = null;
40771
+ signalStartedAt = null;
40772
+ runningTools = /* @__PURE__ */ new Map();
40773
+ latestToolId = null;
40774
+ dirty = false;
40775
+ sending = false;
40776
+ closed = false;
40777
+ lastSentAt = Number.NEGATIVE_INFINITY;
40778
+ timer = null;
40779
+ heartbeatTimer = null;
40780
+ /** Observe the listener state machine without changing its durable effect path. */
40781
+ onRuntimeEvent(event) {
40782
+ if (event.type === "ready") {
40783
+ this.setIdle();
40784
+ } else if (event.type === "delivery_claim" && event.signalId !== null) {
40785
+ this.beginSignal(event.signalId);
40786
+ } else if (event.type === "routing_decision") {
40787
+ this.beginSignal(event.signalId);
40788
+ } else if (event.type === "effect") {
40789
+ this.setIdle();
40790
+ }
40791
+ }
40792
+ /** Wrap one provider model so prompt and reply phases are visible. */
40793
+ instrumentModel(model) {
40794
+ return {
40795
+ start: async () => await model.start(),
40796
+ prompt: async (signal, mode3, prompt, attempt) => {
40797
+ this.beginSignal(signal.id);
40798
+ this.setPhase("prompting");
40799
+ const result = await model.prompt(signal, mode3, prompt, attempt);
40800
+ this.runningTools.clear();
40801
+ this.latestToolId = null;
40802
+ this.setPhase("replying");
40803
+ return result;
40804
+ },
40805
+ cancel: () => model.cancel(),
40806
+ close: async () => await model.close()
40807
+ };
40808
+ }
40809
+ /** Stop pending local timers; a missing next frame becomes stale in the panel. */
40810
+ close() {
40811
+ this.closed = true;
40812
+ if (this.timer !== null) this.clock.clearTimer(this.timer);
40813
+ if (this.heartbeatTimer !== null) this.clock.clearTimer(this.heartbeatTimer);
40814
+ this.timer = null;
40815
+ this.heartbeatTimer = null;
40816
+ }
40817
+ onSessionUpdate(update) {
40818
+ if (this.signalId === null) return;
40819
+ if (update.kind !== "tool_call" && update.kind !== "tool_call_update") return;
40820
+ const id = update.toolCallId;
40821
+ if (!id) return;
40822
+ const status = update.status?.toLowerCase();
40823
+ if (status && TERMINAL_TOOL_STATUSES.has(status)) {
40824
+ this.runningTools.delete(id);
40825
+ if (this.latestToolId === id) {
40826
+ this.latestToolId = [...this.runningTools.keys()].at(-1) ?? null;
40827
+ }
40828
+ this.setPhase(this.runningTools.size > 0 ? "tool-running" : "prompting");
40829
+ return;
40830
+ }
40831
+ const sanitizedTitle = update.title === void 0 ? this.runningTools.get(id) : sanitizeText(update.title).slice(0, ACTIVITY_TOOL_TITLE_MAX);
40832
+ const title = sanitizedTitle && sanitizedTitle.length > 0 ? sanitizedTitle : "Tool";
40833
+ this.runningTools.set(id, title);
40834
+ this.latestToolId = id;
40835
+ this.setPhase("tool-running");
40836
+ }
40837
+ beginSignal(signalId) {
40838
+ if (this.signalId === signalId) return;
40839
+ this.signalId = signalId;
40840
+ this.signalStartedAt = this.clock.now();
40841
+ this.runningTools.clear();
40842
+ this.latestToolId = null;
40843
+ this.setPhase("claimed");
40844
+ }
40845
+ setIdle() {
40846
+ this.signalId = null;
40847
+ this.signalStartedAt = null;
40848
+ this.runningTools.clear();
40849
+ this.latestToolId = null;
40850
+ this.setPhase("idle");
40851
+ }
40852
+ setPhase(phase) {
40853
+ this.phase = phase;
40854
+ if (this.heartbeatTimer !== null) {
40855
+ this.clock.clearTimer(this.heartbeatTimer);
40856
+ this.heartbeatTimer = null;
40857
+ }
40858
+ this.dirty = true;
40859
+ this.schedule();
40860
+ }
40861
+ armHeartbeat() {
40862
+ if (this.closed || this.heartbeatTimer !== null) return;
40863
+ this.heartbeatTimer = this.clock.setTimer(() => {
40864
+ this.heartbeatTimer = null;
40865
+ this.dirty = true;
40866
+ this.schedule();
40867
+ }, ACTIVITY_HEARTBEAT_MS);
40868
+ }
40869
+ schedule() {
40870
+ if (this.closed || this.timer !== null || this.sending) return;
40871
+ const delay2 = Math.max(
40872
+ 0,
40873
+ this.lastSentAt + ACTIVITY_FRAME_INTERVAL_MS - this.clock.now()
40874
+ );
40875
+ if (delay2 === 0) {
40876
+ void this.flush();
40877
+ return;
40878
+ }
40879
+ this.timer = this.clock.setTimer(() => {
40880
+ this.timer = null;
40881
+ void this.flush();
40882
+ }, delay2);
40883
+ }
40884
+ async flush() {
40885
+ if (this.closed || this.sending || !this.dirty) return;
40886
+ this.dirty = false;
40887
+ this.sending = true;
40888
+ this.lastSentAt = this.clock.now();
40889
+ const toolTitle = this.latestToolId === null ? null : this.runningTools.get(this.latestToolId) ?? null;
40890
+ const frame = {
40891
+ version: 1,
40892
+ workspaceId: this.options.workspaceId,
40893
+ streamId: this.streamId,
40894
+ sequence: ++this.sequence,
40895
+ phase: this.phase,
40896
+ signalId: this.signalId,
40897
+ toolTitle,
40898
+ elapsedMs: this.signalStartedAt === null ? 0 : Math.max(0, Math.round(this.clock.now() - this.signalStartedAt))
40899
+ };
40900
+ try {
40901
+ await this.options.transport.publish(frame);
40902
+ } catch {
40903
+ } finally {
40904
+ this.sending = false;
40905
+ if (this.dirty) this.schedule();
40906
+ else this.armHeartbeat();
40907
+ }
40908
+ }
40909
+ };
40910
+
40911
+ // src/listener/http-client.ts
40912
+ var import_node_http2 = require("node:http");
40913
+ var import_node_https = require("node:https");
40914
+ var import_node_zlib = require("node:zlib");
40915
+ var LISTENER_HTTP_IDLE_TIMEOUT_MS = 6e4;
40916
+ function responseHeaders(message) {
40917
+ const headers = new Headers();
40918
+ for (let index = 0; index < message.rawHeaders.length; index += 2) {
40919
+ const name = message.rawHeaders[index];
40920
+ const value = message.rawHeaders[index + 1];
40921
+ if (name !== void 0 && value !== void 0) headers.append(name, value);
40922
+ }
40923
+ return headers;
40924
+ }
40925
+ function decodeResponseBody(bytes, headers) {
40926
+ const codings = (headers.get("content-encoding") ?? "").split(",").map((coding) => coding.trim().toLowerCase()).filter((coding) => coding !== "" && coding !== "identity");
40927
+ if (!codings.every((coding) => ["br", "deflate", "gzip", "x-gzip"].includes(coding))) {
40928
+ return Uint8Array.from(bytes).buffer;
40929
+ }
40930
+ let decoded = bytes;
40931
+ for (const coding of codings.reverse()) {
40932
+ if (coding === "br") decoded = (0, import_node_zlib.brotliDecompressSync)(decoded);
40933
+ else if (coding === "deflate") decoded = (0, import_node_zlib.inflateSync)(decoded);
40934
+ else decoded = (0, import_node_zlib.gunzipSync)(decoded);
40935
+ }
40936
+ return Uint8Array.from(decoded).buffer;
40937
+ }
40938
+ var ListenerHttpClient = class {
40939
+ fetch;
40940
+ httpAgent = new import_node_http2.Agent({
40941
+ keepAlive: true,
40942
+ maxSockets: 1,
40943
+ maxFreeSockets: 1,
40944
+ scheduling: "fifo"
40945
+ });
40946
+ httpsAgent = new import_node_https.Agent({
40947
+ keepAlive: true,
40948
+ maxSockets: 1,
40949
+ maxFreeSockets: 1,
40950
+ scheduling: "fifo"
40951
+ });
40952
+ idleTimeoutMs;
40953
+ sockets = /* @__PURE__ */ new WeakSet();
40954
+ requestCount = 0;
40955
+ openedCount = 0;
40956
+ activeRequests = 0;
40957
+ idleTimer = null;
40958
+ closed = false;
40959
+ constructor(options = {}) {
40960
+ const idleTimeoutMs = options.idleTimeoutMs ?? LISTENER_HTTP_IDLE_TIMEOUT_MS;
40961
+ if (!Number.isSafeInteger(idleTimeoutMs) || idleTimeoutMs <= 0) {
40962
+ throw new Error("listener HTTP idle timeout must be a positive integer");
40963
+ }
40964
+ this.idleTimeoutMs = idleTimeoutMs;
40965
+ this.fetch = this.request.bind(this);
40966
+ }
40967
+ /** Snapshot process-local counts without exposing the agents themselves. */
40968
+ metrics() {
40969
+ return {
40970
+ requests: this.requestCount,
40971
+ connectionsOpened: this.openedCount,
40972
+ connectionReuseRatio: this.openedCount === 0 ? 0 : this.requestCount / this.openedCount
40973
+ };
40974
+ }
40975
+ /** Close every idle or active socket when the listener process stops. */
40976
+ close() {
40977
+ if (this.closed) return;
40978
+ this.closed = true;
40979
+ this.clearIdleTimer();
40980
+ this.destroyAgents();
40981
+ }
40982
+ clearIdleTimer() {
40983
+ if (this.idleTimer === null) return;
40984
+ clearTimeout(this.idleTimer);
40985
+ this.idleTimer = null;
40986
+ }
40987
+ destroyAgents() {
40988
+ this.httpAgent.destroy();
40989
+ this.httpsAgent.destroy();
40990
+ }
40991
+ beginRequest() {
40992
+ if (this.closed) throw new Error("listener HTTP client is closed");
40993
+ this.clearIdleTimer();
40994
+ this.requestCount += 1;
40995
+ this.activeRequests += 1;
40996
+ }
40997
+ finishRequest() {
40998
+ this.activeRequests -= 1;
40999
+ if (this.activeRequests !== 0 || this.closed) return;
41000
+ this.idleTimer = setTimeout(() => {
41001
+ this.idleTimer = null;
41002
+ if (this.activeRequests === 0 && !this.closed) this.destroyAgents();
41003
+ }, this.idleTimeoutMs);
41004
+ this.idleTimer.unref?.();
41005
+ }
41006
+ trackSocket(request) {
41007
+ request.once("socket", (socket) => {
41008
+ if (this.sockets.has(socket)) return;
41009
+ this.sockets.add(socket);
41010
+ const opened = () => {
41011
+ this.openedCount += 1;
41012
+ };
41013
+ if (socket.connecting) socket.once("connect", opened);
41014
+ else opened();
41015
+ });
41016
+ }
41017
+ async request(input, init) {
41018
+ const webRequest = new Request(input, init);
41019
+ const body = webRequest.method === "GET" || webRequest.method === "HEAD" ? null : Buffer.from(await webRequest.arrayBuffer());
41020
+ let url = new URL(webRequest.url);
41021
+ let method = webRequest.method;
41022
+ let headers = new Headers(webRequest.headers);
41023
+ let redirected = false;
41024
+ for (let redirects = 0; ; redirects += 1) {
41025
+ const response = await this.sendOnce({
41026
+ url,
41027
+ method,
41028
+ headers,
41029
+ body: method === "GET" || method === "HEAD" ? null : body,
41030
+ signal: webRequest.signal,
41031
+ redirected
41032
+ });
41033
+ const location2 = response.headers.get("location");
41034
+ if (location2 === null || ![301, 302, 303, 307, 308].includes(response.status) || webRequest.redirect === "manual") {
41035
+ return response;
41036
+ }
41037
+ if (webRequest.redirect === "error" || redirects >= 20) {
41038
+ throw new TypeError("fetch failed while following a redirect");
41039
+ }
41040
+ const nextUrl = new URL(location2, url);
41041
+ if (nextUrl.protocol !== "http:" && nextUrl.protocol !== "https:") {
41042
+ throw new TypeError("listener HTTP client accepts only http and https URLs");
41043
+ }
41044
+ const rewriteToGet = response.status === 303 && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST";
41045
+ if (rewriteToGet) {
41046
+ method = "GET";
41047
+ for (const name of [
41048
+ "content-encoding",
41049
+ "content-language",
41050
+ "content-length",
41051
+ "content-location",
41052
+ "content-type",
41053
+ "transfer-encoding"
41054
+ ]) {
41055
+ headers.delete(name);
41056
+ }
41057
+ }
41058
+ if (nextUrl.origin !== url.origin) {
41059
+ for (const name of [
41060
+ "apikey",
41061
+ "authorization",
41062
+ "cookie",
41063
+ "host",
41064
+ "proxy-authorization"
41065
+ ]) {
41066
+ headers.delete(name);
41067
+ }
41068
+ }
41069
+ url = nextUrl;
41070
+ redirected = true;
41071
+ }
41072
+ }
41073
+ async sendOnce(options) {
41074
+ if (options.url.protocol !== "http:" && options.url.protocol !== "https:") {
41075
+ throw new TypeError("listener HTTP client accepts only http and https URLs");
41076
+ }
41077
+ const headers = Object.fromEntries(options.headers.entries());
41078
+ if (!("accept-encoding" in headers)) headers["accept-encoding"] = "gzip, deflate";
41079
+ if (options.body !== null && !("content-length" in headers) && !("transfer-encoding" in headers)) {
41080
+ headers["content-length"] = String(options.body.byteLength);
41081
+ }
41082
+ this.beginRequest();
41083
+ let finished = false;
41084
+ const finish = () => {
41085
+ if (finished) return;
41086
+ finished = true;
41087
+ this.finishRequest();
41088
+ };
41089
+ return await new Promise((resolve2, reject) => {
41090
+ const send = options.url.protocol === "https:" ? import_node_https.request : import_node_http2.request;
41091
+ const agent = options.url.protocol === "https:" ? this.httpsAgent : this.httpAgent;
41092
+ let request;
41093
+ try {
41094
+ request = send(options.url, {
41095
+ agent,
41096
+ method: options.method,
41097
+ headers,
41098
+ signal: options.signal
41099
+ });
41100
+ } catch (error) {
41101
+ finish();
41102
+ reject(error);
41103
+ return;
41104
+ }
41105
+ this.trackSocket(request);
41106
+ request.once("error", (error) => {
41107
+ finish();
41108
+ reject(error);
41109
+ });
41110
+ request.once("response", (message) => {
41111
+ const chunks = [];
41112
+ let settled = false;
41113
+ const rejectResponse = (error) => {
41114
+ if (settled) return;
41115
+ settled = true;
41116
+ finish();
41117
+ reject(error);
41118
+ };
41119
+ message.on("data", (chunk) => {
41120
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
41121
+ });
41122
+ message.once("aborted", () => {
41123
+ rejectResponse(new TypeError("response body was aborted"));
41124
+ });
41125
+ message.once("error", rejectResponse);
41126
+ message.once("end", () => {
41127
+ if (settled) return;
41128
+ settled = true;
41129
+ finish();
41130
+ const status = message.statusCode ?? 0;
41131
+ const bytes = Buffer.concat(chunks);
41132
+ const webHeaders = responseHeaders(message);
41133
+ const responseBody2 = status === 204 || status === 205 || status === 304 ? null : decodeResponseBody(bytes, webHeaders);
41134
+ try {
41135
+ const response = new Response(responseBody2, {
41136
+ status,
41137
+ statusText: message.statusMessage,
41138
+ headers: webHeaders
41139
+ });
41140
+ Object.defineProperties(response, {
41141
+ redirected: { value: options.redirected },
41142
+ url: { value: options.url.href }
41143
+ });
41144
+ resolve2(response);
41145
+ } catch (error) {
41146
+ reject(error);
41147
+ }
41148
+ });
41149
+ });
41150
+ try {
41151
+ if (options.body !== null && options.body.byteLength > 0) {
41152
+ request.write(options.body);
41153
+ }
41154
+ request.end();
41155
+ } catch (error) {
41156
+ request.destroy();
41157
+ finish();
41158
+ reject(error);
41159
+ }
41160
+ });
41161
+ }
41162
+ };
41163
+
41164
+ // src/resume.ts
41165
+ var import_node_child_process8 = require("node:child_process");
41166
+ function execFileText(file, args) {
41167
+ return new Promise((resolve2, reject) => {
41168
+ (0, import_node_child_process8.execFile)(file, [...args], {
41169
+ encoding: "utf8",
41170
+ maxBuffer: 4 * 1024 * 1024
41171
+ }, (error, stdout) => {
41172
+ if (error) reject(error);
41173
+ else resolve2(stdout);
41174
+ });
41175
+ });
41176
+ }
41177
+ function systemProcessTable() {
41178
+ return {
41179
+ async list() {
41180
+ const output = await execFileText("ps", ["-axo", "pid=,command="]);
41181
+ return output.split("\n").flatMap((line) => {
41182
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
41183
+ if (!match) return [];
41184
+ const pid = Number(match[1]);
41185
+ return Number.isSafeInteger(pid) && pid > 0 ? [{ pid, command: match[2] }] : [];
41186
+ });
41187
+ }
41188
+ };
41189
+ }
41190
+ function lsofStdoutConsumer() {
41191
+ return {
41192
+ async inspect(pid) {
41193
+ let output;
41194
+ try {
41195
+ output = await execFileText(
41196
+ process.platform === "darwin" ? "/usr/sbin/lsof" : "lsof",
41197
+ ["-nP", "-a", "-p", String(pid), "-d", "1", "-F", "pftan"]
41198
+ );
41199
+ } catch {
41200
+ return "cannot_determine";
41201
+ }
41202
+ const lines = output.split("\n");
41203
+ const type = lines.find((line) => line.startsWith("t"))?.slice(1) ?? "";
41204
+ const names = lines.filter((line) => line.startsWith("n")).map((line) => line.slice(1));
41205
+ if (type === "unix") {
41206
+ if (names.some((name) => name === "->(none)")) return "orphaned";
41207
+ if (names.some((name) => name.startsWith("->") && name !== "->(none)")) {
41208
+ return "live_reader";
41209
+ }
41210
+ return "cannot_determine";
41211
+ }
41212
+ if (type === "PIPE" || type === "FIFO") return "cannot_determine";
41213
+ return type.length === 0 ? "cannot_determine" : "not_pipe";
41214
+ }
41215
+ };
41216
+ }
41217
+ function commandHasFlagValue(command2, flag, values2) {
41218
+ for (const value of values2) {
41219
+ const marker = `${flag} ${value}`;
41220
+ let start = command2.indexOf(marker);
41221
+ while (start !== -1) {
41222
+ const before = start === 0 ? " " : command2[start - 1];
41223
+ const afterIndex = start + marker.length;
41224
+ const after = afterIndex >= command2.length ? " " : command2[afterIndex];
41225
+ if (/\s/.test(before) && /\s/.test(after)) return true;
41226
+ start = command2.indexOf(marker, start + 1);
41227
+ }
41228
+ }
41229
+ return false;
41230
+ }
41231
+ function isNotifyCommand(command2) {
41232
+ return /(?:^|\s)inbox(?:\s|$)/.test(command2) && /(?:^|\s)--notify(?:\s|$)/.test(command2);
41233
+ }
41234
+ async function findNotifyWatchers(options) {
41235
+ const processTable = options.processTable ?? systemProcessTable();
41236
+ const stdoutConsumer = options.stdoutConsumer ?? lsofStdoutConsumer();
41237
+ const rows3 = await processTable.list();
41238
+ const matches = rows3.flatMap((row) => {
41239
+ if (!isNotifyCommand(row.command)) return [];
41240
+ const matchedBy = [];
41241
+ if (commandHasFlagValue(row.command, "--agent-token-file", options.credentialPaths)) {
41242
+ matchedBy.push("agent_token_file");
41243
+ }
41244
+ if (commandHasFlagValue(row.command, "--principal-id", [options.principalId])) {
41245
+ matchedBy.push("principal_id");
41246
+ }
41247
+ return matchedBy.length === 0 ? [] : [{ pid: row.pid, matchedBy }];
41248
+ });
41249
+ const unique = [...new Map(matches.map((row) => [row.pid, row])).values()].sort((left, right) => left.pid - right.pid);
41250
+ return await Promise.all(unique.map(async (row) => ({
41251
+ ...row,
41252
+ stdout: await stdoutConsumer.inspect(row.pid)
41253
+ })));
41254
+ }
41255
+ async function readOnlyListenerInspection(paths, adapters = {}) {
41256
+ const query = adapters.queryStatus ?? queryListenerControl;
41257
+ const read = adapters.readStatus ?? readListenerStatusIfPresent;
41258
+ try {
41259
+ return {
41260
+ checkedDirectory: paths.instanceDirectory,
41261
+ status: await query(paths, "status"),
41262
+ source: "live_process"
41263
+ };
41264
+ } catch {
41265
+ const status = await read(paths);
41266
+ return {
41267
+ checkedDirectory: paths.instanceDirectory,
41268
+ status,
41269
+ source: status === null ? "not_found" : "recorded_file"
41270
+ };
41271
+ }
41272
+ }
41273
+ async function inspectResume(options, adapters) {
41274
+ const identity = await adapters.readIdentity();
41275
+ const principalId = identity.principalId.toLowerCase();
41276
+ const paths = listenerPaths({
41277
+ profileId: options.target.profileId,
41278
+ workspaceId: options.workspaceId,
41279
+ principalId,
41280
+ ...options.stateDirectory ? { stateDirectory: options.stateDirectory } : {}
41281
+ });
41282
+ const listener = await readOnlyListenerInspection(paths, adapters);
41283
+ const watchers = await findNotifyWatchers({
41284
+ credentialPaths: options.credentialPathAliases ?? [options.credentialFile],
41285
+ principalId,
41286
+ ...adapters.processTable ? { processTable: adapters.processTable } : {},
41287
+ ...adapters.stdoutConsumer ? { stdoutConsumer: adapters.stdoutConsumer } : {}
41288
+ });
41289
+ const topics = await adapters.readBrainTopics();
41290
+ const digestStore = new FileBrainDigestStore(paths.instanceDirectory, principalId);
41291
+ const digest = await digestStore.preview(topics);
41292
+ const inbox = await adapters.readInboxCount(principalId, paths.instanceDirectory);
41293
+ return {
41294
+ identity: { ...identity, principalId },
41295
+ listener,
41296
+ watchers,
41297
+ brain: { digest, highWaterFile: digestStore.location },
41298
+ inbox,
41299
+ target: options.target,
41300
+ workspaceId: options.workspaceId,
41301
+ credentialFile: options.credentialFile,
41302
+ installedVersion: options.installedVersion
41303
+ };
41304
+ }
41305
+ function safeText(value) {
41306
+ return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ").slice(0, 2e3);
41307
+ }
41308
+ function shellArg(value) {
41309
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
41310
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
41311
+ }
41312
+ function commonCommandArgs(report) {
41313
+ return [
41314
+ "--agent-token-file",
41315
+ shellArg(report.credentialFile),
41316
+ "--url",
41317
+ shellArg(report.target.url),
41318
+ "--anon-key",
41319
+ shellArg(report.target.anonKey),
41320
+ "--workspace-id",
41321
+ report.workspaceId
41322
+ ].join(" ");
41323
+ }
41324
+ function restartCommand(report, status) {
41325
+ const common = commonCommandArgs(report);
41326
+ const route = status.routeMode ?? "worker";
41327
+ const start = [
41328
+ "cswarm listen start",
41329
+ common,
41330
+ `--provider ${status.provider}`,
41331
+ `--permissions ${status.permissionMode ?? "allow"}`,
41332
+ `--route ${route}`,
41333
+ ...route === "split" && status.deferOverChars !== null && status.deferOverChars !== void 0 ? [`--defer-over ${status.deferOverChars}`] : []
41334
+ ].join(" ");
41335
+ return `cswarm listen stop ${common} && ${start}`;
41336
+ }
41337
+ function watcherStateLine(watcher) {
41338
+ const matched = watcher.matchedBy.map(
41339
+ (value) => value === "agent_token_file" ? "credential path" : "principal id"
41340
+ ).join(" and ");
41341
+ 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";
41342
+ return `- PID ${watcher.pid}: ${state}; matched ${matched}.`;
41343
+ }
41344
+ function renderResume(report) {
41345
+ const lines = [
41346
+ "Identity",
41347
+ `You are ${safeText(report.identity.displayName)} (${report.identity.principalId}).`,
41348
+ "Next: use this principal for every listener, watcher, brain, and inbox check below.",
41349
+ "",
41350
+ "Listener"
41351
+ ];
41352
+ const listener = report.listener;
41353
+ const status = listener.status;
41354
+ if (status === null) {
41355
+ lines.push(
41356
+ `No listener found under ${safeText(listener.checkedDirectory)} for profile ${report.target.profileId}.`,
41357
+ `Next: start one with the original provider: cswarm listen start ${commonCommandArgs(report)} --provider <provider>`
41358
+ );
41359
+ } else {
41360
+ if (listener.source === "live_process") {
41361
+ lines.push(
41362
+ `Found under ${safeText(listener.checkedDirectory)}. State: ${status.state}, reported by running PID ${status.pid}.`
41363
+ );
41364
+ } else {
41365
+ lines.push(
41366
+ `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.`
41367
+ );
41368
+ }
41369
+ const runningVersion = status.cswarmVersion ?? null;
41370
+ if (listener.source !== "live_process") {
41371
+ lines.push(
41372
+ `Running listener cswarm version: cannot determine because the process did not answer; status file recorded ${runningVersion ?? "no version"}; installed CLI: ${report.installedVersion}.`,
41373
+ `Next: restart the listener because its process did not answer: ${restartCommand(report, status)}`
41374
+ );
41375
+ } else if (runningVersion === null) {
41376
+ lines.push(
41377
+ `Listener cswarm version: cannot determine from this listener; installed CLI: ${report.installedVersion}.`,
41378
+ `Next: restart it to make the running version reportable: ${restartCommand(report, status)}`
41379
+ );
41380
+ } else if (runningVersion !== report.installedVersion) {
41381
+ lines.push(
41382
+ `VERSION MISMATCH: listener runs ${runningVersion}; installed ${report.installedVersion} \u2014 restart it: ${restartCommand(report, status)}`
41383
+ );
41384
+ } else {
41385
+ lines.push(
41386
+ `Listener-reported cswarm: ${runningVersion}; installed CLI: ${report.installedVersion}.`,
41387
+ "Next: no listener restart is needed for a version change."
41388
+ );
41389
+ }
41390
+ }
41391
+ lines.push(
41392
+ "",
41393
+ "Notify watchers",
41394
+ `Checked process arguments for inbox --notify matching --agent-token-file ${safeText(report.credentialFile)} or --principal-id ${report.identity.principalId}.`
41395
+ );
41396
+ if (report.watchers.length === 0) {
41397
+ lines.push(
41398
+ "Found: 0.",
41399
+ `Next: start one watcher under a live Monitor: cswarm inbox --notify ${commonCommandArgs(report)}`
41400
+ );
41401
+ } else {
41402
+ lines.push(`Found: ${report.watchers.length}.`);
41403
+ lines.push(...report.watchers.map(watcherStateLine));
41404
+ const orphans = report.watchers.filter((watcher) => watcher.stdout === "orphaned");
41405
+ if (orphans.length > 0) {
41406
+ lines.push(
41407
+ `Next: stop only the orphan watcher${orphans.length === 1 ? "" : "s"}; CommonSwarm did not kill anything: kill ${orphans.map((watcher) => watcher.pid).join(" ")}`
41408
+ );
41409
+ } else if (report.watchers.some((watcher) => watcher.stdout === "cannot_determine")) {
41410
+ lines.push(
41411
+ "Next: verify each unknown stdout reader in the host Monitor before you start another watcher."
41412
+ );
41413
+ } else {
41414
+ lines.push("Next: keep one watcher with a live output surface; do not start a duplicate.");
41415
+ }
41416
+ }
41417
+ lines.push(
41418
+ "",
41419
+ "Brain digest",
41420
+ `Checked ${safeText(report.brain.highWaterFile)} without advancing it.`
41421
+ );
41422
+ if (report.brain.digest === null) {
41423
+ lines.push(
41424
+ "No brain topic is new or changed since this principal's digest high-water.",
41425
+ "Next: no brain read is needed now."
41426
+ );
41427
+ } else {
41428
+ lines.push(report.brain.digest, "Next: read any needed topic with the command above.");
41429
+ }
41430
+ const inboxCount = report.inbox.exact ? String(report.inbox.count) : `at least ${report.inbox.count}`;
41431
+ lines.push(
41432
+ "",
41433
+ "Unread inbox",
41434
+ `Unread directed asks and notes from the same read used by the hook: ${inboxCount}.`,
41435
+ report.inbox.count === 0 ? "Next: no inbox action is needed now." : `Next: read them without acknowledging them first: cswarm inbox ${commonCommandArgs(report)}`,
41436
+ "",
41437
+ "Read-only check complete. No cursor, brain high-water, listener status, receipt, acknowledgement, or process was changed."
41438
+ );
41439
+ return lines.join("\n");
41440
+ }
41441
+ function resumeJson(report) {
41442
+ return {
41443
+ identity: {
41444
+ display_name: report.identity.displayName,
41445
+ principal_id: report.identity.principalId
41446
+ },
41447
+ listener: {
41448
+ found: report.listener.status !== null,
41449
+ checked_directory: report.listener.checkedDirectory,
41450
+ source: report.listener.source,
41451
+ state: report.listener.status?.state ?? null,
41452
+ pid: report.listener.status?.pid ?? null,
41453
+ running_cswarm_version: report.listener.source === "live_process" ? report.listener.status?.cswarmVersion ?? null : null,
41454
+ installed_cswarm_version: report.installedVersion,
41455
+ version_mismatch: report.listener.source === "live_process" && report.listener.status?.cswarmVersion !== null && report.listener.status?.cswarmVersion !== void 0 && report.listener.status.cswarmVersion !== report.installedVersion
41456
+ },
41457
+ notify_watchers: report.watchers.map((watcher) => ({
41458
+ pid: watcher.pid,
41459
+ matched_by: watcher.matchedBy,
41460
+ stdout: watcher.stdout
41461
+ })),
41462
+ brain: {
41463
+ high_water_file: report.brain.highWaterFile,
41464
+ high_water_advanced: false,
41465
+ digest: report.brain.digest
41466
+ },
41467
+ unread_inbox: report.inbox,
41468
+ read_only: true
41469
+ };
41470
+ }
41471
+
39646
41472
  // src/cli.ts
39647
41473
  var import_meta = {};
39648
41474
  var KNOWN_FLAGS = /* @__PURE__ */ new Set([
@@ -39650,6 +41476,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
39650
41476
  "agent-token-file",
39651
41477
  "agent-token-stdin",
39652
41478
  "all-devices",
41479
+ "allow-unattended",
39653
41480
  "anon-key",
39654
41481
  "attach",
39655
41482
  "branch",
@@ -39699,6 +41526,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
39699
41526
  "since",
39700
41527
  "site",
39701
41528
  "slug",
41529
+ "state-dir",
39702
41530
  "renewal-horizon-days",
39703
41531
  "standing",
39704
41532
  "task-id",
@@ -39718,6 +41546,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
39718
41546
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
39719
41547
  "agent-token-stdin",
39720
41548
  "all-devices",
41549
+ "allow-unattended",
39721
41550
  "confirm-standing",
39722
41551
  "force-file-store",
39723
41552
  "follow",
@@ -39747,8 +41576,8 @@ var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
39747
41576
  AGENT_CREDENTIAL_MESSAGE_D088
39748
41577
  ];
39749
41578
  function packageVersion() {
39750
- if ("0.1.44".length > 0) {
39751
- return "0.1.44";
41579
+ if ("0.1.46".length > 0) {
41580
+ return "0.1.46";
39752
41581
  }
39753
41582
  try {
39754
41583
  const value = JSON.parse(
@@ -39865,6 +41694,7 @@ Usage:
39865
41694
  cswarm target clear [--json]
39866
41695
  cswarm status [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
39867
41696
  cswarm whoami ${requiredAgentCredential} [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
41697
+ cswarm resume --agent-token-file <path> [--url <url> --anon-key <key>] --workspace-id <uuid> [--json]
39868
41698
  cswarm members [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
39869
41699
  cswarm working-on "<what>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--about <ref>] [--until <dur>] [--json]
39870
41700
  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 +41714,8 @@ Usage:
39884
41714
  cswarm brain get <topic> [--version <n>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
39885
41715
  cswarm brain put <topic> [<markdown-path>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json] # without a path, reads Markdown from stdin
39886
41716
  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]
41717
+ 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]
41718
+ cswarm listen canary ${requiredAgentCredential} [--url <url> --anon-key <key>] --workspace-id <uuid> [--state-dir <path>] [--wait <seconds>] [--json]
39888
41719
  cswarm listen status ${agentCredential2} [--url <url> --anon-key <key>] --workspace-id <uuid> [--principal-id <uuid>] [--json]
39889
41720
  cswarm listen stop ${agentCredential2} [--url <url> --anon-key <key>] --workspace-id <uuid> [--principal-id <uuid>] [--json]
39890
41721
  cswarm hook check [--principal-id <uuid> ...] [--cooldown <seconds>]
@@ -39923,6 +41754,7 @@ Credential selection for command/dogfood:
39923
41754
  One that persists or references the credential needs the complete
39924
41755
  JSON artifact, because it needs a field a bare secret does not carry:
39925
41756
  whoami reads server-proven identity -- complete or bare form
41757
+ resume reads reconnect state -- complete or bare file form
39926
41758
  members reads only -- either form
39927
41759
  working-on, note, ask, reply, feed, inbox
39928
41760
  signal command/read only -- either form
@@ -39935,6 +41767,8 @@ Credential selection for command/dogfood:
39935
41767
  command, dogfood
39936
41768
  task protocol commands -- either form
39937
41769
  listen start persists durable state, rotates -- needs expires_at
41770
+ listen canary posts one self-note and selects local state -- needs
41771
+ principal_id; it does not renew the credential
39938
41772
  listen status
39939
41773
  selects the listener profile -- complete JSON or
39940
41774
  an explicit --principal-id without a credential
@@ -39972,15 +41806,20 @@ fresh credential.
39972
41806
  listen start --route worker|main|split chooses where directed messages go. worker
39973
41807
  is the unchanged default. main queues every ask or note for the interactive session.
39974
41808
  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
41809
+ 1..10000 and an equal-length message stays on the worker path. main and split require
41810
+ a principal-scoped Claude hook or prior hook surface. --allow-unattended accepts the
41811
+ risk explicitly. Run cswarm hook check
39976
41812
  --principal-id <uuid> to surface that agent's queued messages. A bare check works only
39977
41813
  when the state directory holds one principal. hook check has its own 3s ceiling, exits 0
39978
41814
  on every outcome, and skips network checks made within --cooldown seconds (default 30).
41815
+ listen canary posts one self-addressed note, waits at most --wait seconds (default 10),
41816
+ and reports accepted, claimed, queued/worker, surfaced, and observed as separate hops.
39979
41817
  hook install claude prints principal-scoped UserPromptSubmit JSON by default. --write changes
39980
41818
  <project>/.claude/settings.local.json, which applies only to Claude Code sessions started in
39981
41819
  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
41820
+ \${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json and warns that every Claude Code session reading
41821
+ that directory is affected. --repo keeps the repository-wide .claude/settings.json scope and
41822
+ also requires an ignored file. Uninstall also
39984
41823
  requires --write and uses the same scope selection.
39985
41824
 
39986
41825
  Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, new, and workspace close require a
@@ -40237,7 +42076,7 @@ async function stdinInviteLink() {
40237
42076
  return link;
40238
42077
  }
40239
42078
  async function confirmationLine(prompt) {
40240
- const reader = (0, import_promises12.createInterface)({
42079
+ const reader = (0, import_promises13.createInterface)({
40241
42080
  input: process.stdin,
40242
42081
  output: process.stderr,
40243
42082
  terminal: Boolean(process.stdin.isTTY)
@@ -40362,7 +42201,7 @@ async function runNew(args) {
40362
42201
  assertWorkspaceName(name);
40363
42202
  const cloud = await target(args);
40364
42203
  const human = await humanCredential(args, cloud);
40365
- const proposedId = (0, import_node_crypto20.randomUUID)();
42204
+ const proposedId = (0, import_node_crypto21.randomUUID)();
40366
42205
  let result;
40367
42206
  try {
40368
42207
  result = await new ThinCommandClient(cloud).sendConnect({
@@ -41385,7 +43224,7 @@ function accepted(label, result) {
41385
43224
  );
41386
43225
  }
41387
43226
  }
41388
- async function agentSession(cloud, workspaceId2, agent) {
43227
+ async function agentSession(cloud, workspaceId2, agent, fetcher) {
41389
43228
  let store2 = null;
41390
43229
  try {
41391
43230
  const candidate = await agentCredentialStore({
@@ -41411,7 +43250,8 @@ async function agentSession(cloud, workspaceId2, agent) {
41411
43250
  runId: agent.runId,
41412
43251
  expiresAt: agent.expiresAt
41413
43252
  },
41414
- store: store2
43253
+ store: store2,
43254
+ ...fetcher ? { fetcher } : {}
41415
43255
  });
41416
43256
  }
41417
43257
  async function commandWorkspaceAndCredential(args, cloud, options = {}) {
@@ -41693,8 +43533,8 @@ function prepareSignalAttachments(localPaths) {
41693
43533
  name,
41694
43534
  bytes,
41695
43535
  contentType,
41696
- fileId: (0, import_node_crypto20.randomUUID)(),
41697
- versionId: (0, import_node_crypto20.randomUUID)(),
43536
+ fileId: (0, import_node_crypto21.randomUUID)(),
43537
+ versionId: (0, import_node_crypto21.randomUUID)(),
41698
43538
  createCommandId: newCommandId(),
41699
43539
  commitCommandId: newCommandId()
41700
43540
  };
@@ -42168,6 +44008,100 @@ Owner: ${ownerName} (${identity.owner_user_id}).
42168
44008
  `)
42169
44009
  );
42170
44010
  }
44011
+ async function runResume(args) {
44012
+ args.assertShape([
44013
+ ...TARGET_FLAGS,
44014
+ "workspace-id",
44015
+ "agent-token-file",
44016
+ "state-dir",
44017
+ "json"
44018
+ ], 1);
44019
+ const suppliedCredentialPath = args.optional("agent-token-file");
44020
+ if (suppliedCredentialPath === void 0) {
44021
+ throw new UsageError("cswarm resume needs --agent-token-file <path>");
44022
+ }
44023
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(suppliedCredentialPath)) {
44024
+ throw new Error("--agent-token-file must not contain control characters");
44025
+ }
44026
+ const credentialFile = (0, import_node_path21.resolve)(suppliedCredentialPath);
44027
+ const cloud = await target(args);
44028
+ const workspaceId2 = listenerUuid(
44029
+ args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
44030
+ "workspace-id"
44031
+ );
44032
+ const agent = await agentCredential(args);
44033
+ const report = await inspectResume({
44034
+ target: cloud,
44035
+ workspaceId: workspaceId2,
44036
+ credentialFile,
44037
+ credentialPathAliases: [.../* @__PURE__ */ new Set([suppliedCredentialPath, credentialFile])],
44038
+ installedVersion: CLI_BUILD_VERSION,
44039
+ ...listenerStateDirectory(args) ? { stateDirectory: listenerStateDirectory(args) } : {}
44040
+ }, {
44041
+ readIdentity: async () => {
44042
+ const directory = await readAgentSignalDirectory(
44043
+ cloud,
44044
+ agent.token,
44045
+ workspaceId2
44046
+ );
44047
+ const identity = directory.identity;
44048
+ if (identity === void 0 || identity.workspace_id !== workspaceId2) {
44049
+ throw new Error(
44050
+ "the read service authenticated this credential but did not return its identity for this workspace; resume stopped without changing state"
44051
+ );
44052
+ }
44053
+ const principal = directory.agents.find(
44054
+ (candidate) => candidate.principal_id === identity.principal_id && candidate.owner_user_id === identity.owner_user_id
44055
+ );
44056
+ if (principal === void 0) {
44057
+ throw new Error(
44058
+ "the read service authenticated this credential but returned no matching live principal; resume stopped without changing state"
44059
+ );
44060
+ }
44061
+ if (agent.principalId !== null && agent.principalId !== identity.principal_id) {
44062
+ process.stderr.write(
44063
+ `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.
44064
+ `
44065
+ );
44066
+ }
44067
+ return {
44068
+ displayName: sanitizeDisplayLabel(principal.name, "Unnamed agent"),
44069
+ principalId: identity.principal_id
44070
+ };
44071
+ },
44072
+ readBrainTopics: async () => brainTopicSnapshots(await listBrainRowsAsAgent(
44073
+ cloud,
44074
+ agent.token,
44075
+ workspaceId2
44076
+ )),
44077
+ readInboxCount: async (principalId, instanceDirectory) => {
44078
+ const page = await readAgentSignalPage(
44079
+ cloud,
44080
+ { kind: "agent", token: agent.token },
44081
+ {
44082
+ workspaceId: workspaceId2,
44083
+ inbox: true,
44084
+ ascending: false,
44085
+ limit: 100,
44086
+ includeStale: false
44087
+ },
44088
+ fetch,
44089
+ { tolerateMalformedRows: true, maxMalformedRows: 3 }
44090
+ );
44091
+ const candidates = page.signals.filter(
44092
+ (signal) => (signal.kind === "ask" || signal.kind === "note") && signal.workspace_id === workspaceId2 && signal.to_agent === principalId
44093
+ ).map((signal) => ({ signalId: signal.id }));
44094
+ const unseen = await new FileHookSurfaceStore(instanceDirectory).previewUnseen(candidates);
44095
+ return {
44096
+ count: unseen.length,
44097
+ exact: page.rawCount < 100 && page.malformedRows === 0
44098
+ };
44099
+ }
44100
+ });
44101
+ if (args.has("json")) printJson(resumeJson(report));
44102
+ else process.stdout.write(`${renderResume(report)}
44103
+ `);
44104
+ }
42171
44105
  async function runSignalRead(args, inbox) {
42172
44106
  const notify = inbox && args.has("notify");
42173
44107
  args.assertShape(notify ? [
@@ -42268,15 +44202,6 @@ async function runSignalRead(args, inbox) {
42268
44202
  })}
42269
44203
  `);
42270
44204
  }
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
44205
  async function runInboxNotifyCommand(args) {
42281
44206
  if (!hasAgentCredential(args)) {
42282
44207
  throw new Error(
@@ -42294,6 +44219,7 @@ async function runInboxNotifyCommand(args) {
42294
44219
  );
42295
44220
  }
42296
44221
  const controller = new AbortController();
44222
+ const httpClient = new ListenerHttpClient();
42297
44223
  const stop = () => controller.abort();
42298
44224
  process.on("SIGINT", stop);
42299
44225
  process.on("SIGTERM", stop);
@@ -42324,7 +44250,7 @@ async function runInboxNotifyCommand(args) {
42324
44250
  ...after === null ? {} : { after }
42325
44251
  }
42326
44252
  },
42327
- { signal: controller.signal }
44253
+ { signal: controller.signal, fetcher: httpClient.fetch }
42328
44254
  );
42329
44255
  },
42330
44256
  emit: async (signal) => {
@@ -42333,7 +44259,7 @@ async function runInboxNotifyCommand(args) {
42333
44259
  selected.selectedWorkspace,
42334
44260
  cloud
42335
44261
  );
42336
- await writeMonitorLine(
44262
+ await writeArrivalMonitorLine(
42337
44263
  args.has("json") ? JSON.stringify(notification) : formatArrivalNotification(notification)
42338
44264
  );
42339
44265
  },
@@ -42358,6 +44284,7 @@ async function runInboxNotifyCommand(args) {
42358
44284
  } finally {
42359
44285
  process.off("SIGINT", stop);
42360
44286
  process.off("SIGTERM", stop);
44287
+ httpClient.close();
42361
44288
  }
42362
44289
  }
42363
44290
  async function runReceipt(args) {
@@ -42430,6 +44357,7 @@ async function runInboxFollowCommand(args) {
42430
44357
  includeStale: args.has("include-stale")
42431
44358
  };
42432
44359
  const controller = new AbortController();
44360
+ const httpClient = new ListenerHttpClient();
42433
44361
  let legacyCursorWarned = false;
42434
44362
  let malformedRowWarnings = 0;
42435
44363
  const onAbortSignal = () => controller.abort();
@@ -42461,7 +44389,7 @@ async function runInboxFollowCommand(args) {
42461
44389
  cloud,
42462
44390
  credential,
42463
44391
  query,
42464
- { signal: controller.signal },
44392
+ { signal: controller.signal, fetcher: httpClient.fetch },
42465
44393
  {
42466
44394
  allowLegacyCursorFallback: true,
42467
44395
  tolerateMalformedRows: true,
@@ -42493,7 +44421,7 @@ async function runInboxFollowCommand(args) {
42493
44421
  cloud,
42494
44422
  credential,
42495
44423
  query,
42496
- { signal: controller.signal }
44424
+ { signal: controller.signal, fetcher: httpClient.fetch }
42497
44425
  );
42498
44426
  },
42499
44427
  emit: (frame) => {
@@ -42511,6 +44439,7 @@ async function runInboxFollowCommand(args) {
42511
44439
  } finally {
42512
44440
  process.off("SIGINT", onAbortSignal);
42513
44441
  process.off("SIGTERM", onAbortSignal);
44442
+ httpClient.close();
42514
44443
  }
42515
44444
  }
42516
44445
  function listenerUuid(value, flag) {
@@ -42677,10 +44606,148 @@ function listenerHostLimits(provider) {
42677
44606
  }
42678
44607
  };
42679
44608
  }
42680
- function listenerStatusJson(status, permissionMode) {
44609
+ function listenerAttendanceState(status, evidence) {
44610
+ const routeMode = status.routeMode ?? "worker";
44611
+ const pending = status.pendingForMainCount ?? 0;
44612
+ const connected = status.state === "ready";
44613
+ const attendanceState = routeMode === "worker" ? "not_required" : pending > 0 ? "unattended" : evidence.hookSurfaceAdvanced ? "attended" : "unproven";
44614
+ const attended = attendanceState === "attended" ? true : attendanceState === "unattended" ? false : null;
44615
+ const handled = pending > 0 ? false : routeMode === "worker" && status.lastAckAt !== null ? true : null;
44616
+ return {
44617
+ connected,
44618
+ attended,
44619
+ attendanceState,
44620
+ handled,
44621
+ handledState: handled === true ? "handled" : handled === false ? "not_handled" : "not_yet_measured"
44622
+ };
44623
+ }
44624
+ function listenerAttendanceRemedy(principalId) {
44625
+ return `cswarm hook install claude --principal-id ${principalId} --write, then start a fresh session. Or restart the listener with --route worker.`;
44626
+ }
44627
+ function listenerReadHealthSummary(status, nowMs) {
44628
+ return summarizeListenerReadHealth(
44629
+ status.readHealth ?? emptyListenerReadHealth(),
44630
+ status.readyAt,
44631
+ nowMs
44632
+ );
44633
+ }
44634
+ function listenerLapseNotices(status, summary) {
44635
+ const health = status.readHealth ?? emptyListenerReadHealth();
44636
+ const notices = [];
44637
+ if (health.currentReasonCode === "host_ports_exhausted") {
44638
+ notices.push({
44639
+ code: "listener_host_ports_exhausted",
44640
+ message: "This host has run out of outbound ports. The listener is probing only once per minute so it does not amplify the outage.",
44641
+ nextStep: "Find the consumer: lsof -nP -iTCP | awk '{print $1}' | sort | uniq -c | sort -rn"
44642
+ });
44643
+ } else if (
44644
+ // Reuse arrival-watch.ts's 60s loud-lapse transition. The listener keeps
44645
+ // the episode in durable status instead of the monitor's process-local machine.
44646
+ summary.currentEpisodeDurationMs !== null && summary.currentEpisodeDurationMs >= ARRIVAL_RETRY_NOTICE_THRESHOLD_MS
44647
+ ) {
44648
+ notices.push({
44649
+ code: "listener_read_retry_persisting",
44650
+ message: `Listener reads have failed continuously for ${Math.floor(summary.currentEpisodeDurationMs / 1e3)}s. This is still in progress.`,
44651
+ nextStep: "Check cswarm status and the CommonSwarm service. If both are healthy, restart the listener."
44652
+ });
44653
+ }
44654
+ if (summary.throughputLapseHours.length > 0) {
44655
+ const latest = summary.throughputLapseHours.at(-1);
44656
+ notices.push({
44657
+ code: "listener_claim_throughput_lapse",
44658
+ message: `Claim throughput fell below 0.50 for the full hour at ${latest.hourStart}: ${latest.claims}/${Math.round(latest.expectedClaims)} expected (${latest.ratio.toFixed(3)}).`,
44659
+ nextStep: "This host is starving the listener \u2014 check load/memory pressure (sysctl kern.memorystatus_vm_pressure_level), or move the listener."
44660
+ });
44661
+ }
44662
+ return notices;
44663
+ }
44664
+ async function listenerProviderInstallEvidence(status) {
44665
+ if (status.provider !== "claude") return null;
44666
+ try {
44667
+ const notice = await inspectClaudeBridgeExecutable(
44668
+ status.providerExecutable ?? "claude-agent-acp",
44669
+ { pathEnv: process.env.PATH, env: process.env }
44670
+ );
44671
+ return {
44672
+ executable: notice.executable,
44673
+ providerVersion: notice.providerVersion,
44674
+ bundledAgentSdkVersion: notice.bundledAgentSdkVersion,
44675
+ bundledClaudeCodeVersion: notice.bundledClaudeCodeVersion
44676
+ };
44677
+ } catch {
44678
+ return {
44679
+ executable: null,
44680
+ providerVersion: null,
44681
+ bundledAgentSdkVersion: null,
44682
+ bundledClaudeCodeVersion: null
44683
+ };
44684
+ }
44685
+ }
44686
+ function versionIsBelow(left, right) {
44687
+ if (!left || !right) return false;
44688
+ try {
44689
+ return compareSemVer(left, right) < 0;
44690
+ } catch {
44691
+ return false;
44692
+ }
44693
+ }
44694
+ function providerRestartRequired(status, installed) {
44695
+ if (!installed) return false;
44696
+ return status.providerVersion !== null && status.providerVersion !== void 0 && installed.providerVersion !== null && status.providerVersion !== installed.providerVersion || status.providerBundledClaudeCodeVersion !== null && status.providerBundledClaudeCodeVersion !== void 0 && installed.bundledClaudeCodeVersion !== null && status.providerBundledClaudeCodeVersion !== installed.bundledClaudeCodeVersion;
44697
+ }
44698
+ function listenerStatusJson(status, permissionMode, evidence = {
44699
+ pendingForMainOldestAt: null,
44700
+ hookSurfaceExists: false,
44701
+ hookSurfaceAdvanced: false
44702
+ }, nowMs = Date.now(), installed = null) {
42681
44703
  const mode3 = permissionMode ?? status.permissionMode;
44704
+ const attendance = listenerAttendanceState(status, evidence);
44705
+ const pending = status.pendingForMainCount ?? 0;
44706
+ const readHealth = status.readHealth ?? emptyListenerReadHealth();
44707
+ const readSummary = listenerReadHealthSummary(status, nowMs);
44708
+ const lapseNotices = listenerLapseNotices(status, readSummary);
42682
44709
  return {
42683
44710
  ...status,
44711
+ providerExecutable: status.providerExecutable ?? null,
44712
+ providerExecutableMeasured: typeof status.providerExecutable === "string",
44713
+ providerVersion: status.providerVersion ?? null,
44714
+ providerVersionMeasured: typeof status.providerVersion === "string",
44715
+ providerBundledAgentSdkVersion: status.providerBundledAgentSdkVersion ?? null,
44716
+ providerBundledClaudeCodeVersion: status.providerBundledClaudeCodeVersion ?? null,
44717
+ providerMinimumRequiredVersion: status.providerMinimumRequiredVersion ?? null,
44718
+ lastErrorReasonCode: status.lastErrorReasonCode ?? null,
44719
+ providerBelowDemandedMinimum: versionIsBelow(
44720
+ status.providerBundledClaudeCodeVersion,
44721
+ status.providerMinimumRequiredVersion
44722
+ ),
44723
+ providerOnDiskExecutable: installed?.executable ?? null,
44724
+ providerOnDiskVersion: installed?.providerVersion ?? null,
44725
+ providerOnDiskBundledAgentSdkVersion: installed?.bundledAgentSdkVersion ?? null,
44726
+ providerOnDiskBundledClaudeCodeVersion: installed?.bundledClaudeCodeVersion ?? null,
44727
+ providerRestartRequired: providerRestartRequired(status, installed),
44728
+ ...attendance,
44729
+ hookSurfaceExists: evidence.hookSurfaceExists,
44730
+ hookSurfaceAdvanced: evidence.hookSurfaceAdvanced,
44731
+ pendingForMainOldestAt: evidence.pendingForMainOldestAt,
44732
+ pendingForMainOldestAgeMs: evidence.pendingForMainOldestAt === null ? null : Math.max(0, nowMs - Date.parse(evidence.pendingForMainOldestAt)),
44733
+ attendanceWarningCode: pending > 0 ? "listener_unattended_main_queue" : null,
44734
+ attendanceNextStep: pending > 0 ? listenerAttendanceRemedy(status.principalId) : null,
44735
+ readRetryCurrentEpisodeStartedAt: readHealth.currentEpisodeStartedAt,
44736
+ readRetryCurrentEpisodeAttempts: readHealth.currentEpisodeAttempts,
44737
+ readRetryCurrentReasonCode: readHealth.currentReasonCode,
44738
+ readRetryCurrentHttpStatus: readHealth.currentHttpStatus,
44739
+ readRetryCurrentErrorConstructor: readHealth.currentErrorConstructor,
44740
+ readRetryCurrentEpisodeDurationMs: readSummary.currentEpisodeDurationMs,
44741
+ readRetryEpisodesLast24h: readSummary.episodesLast24h,
44742
+ readRetryLongestEpisodeAttemptsLast24h: readSummary.longestEpisodeAttemptsLast24h,
44743
+ readRetryLongestEpisodeDurationMsLast24h: readSummary.longestEpisodeDurationMsLast24h,
44744
+ readRetriesLastHour: readSummary.retriesLastHour,
44745
+ readRetryHours: readSummary.retryHours,
44746
+ claimCadenceMs: readHealth.claimCadenceMs,
44747
+ claimThroughputHours: readSummary.claimThroughputHours,
44748
+ listenerLapse: lapseNotices.length > 0,
44749
+ listenerLapseCodes: lapseNotices.map((notice) => notice.code),
44750
+ listenerLapseNextSteps: lapseNotices.map((notice) => notice.nextStep),
42684
44751
  deliveryMode: status.deliveryMode ?? null,
42685
44752
  pendingDeliveryCount: status.pendingDeliveryCount ?? null,
42686
44753
  lastTerminalDeliveryFailureCount: status.lastTerminalDeliveryFailureCount ?? null,
@@ -42691,6 +44758,8 @@ function listenerStatusJson(status, permissionMode) {
42691
44758
  deferOverChars: status.deferOverChars ?? null,
42692
44759
  pendingForMainCount: status.pendingForMainCount ?? 0,
42693
44760
  droppedForMainCount: status.droppedForMainCount ?? 0,
44761
+ connectionsOpened: status.connectionsOpened ?? null,
44762
+ connectionReuseRatio: status.connectionReuseRatio ?? null,
42694
44763
  ...mode3 ? {
42695
44764
  permission_mode: mode3,
42696
44765
  /* "allowed once" alone overstates it: allowOnceOrDeny selects allow_once only when the
@@ -42702,22 +44771,60 @@ function listenerStatusJson(status, permissionMode) {
42702
44771
  host_limits: listenerHostLimits(status.provider)
42703
44772
  };
42704
44773
  }
42705
- function renderListenerStatus(status) {
44774
+ function renderListenerStatus(status, evidence = {
44775
+ pendingForMainOldestAt: null,
44776
+ hookSurfaceExists: false,
44777
+ hookSurfaceAdvanced: false
44778
+ }, nowMs = Date.now(), installed = null) {
42706
44779
  const routeMode = status.routeMode ?? "worker";
42707
44780
  const pendingForMainCount = status.pendingForMainCount ?? 0;
42708
44781
  const droppedForMainCount = status.droppedForMainCount ?? 0;
44782
+ const unattendedCount = `${pendingForMainCount} ${pendingForMainCount === 1 ? "message is" : "messages are"} unattended`;
44783
+ const attendance = listenerAttendanceState(status, evidence);
44784
+ const readHealth = status.readHealth ?? emptyListenerReadHealth();
44785
+ const readSummary = listenerReadHealthSummary(status, nowMs);
44786
+ const lapseNotices = listenerLapseNotices(status, readSummary);
42709
44787
  const lines = [
42710
- `Listener ${status.state} for agent ${status.principalId}.`,
44788
+ lapseNotices.length > 0 ? `Listener LAPSE for agent ${status.principalId}: ${lapseNotices.map((notice) => notice.code).join(", ")}.` : pendingForMainCount > 0 ? `Listener WARNING for agent ${status.principalId}: ${unattendedCount}.` : `Listener ${status.state} for agent ${status.principalId}.`,
44789
+ `CONNECTED: ${attendance.connected ? "yes" : "no"}. Transport state is ${status.state}.`,
44790
+ `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"}.`,
44791
+ `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
44792
  `Provider: ${status.provider}; process: ${status.pid}; started: ${status.startedAt}.`,
44793
+ `Provider executable: ${status.providerExecutable ?? "not measured"}.`,
44794
+ `Connections opened: ${status.connectionsOpened ?? "not measured"}.`,
44795
+ `Connection reuse ratio: ${status.connectionReuseRatio ?? "not measured"}.`,
42712
44796
  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."
44797
+ 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.",
44798
+ status.lastErrorCode ? `Last status code: ${status.lastErrorCode}.` : "No listener process error is recorded.",
44799
+ readHealth.currentEpisodeStartedAt === null ? "Current read retry episode: none." : `Current read retry episode: ${readHealth.currentEpisodeAttempts} attempt${readHealth.currentEpisodeAttempts === 1 ? "" : "s"} since ${readHealth.currentEpisodeStartedAt}; reason ${readHealth.currentReasonCode}${readHealth.currentHttpStatus === null ? "" : ` (HTTP ${readHealth.currentHttpStatus})`}${readHealth.currentErrorConstructor === null ? "" : ` (${readHealth.currentErrorConstructor})`}.`,
44800
+ `Read retry episodes in the last 24h: ${readSummary.episodesLast24h}; retries in the rolling hour: ${readSummary.retriesLastHour}.`,
44801
+ readSummary.longestEpisodeAttemptsLast24h === 0 ? "Longest read retry episode in the last 24h: none recorded." : `Longest read retry episode in the last 24h: ${readSummary.longestEpisodeAttemptsLast24h} attempts over ${Math.floor(readSummary.longestEpisodeDurationMsLast24h / 1e3)}s.`,
44802
+ readSummary.retryHours.length === 0 ? "Read retries by hour in the last 24h: none." : `Read retries by hour in the last 24h: ${readSummary.retryHours.map((hour) => `${hour.hourStart}=${hour.retries}`).join("; ")}.`,
44803
+ readSummary.claimThroughputHours.length === 0 ? "Claim throughput by full hour: no complete listener hour is available yet." : `Claim throughput by full hour: ${readSummary.claimThroughputHours.map((hour) => `${hour.hourStart} ${hour.claims}/${Math.round(hour.expectedClaims)} (${hour.ratio.toFixed(3)})`).join("; ")}.`
42715
44804
  ];
44805
+ for (const notice of lapseNotices) {
44806
+ lines.push(`WARNING [${notice.code}]: ${notice.message}`);
44807
+ lines.push(`Next: ${notice.nextStep}`);
44808
+ }
42716
44809
  if (status.lastErrorDetail) {
42717
44810
  const [first, ...rest] = status.lastErrorDetail.split("\n");
42718
44811
  lines.push(`Last error detail (local only): ${first}`);
42719
44812
  for (const line of rest) lines.push(` ${line}`);
42720
44813
  }
44814
+ if (status.lastErrorReasonCode) {
44815
+ lines.push(`Last provider reason code: ${status.lastErrorReasonCode}.`);
44816
+ }
44817
+ if (status.provider === "claude" && status.lastErrorCode === "permission_canary_failed") {
44818
+ lines.push(
44819
+ `Canary diagnosis: ${listenerFailureMessage(
44820
+ status.lastErrorCode,
44821
+ status.provider,
44822
+ status.lastErrorDetail,
44823
+ status.lastErrorReasonCode,
44824
+ status.providerMinimumRequiredVersion
44825
+ )}.`
44826
+ );
44827
+ }
42721
44828
  if (status.lastWorkerStderrTail) {
42722
44829
  const tailLines = status.lastWorkerStderrTail.split("\n").filter((line) => line.trim().length > 0);
42723
44830
  lines.push("Worker stderr (local log only):");
@@ -42727,8 +44834,34 @@ function renderListenerStatus(status) {
42727
44834
  }
42728
44835
  if (status.providerVersion && status.providerLastMeasuredVersion) {
42729
44836
  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.`
44837
+ status.providerVersion === status.providerLastMeasuredVersion ? `Provider version: ${status.providerVersion} (last measured: ${status.providerLastMeasuredVersion}).` : status.state === "ready" ? `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.` : `Provider version ${status.providerVersion} is newer than the last measured version ${status.providerLastMeasuredVersion}. It was measured before startup failed; compatibility was not established. Next: resolve the startup failure before verifying this provider release.`
44838
+ );
44839
+ } else {
44840
+ lines.push("Provider version: not measured.");
44841
+ }
44842
+ if (status.provider === "claude") {
44843
+ lines.push(
44844
+ `Bundled Claude Code version: ${status.providerBundledClaudeCodeVersion ?? "not measured"}.`,
44845
+ `Bundled Claude agent SDK version: ${status.providerBundledAgentSdkVersion ?? "not measured"}.`
42731
44846
  );
44847
+ if (status.providerMinimumRequiredVersion) {
44848
+ lines.push(
44849
+ `Last API minimum demanded: Claude Code ${status.providerMinimumRequiredVersion}.`
44850
+ );
44851
+ }
44852
+ if (versionIsBelow(
44853
+ status.providerBundledClaudeCodeVersion,
44854
+ status.providerMinimumRequiredVersion
44855
+ )) {
44856
+ lines.push(
44857
+ `WARNING [claude_bridge_below_api_minimum]: this listener has bundled Claude Code ${status.providerBundledClaudeCodeVersion}, below the API minimum ${status.providerMinimumRequiredVersion}. Install the current bridge (npm i -g @agentclientprotocol/claude-agent-acp@latest), restart the listener, then run cswarm listen status and confirm that the bundled Claude Code version meets the API minimum ${status.providerMinimumRequiredVersion}.`
44858
+ );
44859
+ }
44860
+ if (providerRestartRequired(status, installed)) {
44861
+ lines.push(
44862
+ `A different Claude bridge is on disk: ${installed?.providerVersion ?? "version not measured"}${installed?.bundledClaudeCodeVersion ? ` (bundled Claude Code ${installed.bundledClaudeCodeVersion})` : ""}. Restart to pick up ${installed?.providerVersion ?? "the on-disk bridge"}.`
44863
+ );
44864
+ }
42732
44865
  }
42733
44866
  if (status.deliveryMode === "durable_claim") {
42734
44867
  lines.push("Delivery mode: durable claim and acknowledgement.");
@@ -42755,8 +44888,14 @@ function renderListenerStatus(status) {
42755
44888
  }
42756
44889
  if (pendingForMainCount > 0) {
42757
44890
  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.`
44891
+ `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
44892
  );
44893
+ lines.push(`Next: ${listenerAttendanceRemedy(status.principalId)}`);
44894
+ if (status.state === "stopped" || status.state === "failed") {
44895
+ lines.push(
44896
+ `${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)}`
44897
+ );
44898
+ }
42760
44899
  }
42761
44900
  }
42762
44901
  if (status.lastTerminalDeliveryFailureCount !== null && status.lastTerminalDeliveryFailureCount > 0) {
@@ -42776,16 +44915,49 @@ async function unsurfacedPendingMainStats(instanceDirectory, fallback) {
42776
44915
  const queue = new FilePendingMainQueue(instanceDirectory);
42777
44916
  const pending = await queue.read();
42778
44917
  const stats = await queue.stats();
42779
- const staged = await new FileHookSurfaceStore(instanceDirectory).stage(
44918
+ const surface = new FileHookSurfaceStore(instanceDirectory);
44919
+ const staged = await surface.stage(
42780
44920
  pending,
42781
44921
  stats.droppedCount
42782
44922
  );
42783
- return { count: staged.unseen.length, droppedCount: stats.droppedCount };
44923
+ const hook = await surface.evidence();
44924
+ const oldestAt = staged.unseen.reduce((oldest, item) => oldest === null || Date.parse(item.queuedAt) < Date.parse(oldest) ? item.queuedAt : oldest, null);
44925
+ return {
44926
+ count: staged.unseen.length,
44927
+ droppedCount: stats.droppedCount,
44928
+ oldestAt,
44929
+ hookSurfaceExists: hook.exists,
44930
+ hookSurfaceAdvanced: hook.surfacedSignalIds.length > 0
44931
+ };
42784
44932
  } catch {
42785
- return fallback;
44933
+ return {
44934
+ ...fallback,
44935
+ oldestAt: null,
44936
+ hookSurfaceExists: false,
44937
+ hookSurfaceAdvanced: false
44938
+ };
44939
+ }
44940
+ }
44941
+ function quotedListenerFailureDetail(detail) {
44942
+ const recorded = detail?.trim();
44943
+ if (!recorded) return "not recorded";
44944
+ const bounded = recorded.length > 600 ? `${recorded.slice(0, 599)}\u2026` : recorded;
44945
+ return JSON.stringify(bounded);
44946
+ }
44947
+ function listenerProviderIdentitySummary(status) {
44948
+ const parts = [
44949
+ `Provider executable: ${status.providerExecutable ?? "not measured"}`,
44950
+ `provider version: ${status.providerVersion ?? "not measured"}`
44951
+ ];
44952
+ if (status.provider === "claude") {
44953
+ parts.push(
44954
+ `bundled Claude Code: ${status.providerBundledClaudeCodeVersion ?? "not measured"}`,
44955
+ `bundled Claude agent SDK: ${status.providerBundledAgentSdkVersion ?? "not measured"}`
44956
+ );
42786
44957
  }
44958
+ return parts.join("; ");
42787
44959
  }
42788
- function listenerFailureMessage(code, provider) {
44960
+ function listenerFailureMessage(code, provider, detail, reasonCode, minimumRequiredVersion) {
42789
44961
  if (code === "version_below_floor") {
42790
44962
  if (provider === "codex") {
42791
44963
  return "the Codex listener requires codex-acp 1.1.9 or newer; update the bridge, then retry";
@@ -42804,6 +44976,9 @@ function listenerFailureMessage(code, provider) {
42804
44976
  if (code === "version_refused") {
42805
44977
  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
44978
  }
44979
+ if (code === "executable_not_bridge" && provider === "codex") {
44980
+ return "this is the Codex CLI; --codex-executable takes the codex-acp bridge (npm i -g @agentclientprotocol/codex-acp)";
44981
+ }
42807
44982
  if (code === "executable_missing" && provider === "claude") {
42808
44983
  return "claude-agent-acp is not installed; run npm install -g @agentclientprotocol/claude-agent-acp@latest (minimum 0.64.2), then retry";
42809
44984
  }
@@ -42842,10 +45017,25 @@ function listenerFailureMessage(code, provider) {
42842
45017
  }
42843
45018
  if (code === "permission_canary_failed") {
42844
45019
  if (provider === "claude") {
42845
- 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";
45020
+ const shape = classifyClaudeCanaryFailure(detail, reasonCode);
45021
+ const ran = "the Claude ACP permission canary ran, but no workspace signal prompt was delivered";
45022
+ const response = `bridge response [${shape.code}]: ${quotedListenerFailureDetail(detail)}`;
45023
+ if (shape.code === "claude_bridge_version_required") {
45024
+ const minimum = minimumRequiredVersion ?? shape.minimumRequiredVersion;
45025
+ return `${ran}. ${response}. Next: install the current bridge (npm i -g @agentclientprotocol/claude-agent-acp@latest), restart the listener, then run cswarm listen status and confirm that the bundled Claude Code version meets the API minimum ${minimum ?? "reported there"}`;
45026
+ }
45027
+ if (shape.code === "claude_canary_timeout") {
45028
+ return `${ran}. ${response}. Next: run claude -p and check for session-limit text, check host load, then retry`;
45029
+ }
45030
+ if (shape.code === "claude_canary_auth_failed") {
45031
+ return `${ran}. ${response}. Next: confirm Claude Code keychain/OAuth sign-in, then retry`;
45032
+ }
45033
+ return `${ran}. ${response}. The cause was not determined. Next: inspect the quoted bridge response and local worker stderr, then retry only after the cause is known or the failure appears transient`;
42846
45034
  }
42847
45035
  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";
45036
+ const recorded = detail?.trim();
45037
+ const gate = "the Codex listener did not pass the read-only ACP permission safety gate; no workspace signal prompt was delivered";
45038
+ return recorded ? `${gate}. Recorded reason: ${JSON.stringify(recorded)}` : `${gate}. The recorded reason was unavailable. Next: run cswarm listen status, then retry`;
42849
45039
  }
42850
45040
  if (provider === "grok") {
42851
45041
  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";
@@ -42928,11 +45118,19 @@ async function runConfiguredListener(options) {
42928
45118
  principalId: options.principalId,
42929
45119
  ...options.stateDirectory ? { stateDirectory: options.stateDirectory } : {}
42930
45120
  });
42931
- const liveCredentialSession = await agentSession(
42932
- options.cloud,
42933
- options.workspaceId,
42934
- options.agent
42935
- );
45121
+ const httpClient = new ListenerHttpClient();
45122
+ let liveCredentialSession;
45123
+ try {
45124
+ liveCredentialSession = await agentSession(
45125
+ options.cloud,
45126
+ options.workspaceId,
45127
+ options.agent,
45128
+ httpClient.fetch
45129
+ );
45130
+ } catch (error) {
45131
+ httpClient.close();
45132
+ throw error;
45133
+ }
42936
45134
  let storedCredential = null;
42937
45135
  const credentialSession = {
42938
45136
  bearer: async () => {
@@ -42960,7 +45158,7 @@ async function runConfiguredListener(options) {
42960
45158
  options.cloud,
42961
45159
  credential,
42962
45160
  options.workspaceId,
42963
- context
45161
+ { ...context, fetcher: httpClient.fetch }
42964
45162
  );
42965
45163
  const provenance = listenerSenderProvenance(signal, senderDirectory);
42966
45164
  if (context.includeBrainDigest !== true) return provenance;
@@ -42971,7 +45169,8 @@ async function runConfiguredListener(options) {
42971
45169
  options.workspaceId,
42972
45170
  {
42973
45171
  ...context.signal ? { signal: context.signal } : {},
42974
- deadlineMs: context.deadlineMs
45172
+ deadlineMs: context.deadlineMs,
45173
+ fetcher: httpClient.fetch
42975
45174
  }
42976
45175
  );
42977
45176
  const brainDigest = await new FileBrainDigestStore(
@@ -43011,7 +45210,19 @@ async function runConfiguredListener(options) {
43011
45210
  let workerStderrGeneration = 0;
43012
45211
  let providerVersionNotice = null;
43013
45212
  const onVersionNotice = (notice) => {
43014
- providerVersionNotice = notice;
45213
+ providerVersionNotice = {
45214
+ runningVersion: notice.runningVersion,
45215
+ lastMeasuredVersion: notice.lastMeasuredVersion
45216
+ };
45217
+ };
45218
+ const onClaudeRuntimeNotice = (notice) => {
45219
+ providerVersionNotice = {
45220
+ runningVersion: notice.providerVersion,
45221
+ lastMeasuredVersion: notice.lastMeasuredVersion,
45222
+ executable: notice.executable,
45223
+ bundledAgentSdkVersion: notice.bundledAgentSdkVersion,
45224
+ bundledClaudeCodeVersion: notice.bundledClaudeCodeVersion
45225
+ };
43015
45226
  };
43016
45227
  const newWorkerStderrTailSink = () => {
43017
45228
  const generation = ++workerStderrGeneration;
@@ -43021,7 +45232,7 @@ async function runConfiguredListener(options) {
43021
45232
  lastWorkerStderrTail = tail.length > 0 ? tail : null;
43022
45233
  };
43023
45234
  };
43024
- const newModel = (onCanaryAttempt) => {
45235
+ const newModel = (onCanaryAttempt, events) => {
43025
45236
  providerVersionNotice = null;
43026
45237
  return options.provider === "opencode" ? new OpenCodeListenerModel({
43027
45238
  cwd: options.cwd,
@@ -43030,6 +45241,7 @@ async function runConfiguredListener(options) {
43030
45241
  onWorkerStderrTail: newWorkerStderrTailSink(),
43031
45242
  onCanaryAttempt,
43032
45243
  onVersionNotice,
45244
+ events,
43033
45245
  ...options.model ? { model: options.model } : {},
43034
45246
  ...options.opencodeExecutable ? { executable: options.opencodeExecutable } : options.executable ? { executable: options.executable } : {}
43035
45247
  }) : options.provider === "claude" ? new ClaudeListenerModel({
@@ -43038,7 +45250,9 @@ async function runConfiguredListener(options) {
43038
45250
  promptTimeoutMs: resolveTurnBudgetMs,
43039
45251
  onWorkerStderrTail: newWorkerStderrTailSink(),
43040
45252
  onCanaryAttempt,
45253
+ onRuntimeNotice: onClaudeRuntimeNotice,
43041
45254
  onVersionNotice,
45255
+ events,
43042
45256
  ...options.claudeExecutable ? { executable: options.claudeExecutable } : options.executable ? { executable: options.executable } : {}
43043
45257
  }) : options.provider === "codex" ? new CodexListenerModel({
43044
45258
  cwd: options.cwd,
@@ -43047,6 +45261,7 @@ async function runConfiguredListener(options) {
43047
45261
  onWorkerStderrTail: newWorkerStderrTailSink(),
43048
45262
  onCanaryAttempt,
43049
45263
  onVersionNotice,
45264
+ events,
43050
45265
  ...options.codexExecutable ? { executable: options.codexExecutable } : options.executable ? { executable: options.executable } : {}
43051
45266
  }) : new GrokListenerModel({
43052
45267
  cwd: options.cwd,
@@ -43055,6 +45270,7 @@ async function runConfiguredListener(options) {
43055
45270
  onWorkerStderrTail: newWorkerStderrTailSink(),
43056
45271
  onCanaryAttempt,
43057
45272
  onVersionNotice,
45273
+ events,
43058
45274
  ...options.model ? { model: options.model } : {},
43059
45275
  ...options.effort ? { effort: options.effort } : {},
43060
45276
  ...options.executable ? { executable: options.executable } : {}
@@ -43077,6 +45293,7 @@ async function runConfiguredListener(options) {
43077
45293
  workspaceId: options.workspaceId,
43078
45294
  principalId: options.principalId,
43079
45295
  provider: options.provider,
45296
+ cswarmVersion: CLI_BUILD_VERSION,
43080
45297
  permissionMode: options.permissionMode,
43081
45298
  routeMode,
43082
45299
  deferOverChars,
@@ -43084,6 +45301,7 @@ async function runConfiguredListener(options) {
43084
45301
  // one has run, else the configured cap.
43085
45302
  getTurnBudgetMs: () => lastAppliedTurnBudgetMs ?? turnBudgetMs,
43086
45303
  getProviderVersionNotice: () => providerVersionNotice,
45304
+ getConnectionMetrics: () => httpClient.metrics(),
43087
45305
  takeWorkerStderrTail: () => {
43088
45306
  const tail = lastWorkerStderrTail;
43089
45307
  lastWorkerStderrTail = null;
@@ -43117,28 +45335,48 @@ async function runConfiguredListener(options) {
43117
45335
  ts: (/* @__PURE__ */ new Date()).toISOString()
43118
45336
  });
43119
45337
  };
43120
- return await runListenerRuntime({
43121
- target: options.cloud,
45338
+ const activity = new ListenerActivityController({
43122
45339
  workspaceId: options.workspaceId,
43123
- principalId: options.principalId,
43124
- credentialSession,
43125
- store: effectStore,
43126
- model: newModel(onCanaryAttempt),
43127
- signal,
43128
- onEvent,
43129
- declareModel: listenerModelLabel(options.provider),
43130
- listenerInstanceId,
43131
- deliveryJournal: selectedJournal,
43132
- resolveSenderProvenance,
43133
- routeMode,
43134
- deferOverChars,
43135
- pendingMainQueue
45340
+ transport: new AgentActivityEndpointTransport(
45341
+ options.cloud,
45342
+ credentialSession,
45343
+ httpClient.fetch
45344
+ )
43136
45345
  });
45346
+ const instrumentedModel = activity.instrumentModel(
45347
+ newModel(onCanaryAttempt, activity.events)
45348
+ );
45349
+ try {
45350
+ return await runListenerRuntime({
45351
+ target: options.cloud,
45352
+ workspaceId: options.workspaceId,
45353
+ principalId: options.principalId,
45354
+ credentialSession,
45355
+ store: effectStore,
45356
+ model: instrumentedModel,
45357
+ signal,
45358
+ onEvent: (event) => {
45359
+ activity.onRuntimeEvent(event);
45360
+ onEvent(event);
45361
+ },
45362
+ declareModel: listenerModelLabel(options.provider),
45363
+ listenerInstanceId,
45364
+ deliveryJournal: selectedJournal,
45365
+ resolveSenderProvenance,
45366
+ routeMode,
45367
+ deferOverChars,
45368
+ pendingMainQueue,
45369
+ fetcher: httpClient.fetch
45370
+ });
45371
+ } finally {
45372
+ activity.close();
45373
+ }
43137
45374
  }
43138
45375
  });
43139
45376
  } finally {
43140
45377
  process.off("SIGINT", onProcessSignal);
43141
45378
  process.off("SIGTERM", onProcessSignal);
45379
+ httpClient.close();
43142
45380
  }
43143
45381
  }
43144
45382
  async function runListenStart(args) {
@@ -43159,6 +45397,7 @@ async function runListenStart(args) {
43159
45397
  "turn-budget",
43160
45398
  "route",
43161
45399
  "defer-over",
45400
+ "allow-unattended",
43162
45401
  "foreground",
43163
45402
  "json"
43164
45403
  ], 2);
@@ -43198,6 +45437,9 @@ async function runListenStart(args) {
43198
45437
  `a listener is already ${existing.state} for agent ${principalId}`
43199
45438
  );
43200
45439
  }
45440
+ if (routing.routeMode !== "worker" && !args.has("allow-unattended") && !await listenerHasAttendanceSurface(paths.instanceDirectory, cwd, principalId)) {
45441
+ throw new ListenerUnattendedRefusedError(principalId);
45442
+ }
43201
45443
  let status;
43202
45444
  if (args.has("foreground")) {
43203
45445
  status = await runConfiguredListener({
@@ -43282,16 +45524,39 @@ async function runListenStart(args) {
43282
45524
  });
43283
45525
  } catch (error) {
43284
45526
  if (error instanceof ListenerStartupError) {
43285
- throw new Error(listenerFailureMessage(error.code, provider));
45527
+ const failedStatus = await effectiveListenerStatus(paths).catch(() => null);
45528
+ const detail = failedStatus?.lastErrorCode === error.code ? failedStatus.lastErrorDetail : null;
45529
+ const reasonCode = failedStatus?.lastErrorCode === error.code ? failedStatus.lastErrorReasonCode : null;
45530
+ const message = listenerFailureMessage(
45531
+ error.code,
45532
+ provider,
45533
+ detail,
45534
+ reasonCode,
45535
+ failedStatus?.providerMinimumRequiredVersion
45536
+ );
45537
+ throw new Error(
45538
+ failedStatus === null ? message : `${message}. ${listenerProviderIdentitySummary(failedStatus)}`
45539
+ );
43286
45540
  }
43287
45541
  throw error;
43288
45542
  }
43289
45543
  }
43290
45544
  if (status.state === "failed") {
43291
45545
  throw new Error(
43292
- listenerFailureMessage(status.lastErrorCode ?? "unknown_error", provider)
45546
+ `${listenerFailureMessage(
45547
+ status.lastErrorCode ?? "unknown_error",
45548
+ provider,
45549
+ status.lastErrorDetail,
45550
+ status.lastErrorReasonCode,
45551
+ status.providerMinimumRequiredVersion
45552
+ )}. ${listenerProviderIdentitySummary(status)}`
43293
45553
  );
43294
45554
  }
45555
+ let attendanceEvidence = {
45556
+ pendingForMainOldestAt: null,
45557
+ hookSurfaceExists: false,
45558
+ hookSurfaceAdvanced: false
45559
+ };
43295
45560
  if ((status.routeMode ?? "worker") !== "worker") {
43296
45561
  const recordedPending = status.pendingForMainCount ?? 0;
43297
45562
  const recordedDropped = status.droppedForMainCount ?? 0;
@@ -43304,9 +45569,14 @@ async function runListenStart(args) {
43304
45569
  pendingForMainCount: queueStats.count,
43305
45570
  droppedForMainCount: queueStats.droppedCount
43306
45571
  };
45572
+ attendanceEvidence = {
45573
+ pendingForMainOldestAt: queueStats.oldestAt,
45574
+ hookSurfaceExists: queueStats.hookSurfaceExists,
45575
+ hookSurfaceAdvanced: queueStats.hookSurfaceAdvanced
45576
+ };
43307
45577
  }
43308
45578
  if (args.has("json")) {
43309
- printJson(listenerStatusJson(status, permissionMode));
45579
+ printJson(listenerStatusJson(status, permissionMode, attendanceEvidence));
43310
45580
  return;
43311
45581
  }
43312
45582
  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 +45588,8 @@ async function runListenStart(args) {
43318
45588
  ` : `The Grok worker uses your selected cwd and local Grok configuration, including user and cmux hooks. ${workerAudience}
43319
45589
  `;
43320
45590
  process.stdout.write(
43321
- `${args.has("foreground") ? "Listener stopped." : "Listener is ready and will keep receiving after this command exits."}
43322
- ${renderListenerStatus(status)}
45591
+ `${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."}
45592
+ ${renderListenerStatus(status, attendanceEvidence)}
43323
45593
  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
45594
  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
45595
  ` + 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 +45708,11 @@ async function runListenStatusOrStop(args, command2) {
43438
45708
  }
43439
45709
  return;
43440
45710
  }
45711
+ let attendanceEvidence = {
45712
+ pendingForMainOldestAt: null,
45713
+ hookSurfaceExists: false,
45714
+ hookSurfaceAdvanced: false
45715
+ };
43441
45716
  if ((status.routeMode ?? "worker") !== "worker") {
43442
45717
  const recordedPending = status.pendingForMainCount ?? 0;
43443
45718
  const recordedDropped = status.droppedForMainCount ?? 0;
@@ -43450,13 +45725,93 @@ async function runListenStatusOrStop(args, command2) {
43450
45725
  pendingForMainCount: queueStats.count,
43451
45726
  droppedForMainCount: queueStats.droppedCount
43452
45727
  };
45728
+ attendanceEvidence = {
45729
+ pendingForMainOldestAt: queueStats.oldestAt,
45730
+ hookSurfaceExists: queueStats.hookSurfaceExists,
45731
+ hookSurfaceAdvanced: queueStats.hookSurfaceAdvanced
45732
+ };
43453
45733
  }
45734
+ const installed = command2 === "status" ? await listenerProviderInstallEvidence(status) : null;
43454
45735
  if (args.has("json")) {
43455
- printJson(listenerStatusJson(status));
45736
+ printJson(
45737
+ listenerStatusJson(
45738
+ status,
45739
+ void 0,
45740
+ attendanceEvidence,
45741
+ Date.now(),
45742
+ installed
45743
+ )
45744
+ );
43456
45745
  } else {
43457
- process.stdout.write(`${renderListenerStatus(status)}
43458
- `);
45746
+ process.stdout.write(
45747
+ `${renderListenerStatus(status, attendanceEvidence, Date.now(), installed)}
45748
+ `
45749
+ );
45750
+ }
45751
+ }
45752
+ async function runListenCanary(args) {
45753
+ args.assertShape([
45754
+ ...TARGET_FLAGS,
45755
+ ...CREDENTIAL_FLAGS,
45756
+ "workspace-id",
45757
+ "state-dir",
45758
+ "wait",
45759
+ "json"
45760
+ ], 2);
45761
+ if (!hasAgentCredential(args)) {
45762
+ throw new Error(
45763
+ "listen canary requires --agent-token-file or --agent-token-stdin; credentials are never accepted on argv"
45764
+ );
45765
+ }
45766
+ const cloud = await target(args);
45767
+ const workspaceId2 = listenerUuid(
45768
+ args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
45769
+ "workspace-id"
45770
+ );
45771
+ const agent = await agentCredential(args);
45772
+ if (agent.principalId === null) {
45773
+ throw new Error(
45774
+ "listen canary needs the complete JSON agent credential so it can address the agent and select its listener state"
45775
+ );
45776
+ }
45777
+ const principalId = listenerUuid(agent.principalId, "principal-id");
45778
+ const stateDirectory2 = listenerStateDirectory(args);
45779
+ const paths = listenerPaths({
45780
+ profileId: cloud.profileId,
45781
+ workspaceId: workspaceId2,
45782
+ principalId,
45783
+ ...stateDirectory2 ? { stateDirectory: stateDirectory2 } : {}
45784
+ });
45785
+ const waitMs = parseWaitSeconds(args.optional("wait") ?? "10") * 1e3;
45786
+ const httpClient = new ListenerHttpClient();
45787
+ let result;
45788
+ try {
45789
+ result = await runListenerAttendanceCanary({
45790
+ target: cloud,
45791
+ workspaceId: workspaceId2,
45792
+ principalId,
45793
+ paths,
45794
+ waitMs,
45795
+ fetcher: httpClient.fetch,
45796
+ // Canary must remain read-only apart from its one self-note. It therefore
45797
+ // uses the presented token and never enters the renewal/mint path.
45798
+ credential: async () => agent.token
45799
+ });
45800
+ } finally {
45801
+ httpClient.close();
43459
45802
  }
45803
+ if (args.has("json")) {
45804
+ printJson({
45805
+ workspaceId: workspaceId2,
45806
+ principalId,
45807
+ ...result
45808
+ });
45809
+ return;
45810
+ }
45811
+ process.stdout.write(
45812
+ `${renderListenerAttendanceCanary(result, workspaceId2, principalId)}
45813
+ `
45814
+ );
43460
45815
  }
43461
45816
  async function runListen(args) {
43462
45817
  const command2 = args.positionals[1];
@@ -43468,7 +45823,11 @@ async function runListen(args) {
43468
45823
  await runListenStatusOrStop(args, command2);
43469
45824
  return;
43470
45825
  }
43471
- throw new UsageError("listen requires start, status, or stop");
45826
+ if (command2 === "canary") {
45827
+ await runListenCanary(args);
45828
+ return;
45829
+ }
45830
+ throw new UsageError("listen requires start, status, stop, or canary");
43472
45831
  }
43473
45832
  var CLAUDE_HOOK_COMMAND = "cswarm hook check";
43474
45833
  function scopedClaudeHookCommand(principalId) {
@@ -43477,6 +45836,46 @@ function scopedClaudeHookCommand(principalId) {
43477
45836
  function isCommonSwarmClaudeHook(value) {
43478
45837
  return typeof value === "string" && (value === CLAUDE_HOOK_COMMAND || /^cswarm hook check --principal-id [0-9a-f-]{36}$/.test(value));
43479
45838
  }
45839
+ var ListenerUnattendedRefusedError = class extends Error {
45840
+ code = "listen_unattended_refused";
45841
+ constructor(principalId) {
45842
+ super(
45843
+ `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.`
45844
+ );
45845
+ this.name = "ListenerUnattendedRefusedError";
45846
+ }
45847
+ };
45848
+ function settingsHaveScopedClaudeHook(settings, principalId) {
45849
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks)) {
45850
+ return false;
45851
+ }
45852
+ const promptHooks = settings.hooks.UserPromptSubmit;
45853
+ if (!Array.isArray(promptHooks)) return false;
45854
+ const expected = scopedClaudeHookCommand(principalId);
45855
+ return promptHooks.some((group) => {
45856
+ if (!group || typeof group !== "object" || Array.isArray(group)) return false;
45857
+ const hooks = group.hooks;
45858
+ return Array.isArray(hooks) && hooks.some(
45859
+ (hook) => hook !== null && typeof hook === "object" && !Array.isArray(hook) && hook.type === "command" && hook.command === expected
45860
+ );
45861
+ });
45862
+ }
45863
+ async function listenerHasAttendanceSurface(instanceDirectory, cwd, principalId) {
45864
+ const surface = await new FileHookSurfaceStore(instanceDirectory).evidence();
45865
+ if (surface.exists) return true;
45866
+ const repositoryRoot = gitRepositoryRoot(cwd) ?? cwd;
45867
+ const settingsPaths = /* @__PURE__ */ new Set([
45868
+ (0, import_node_path21.join)(repositoryRoot, CLAUDE_PROJECT_SETTINGS_IGNORE_LINE),
45869
+ (0, import_node_path21.join)(repositoryRoot, CLAUDE_REPO_SETTINGS_IGNORE_LINE),
45870
+ userClaudeSettingsTarget().path
45871
+ ]);
45872
+ for (const path of settingsPaths) {
45873
+ if (settingsHaveScopedClaudeHook(readClaudeSettings(path), principalId)) {
45874
+ return true;
45875
+ }
45876
+ }
45877
+ return false;
45878
+ }
43480
45879
  function claudeUserPromptHookSnippet(principalId) {
43481
45880
  return {
43482
45881
  hooks: {
@@ -43495,7 +45894,9 @@ function claudeUserPromptHookSnippet(principalId) {
43495
45894
  }
43496
45895
  var CLAUDE_PROJECT_SETTINGS_IGNORE_LINE = ".claude/settings.local.json";
43497
45896
  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.";
45897
+ function claudeUserScopeWarning(settingsPath) {
45898
+ return `Warning: --user scope writes settings to ${(0, import_node_path21.dirname)(settingsPath)} and applies to every Claude Code session that reads that directory.`;
45899
+ }
43499
45900
  function userClaudeSettingsTarget() {
43500
45901
  const configured = process.env.CLAUDE_CONFIG_DIR;
43501
45902
  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 +45907,7 @@ function userClaudeSettingsTarget() {
43506
45907
  };
43507
45908
  }
43508
45909
  function gitRepositoryRoot(cwd) {
43509
- const result = (0, import_node_child_process8.spawnSync)(
45910
+ const result = (0, import_node_child_process9.spawnSync)(
43510
45911
  "git",
43511
45912
  ["-C", cwd, "rev-parse", "--show-toplevel"],
43512
45913
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
@@ -43526,12 +45927,12 @@ function projectClaudeSettingsTarget(scope, ignoreLine) {
43526
45927
  const base = root ?? process.cwd();
43527
45928
  const path = (0, import_node_path21.join)(base, ignoreLine);
43528
45929
  if (root === null) return { path, scope, projectRoot: base };
43529
- const tracked = (0, import_node_child_process8.spawnSync)(
45930
+ const tracked = (0, import_node_child_process9.spawnSync)(
43530
45931
  "git",
43531
45932
  ["-C", root, "ls-files", "--error-unmatch", "--", ignoreLine],
43532
45933
  { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }
43533
45934
  );
43534
- const ignored = (0, import_node_child_process8.spawnSync)(
45935
+ const ignored = (0, import_node_child_process9.spawnSync)(
43535
45936
  "git",
43536
45937
  ["-C", root, "check-ignore", "--quiet", "--", ignoreLine],
43537
45938
  { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] }
@@ -43664,19 +46065,25 @@ async function runHook(args) {
43664
46065
  process.exit(0);
43665
46066
  }, 3e3);
43666
46067
  hardExit.unref();
43667
- await runListenerHookCheck({
43668
- ...cooldownSeconds === void 0 ? {} : { cooldownSeconds },
43669
- ...principalIds.length === 0 ? {} : { principalIds },
43670
- write: async (output) => {
43671
- await new Promise((resolve2, reject) => {
43672
- process.stdout.write(`${output}
46068
+ const httpClient = new ListenerHttpClient();
46069
+ try {
46070
+ await runListenerHookCheck({
46071
+ ...cooldownSeconds === void 0 ? {} : { cooldownSeconds },
46072
+ ...principalIds.length === 0 ? {} : { principalIds },
46073
+ fetcher: httpClient.fetch,
46074
+ write: async (output) => {
46075
+ await new Promise((resolve2, reject) => {
46076
+ process.stdout.write(`${output}
43673
46077
  `, (error) => {
43674
- if (error) reject(error);
43675
- else resolve2();
46078
+ if (error) reject(error);
46079
+ else resolve2();
46080
+ });
43676
46081
  });
43677
- });
43678
- }
43679
- });
46082
+ }
46083
+ });
46084
+ } finally {
46085
+ httpClient.close();
46086
+ }
43680
46087
  return;
43681
46088
  }
43682
46089
  if (command2 !== "install" && command2 !== "uninstall") {
@@ -43713,7 +46120,7 @@ async function runHook(args) {
43713
46120
  const settings = readClaudeSettings(path);
43714
46121
  const updated = command2 === "install" ? installClaudeHook(settings, principalId) : uninstallClaudeHook(settings);
43715
46122
  if (target2.scope === "user") {
43716
- process.stdout.write(`${CLAUDE_USER_SCOPE_WARNING}
46123
+ process.stdout.write(`${claudeUserScopeWarning(path)}
43717
46124
  `);
43718
46125
  }
43719
46126
  (0, import_node_fs7.mkdirSync)((0, import_node_path21.dirname)(path), { recursive: true });
@@ -43802,8 +46209,8 @@ async function uploadNamedFile(context, name, bytes) {
43802
46209
  workspaceId: context.selected.selectedWorkspace,
43803
46210
  credential: context.selected.bearer
43804
46211
  };
43805
- const fileId = (0, import_node_crypto20.randomUUID)();
43806
- const versionId = (0, import_node_crypto20.randomUUID)();
46212
+ const fileId = (0, import_node_crypto21.randomUUID)();
46213
+ const versionId = (0, import_node_crypto21.randomUUID)();
43807
46214
  const createCommandId = newCommandId();
43808
46215
  const commitCommandId = newCommandId();
43809
46216
  const created = await onceRetried(
@@ -44214,7 +46621,7 @@ async function runDogfood(args) {
44214
46621
  const { selectedWorkspace, bearer } = await commandWorkspaceAndCredential(args, cloud);
44215
46622
  const client = new ThinCommandClient(cloud);
44216
46623
  const route = stream(args);
44217
- const taskId = args.optional("task-id") ?? (0, import_node_crypto20.randomUUID)();
46624
+ const taskId = args.optional("task-id") ?? (0, import_node_crypto21.randomUUID)();
44218
46625
  const ttl = Number(args.optional("ttl-ms") ?? "3600000");
44219
46626
  if (!Number.isSafeInteger(ttl) || ttl <= 0 || ttl > 144e5) {
44220
46627
  throw new Error("--ttl-ms must be an integer in 1..14400000");
@@ -44280,7 +46687,7 @@ async function runSeed(args) {
44280
46687
  if (!tokenOut || !(0, import_node_path21.isAbsolute)(tokenOut)) {
44281
46688
  throw new Error("SEED_TOKEN_OUT must be an absolute path");
44282
46689
  }
44283
- const tokenFile = await (0, import_promises11.open)(tokenOut, "wx", 384).catch((error) => {
46690
+ const tokenFile = await (0, import_promises12.open)(tokenOut, "wx", 384).catch((error) => {
44284
46691
  if (error.code === "EEXIST") {
44285
46692
  throw new Error("SEED_TOKEN_OUT already exists; refusing to overwrite it");
44286
46693
  }
@@ -44319,7 +46726,7 @@ async function runSeed(args) {
44319
46726
  tokenWritten = true;
44320
46727
  }
44321
46728
  await tokenFile.close();
44322
- if (!tokenWritten) await (0, import_promises11.unlink)(tokenOut);
46729
+ if (!tokenWritten) await (0, import_promises12.unlink)(tokenOut);
44323
46730
  process.stdout.write(`${JSON.stringify({
44324
46731
  userId: result.userId,
44325
46732
  membershipRole: result.membershipRole,
@@ -44332,7 +46739,7 @@ async function runSeed(args) {
44332
46739
  `);
44333
46740
  } catch (error) {
44334
46741
  await tokenFile.close().catch(() => void 0);
44335
- if (!tokenWritten) await (0, import_promises11.unlink)(tokenOut).catch(() => void 0);
46742
+ if (!tokenWritten) await (0, import_promises12.unlink)(tokenOut).catch(() => void 0);
44336
46743
  throw error;
44337
46744
  }
44338
46745
  }
@@ -44433,6 +46840,10 @@ async function main() {
44433
46840
  await runWhoami(args);
44434
46841
  return;
44435
46842
  }
46843
+ if (verb === "resume") {
46844
+ await runResume(args);
46845
+ return;
46846
+ }
44436
46847
  if (verb === "feedback") {
44437
46848
  await runFeedback(args);
44438
46849
  return;
@@ -44518,6 +46929,7 @@ function markRestartable(error) {
44518
46929
  return error;
44519
46930
  }
44520
46931
  function exitCodeFor(error) {
46932
+ if (error instanceof NotifyStdoutClosedError) return EXIT_NOTIFY_ORPHANED;
44521
46933
  return error instanceof Error ? restartableExit.get(error) ?? 1 : 1;
44522
46934
  }
44523
46935
  function safeParagraph(message) {
@@ -44574,6 +46986,7 @@ ${usage()}
44574
46986
  // Annotate the CommonJS export names for ESM import in node:
44575
46987
  0 && (module.exports = {
44576
46988
  EXIT_RESTARTABLE,
46989
+ ListenerUnattendedRefusedError,
44577
46990
  TURN_BUDGET_CREDENTIAL_MARGIN_MS,
44578
46991
  clampTurnBudgetToCredential,
44579
46992
  claudeUserPromptHookSnippet,
@@ -44581,6 +46994,7 @@ ${usage()}
44581
46994
  listenerFailureMessage,
44582
46995
  listenerHostLimits,
44583
46996
  listenerPermissionMode,
46997
+ listenerProviderInstallEvidence,
44584
46998
  listenerRouteConfiguration,
44585
46999
  listenerStatusJson,
44586
47000
  renderListenerStatus,