commonswarm 0.1.53 → 0.1.55

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 +822 -52
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -13502,6 +13502,7 @@ var require_main3 = __commonJS({
13502
13502
  // src/cli.ts
13503
13503
  var cli_exports = {};
13504
13504
  __export(cli_exports, {
13505
+ CHANNEL_SUBCOMMAND_NAMES: () => CHANNEL_SUBCOMMAND_NAMES,
13505
13506
  EXIT_RESTARTABLE: () => EXIT_RESTARTABLE,
13506
13507
  ListenerUnattendedRefusedError: () => ListenerUnattendedRefusedError,
13507
13508
  TURN_BUDGET_CREDENTIAL_MARGIN_MS: () => TURN_BUDGET_CREDENTIAL_MARGIN_MS,
@@ -13519,7 +13520,9 @@ __export(cli_exports, {
13519
13520
  replyRefusalHint: () => replyRefusalHint,
13520
13521
  resolveDetachedClaudeExecutable: () => resolveDetachedClaudeExecutable,
13521
13522
  resolveDetachedCodexExecutable: () => resolveDetachedCodexExecutable,
13522
- resolveTurnBudgetOrDefer: () => resolveTurnBudgetOrDefer
13523
+ resolveTurnBudgetOrDefer: () => resolveTurnBudgetOrDefer,
13524
+ threadReplyMessage: () => threadReplyMessage,
13525
+ usage: () => usage
13523
13526
  });
13524
13527
  module.exports = __toCommonJS(cli_exports);
13525
13528
  var import_node_crypto22 = require("node:crypto");
@@ -22141,11 +22144,217 @@ async function logout(target2, store2, scope = "local", options = {}) {
22141
22144
 
22142
22145
  // src/cloud/command-client.ts
22143
22146
  var import_node_crypto3 = require("node:crypto");
22147
+
22148
+ // src/cloud/channels.ts
22149
+ var CHANNEL_SLUG_MAX = 32;
22150
+ var CHANNEL_PURPOSE_MAX = 500;
22151
+ var CHANNEL_SLUG_CLASSES = {
22152
+ edge: [
22153
+ { fragment: "a-z", words: "lowercase letters", one: "a letter" },
22154
+ { fragment: "0-9", words: "digits", one: "a digit" }
22155
+ ],
22156
+ inner: [{ fragment: "-", words: "hyphens" }]
22157
+ };
22158
+ function classList(entries, conjunction = "and", singular = false) {
22159
+ const words = entries.map(
22160
+ (entry) => singular ? entry.one ?? entry.words : entry.words
22161
+ );
22162
+ return words.length === 1 ? words[0] : `${words.slice(0, -1).join(", ")} ${conjunction} ${words[words.length - 1]}`;
22163
+ }
22164
+ var SLUG_EDGE = CHANNEL_SLUG_CLASSES.edge.map((c) => c.fragment).join("");
22165
+ var SLUG_INNER = SLUG_EDGE + CHANNEL_SLUG_CLASSES.inner.map((c) => c.fragment).join("");
22166
+ var CHANNEL_SLUG_RE = new RegExp(
22167
+ `^[${SLUG_EDGE}]([${SLUG_INNER}]*[${SLUG_EDGE}])?$`
22168
+ );
22169
+ var RESERVED_CHANNEL_SLUGS = ["all-signals"];
22170
+ var CHANNEL_SLUG_RULE_TEXT = `A channel name uses ${classList([...CHANNEL_SLUG_CLASSES.edge, ...CHANNEL_SLUG_CLASSES.inner])}, starts and ends with ${classList(CHANNEL_SLUG_CLASSES.edge, "or", true)}, and is 1 to ${CHANNEL_SLUG_MAX} characters.`;
22171
+ var CHANNEL_ID_RULE_TEXT = "channel_id must be a UUID.";
22172
+ var RESERVED_CHANNEL_SLUG_TEXT = `Reserved names: ${RESERVED_CHANNEL_SLUGS.join(", ")}.`;
22173
+ function normalizeChannelSlug(value) {
22174
+ return value.trim().toLowerCase();
22175
+ }
22176
+ function isReservedChannelSlug(value) {
22177
+ return RESERVED_CHANNEL_SLUGS.includes(normalizeChannelSlug(value));
22178
+ }
22179
+ function channelNameProblem(value) {
22180
+ if (typeof value !== "string") return "not-text";
22181
+ const slug = normalizeChannelSlug(value);
22182
+ if (slug.length < 1 || slug.length > CHANNEL_SLUG_MAX) return "shape";
22183
+ if (!CHANNEL_SLUG_RE.test(slug)) return "shape";
22184
+ if (isReservedChannelSlug(slug)) return "reserved";
22185
+ return "ok";
22186
+ }
22187
+ function channelSlugProblem(value) {
22188
+ switch (channelNameProblem(value)) {
22189
+ case "ok":
22190
+ return null;
22191
+ case "not-text":
22192
+ return `A channel name must be text. ${CHANNEL_SLUG_RULE_TEXT}`;
22193
+ case "shape":
22194
+ return CHANNEL_SLUG_RULE_TEXT;
22195
+ case "reserved":
22196
+ return `${normalizeChannelSlug(value)} is reserved. ${RESERVED_CHANNEL_SLUG_TEXT}`;
22197
+ }
22198
+ }
22199
+ var CHANNEL_COLUMNS = [
22200
+ "channel_id",
22201
+ "workspace_id",
22202
+ "slug",
22203
+ "purpose",
22204
+ "created_by_principal",
22205
+ "created_by_kind",
22206
+ "created_at",
22207
+ "archived_at"
22208
+ ];
22209
+ var CHANNEL_LIST_NEEDS_HUMAN_MESSAGE = "Listing channels needs a signed-in person: this deployment's read service has no channel list for an agent credential. Run cswarm channel ls from a session signed in with cswarm login, or name the channel you want by name wherever a command takes one.";
22210
+ var CHANNEL_SELECTOR_NEEDS_ID_MESSAGE = "Turning a channel name into a channel id needs a signed-in person, because this deployment's read service does not list channels for an agent credential. Pass the channel id instead. cswarm channel create prints it, and cswarm channel ls shows it from a session signed in with cswarm login.";
22211
+ function channelSelectorProblem(selector) {
22212
+ const problem = channelNameProblem(selector);
22213
+ if (problem === "ok") return null;
22214
+ if (problem === "reserved") return channelSlugProblem(selector);
22215
+ return `That is neither a channel name nor a channel id. ${CHANNEL_SLUG_RULE_TEXT} ${CHANNEL_ID_RULE_TEXT}`;
22216
+ }
22217
+ var ChannelListError = class extends Error {
22218
+ constructor(status, message, noResponse = false) {
22219
+ super(message);
22220
+ this.status = status;
22221
+ this.noResponse = noResponse;
22222
+ this.name = "ChannelListError";
22223
+ }
22224
+ status;
22225
+ noResponse;
22226
+ };
22227
+ async function listChannelsAsHuman(target2, accessToken, workspaceId2, fetcher = fetch, timeoutMs = 3e4) {
22228
+ const url = new URL("/rest/v1/channels", target2.url);
22229
+ url.searchParams.set("workspace_id", `eq.${workspaceId2}`);
22230
+ url.searchParams.set("select", CHANNEL_COLUMNS.join(","));
22231
+ url.searchParams.set("order", "slug.asc");
22232
+ const controller = new AbortController();
22233
+ const timer2 = setTimeout(() => controller.abort(), timeoutMs);
22234
+ let response;
22235
+ try {
22236
+ response = await fetcher(url.toString(), {
22237
+ headers: {
22238
+ authorization: `Bearer ${accessToken}`,
22239
+ apikey: target2.anonKey,
22240
+ "accept-profile": "swarm_read"
22241
+ },
22242
+ signal: controller.signal
22243
+ });
22244
+ } catch {
22245
+ throw new ChannelListError(
22246
+ 0,
22247
+ "The channel list did not complete. Nothing changed. Run the same command again.",
22248
+ true
22249
+ );
22250
+ } finally {
22251
+ clearTimeout(timer2);
22252
+ }
22253
+ if (!response.ok) {
22254
+ throw new ChannelListError(
22255
+ response.status,
22256
+ `The channel list was refused (HTTP ${response.status}). Nothing changed.`
22257
+ );
22258
+ }
22259
+ let body = null;
22260
+ try {
22261
+ body = await response.json();
22262
+ } catch {
22263
+ body = null;
22264
+ }
22265
+ if (!Array.isArray(body)) {
22266
+ throw new ChannelListError(
22267
+ response.status,
22268
+ "The channel list came back in a shape this version does not understand."
22269
+ );
22270
+ }
22271
+ return body;
22272
+ }
22273
+ function findChannelBySlug(rows3, slug) {
22274
+ const wanted = normalizeChannelSlug(slug);
22275
+ return rows3.find((row) => normalizeChannelSlug(row.slug) === wanted) ?? null;
22276
+ }
22277
+ function unknownChannelMessage(slug, rows3) {
22278
+ const live = rows3.filter((row) => row.archived_at === null).map((row) => row.slug).sort();
22279
+ return live.length === 0 ? `There is no channel named ${normalizeChannelSlug(slug)} in this workspace, and no channel has been created yet. Create it with cswarm channel create ${normalizeChannelSlug(slug)}.` : `There is no channel named ${normalizeChannelSlug(slug)} in this workspace. Channels here: ${live.join(", ")}.`;
22280
+ }
22281
+ function renderChannelList(rows3, options) {
22282
+ const live = rows3.filter((row) => row.archived_at === null);
22283
+ const archived = rows3.filter((row) => row.archived_at !== null);
22284
+ const shown = options.includeArchived ? [...live, ...archived] : live;
22285
+ if (shown.length === 0) {
22286
+ return live.length === 0 && archived.length > 0 ? "Every channel in this workspace is archived. See them with cswarm channel ls --include-archived.\n" : "No channels in this workspace yet. Create one with cswarm channel create <name>.\n";
22287
+ }
22288
+ const lines = shown.map((row) => {
22289
+ const marker = row.archived_at === null ? "" : " [archived; it keeps its history and takes no new messages]";
22290
+ const purpose = row.purpose === null ? "" : `: ${row.purpose}`;
22291
+ return `- ${row.slug}${purpose}${marker}`;
22292
+ });
22293
+ const head2 = `Channels in this workspace (${shown.length}):`;
22294
+ const tail = options.includeArchived || archived.length === 0 ? "" : `
22295
+ ${archived.length} archived channel${archived.length === 1 ? "" : "s"} not shown. See them with cswarm channel ls --include-archived.`;
22296
+ return `${head2}
22297
+ ${lines.join("\n")}${tail}
22298
+ `;
22299
+ }
22300
+ var CHANNEL_UNSUPPORTED_MESSAGE = `This deployment refused the request and did not say why. Channels need a deployment whose command service knows them; this cswarm speaks protocol ${CLIENT_PROTOCOL_VERSION}. Nothing was created or changed. Ask whoever runs this deployment to update it, or drop the channel options and post as before.`;
22301
+
22302
+ // src/cloud/command-client.ts
22144
22303
  var AGENT_TOKEN_RE = /^swm_agt_[A-Za-z0-9_-]{43}$/;
22145
22304
  var INVITATION_TOKEN_RE = /^swm_inv_[A-Za-z0-9_-]{43}$/;
22146
22305
  var CAPABILITY_TOKEN_RE = /^swm_cap_[A-Za-z0-9_-]{43}$/;
22147
22306
  var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
22148
22307
  var WORKSPACE_NAME_MAX_LENGTH = 80;
22308
+ var ChannelCommandError = class extends Error {
22309
+ constructor(status, code, message) {
22310
+ super(message);
22311
+ this.status = status;
22312
+ this.code = code;
22313
+ this.name = "ChannelCommandError";
22314
+ }
22315
+ status;
22316
+ code;
22317
+ };
22318
+ function channelCommandError(status, body) {
22319
+ const record = body && typeof body === "object" && !Array.isArray(body) ? body : {};
22320
+ const code = typeof record.error === "string" ? record.error : "unknown";
22321
+ const served = typeof record.message === "string" && record.message.length > 0 ? record.message.slice(0, 600) : null;
22322
+ if (served !== null) return new ChannelCommandError(status, code, served);
22323
+ if (status === 426) {
22324
+ const minimum = typeof record.min_client_version === "string" ? record.min_client_version : null;
22325
+ return new ChannelCommandError(
22326
+ status,
22327
+ "upgrade_required",
22328
+ `This copy of cswarm is older than the deployment accepts${minimum === null ? "" : ` (minimum ${minimum})`}. Update cswarm, then run the same command again. Nothing changed.`
22329
+ );
22330
+ }
22331
+ if (status === 403) {
22332
+ return new ChannelCommandError(
22333
+ status,
22334
+ code === "unknown" ? "forbidden" : code,
22335
+ "This credential may not do that in this workspace. Nothing changed."
22336
+ );
22337
+ }
22338
+ if (status === 401) {
22339
+ return new ChannelCommandError(
22340
+ status,
22341
+ code === "unknown" ? "unauthenticated" : code,
22342
+ "Your sign-in is no longer valid for this deployment. Run cswarm login, then run the same command again. Nothing changed."
22343
+ );
22344
+ }
22345
+ if (status === 400) {
22346
+ return new ChannelCommandError(
22347
+ status,
22348
+ code === "unknown" ? "invalid_request" : code,
22349
+ CHANNEL_UNSUPPORTED_MESSAGE
22350
+ );
22351
+ }
22352
+ return new ChannelCommandError(
22353
+ status,
22354
+ code,
22355
+ `CommonSwarm could not tell whether the change was made (HTTP ${status}). Run cswarm channel ls to see the current channels before trying again.`
22356
+ );
22357
+ }
22149
22358
  var CAPABILITY_MIN_TTL_MS = 6e4;
22150
22359
  var CAPABILITY_MAX_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
22151
22360
  var SIGNAL_REQUEST_TIMEOUT_MS = 3e4;
@@ -22705,6 +22914,80 @@ var ThinCommandClient = class {
22705
22914
  }
22706
22915
  return { httpStatus: response.status, response: body };
22707
22916
  }
22917
+ /**
22918
+ * Create, rename, or archive a channel.
22919
+ *
22920
+ * Deliberately not folded into `sendConnect`. That path throws a bare
22921
+ * `CommandHttpError(403)` and, on any other refusal, reads only the `error`
22922
+ * code — so every generated sentence the chat validator returns would be
22923
+ * thrown away at the one moment the caller needs it. This method reads the
22924
+ * body once and hands it to `channelCommandError`.
22925
+ */
22926
+ async sendChannel(request) {
22927
+ if (!request.workspaceId) {
22928
+ throw new Error("workspaceId is required for a channel command");
22929
+ }
22930
+ const commandId = request.commandId ?? newCommandId();
22931
+ const controller = new AbortController();
22932
+ const timer2 = setTimeout(() => controller.abort(), 3e4);
22933
+ let response;
22934
+ try {
22935
+ response = await this.fetcher(commandEndpoint(this.target), {
22936
+ method: "POST",
22937
+ headers: {
22938
+ authorization: `Bearer ${request.credential}`,
22939
+ apikey: this.target.anonKey,
22940
+ "content-type": "application/json"
22941
+ },
22942
+ body: JSON.stringify({
22943
+ command_id: commandId,
22944
+ client_version: CLIENT_PROTOCOL_VERSION,
22945
+ workspace_id: request.workspaceId,
22946
+ stream: { kind: "workspace" },
22947
+ command: request.command
22948
+ }),
22949
+ signal: controller.signal
22950
+ });
22951
+ } catch (error) {
22952
+ if (error.name === "AbortError") {
22953
+ throw new CommandTransportError("channel request timed out");
22954
+ }
22955
+ throw new CommandTransportError(
22956
+ "channel request failed before a response"
22957
+ );
22958
+ } finally {
22959
+ clearTimeout(timer2);
22960
+ }
22961
+ let raw = null;
22962
+ try {
22963
+ raw = await parsedJson(response);
22964
+ } catch (error) {
22965
+ if (response.ok || error instanceof CommandTransportError) throw error;
22966
+ }
22967
+ if (!response.ok) throw channelCommandError(response.status, raw);
22968
+ const body = responseBody(raw);
22969
+ if (body.min_client_version !== void 0) {
22970
+ const order = compareVersion(
22971
+ CLIENT_PROTOCOL_VERSION,
22972
+ body.min_client_version
22973
+ );
22974
+ if (order === null) {
22975
+ throw new Error("server returned a malformed min_client_version");
22976
+ }
22977
+ if (order < 0) {
22978
+ throw new Error(
22979
+ `client upgrade required (minimum ${body.min_client_version})`
22980
+ );
22981
+ }
22982
+ }
22983
+ const channel = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.channel : null;
22984
+ if (channel === null || channel === void 0 || typeof channel.channel_id !== "string" || typeof channel.slug !== "string") {
22985
+ throw new Error(
22986
+ "the deployment accepted the change without saying which channel it applies to"
22987
+ );
22988
+ }
22989
+ return { httpStatus: response.status, response: body, channel };
22990
+ }
22708
22991
  async sendSignal(request) {
22709
22992
  const commandId = request.commandId ?? newCommandId();
22710
22993
  const command2 = {
@@ -22716,7 +22999,17 @@ var ThinCommandClient = class {
22716
22999
  in_reply_to: request.command.in_reply_to,
22717
23000
  about: request.command.about,
22718
23001
  ...request.command.attachments === void 0 ? {} : { attachments: request.command.attachments },
22719
- ...request.command.until_ms === void 0 ? {} : { until_ms: request.command.until_ms }
23002
+ ...request.command.until_ms === void 0 ? {} : { until_ms: request.command.until_ms },
23003
+ /* One spread per chat key, never a shared group. The edge reads each with
23004
+ * its own Object.hasOwn and refuses any key it did not expect, so sending
23005
+ * `channel: undefined` here would still put the key on the wire through
23006
+ * JSON.stringify's own omission rules only by accident — and sending a
23007
+ * null placeholder, the way to_user_id is sent, would make every post
23008
+ * demand a channel. This rebuild is also the reason the fields have to be
23009
+ * listed here at all: it drops anything it does not name. */
23010
+ ...request.command.channel === void 0 ? {} : { channel: request.command.channel },
23011
+ ...request.command.thread_root_id === void 0 ? {} : { thread_root_id: request.command.thread_root_id },
23012
+ ...request.command.broadcast_to_channel === void 0 ? {} : { broadcast_to_channel: request.command.broadcast_to_channel }
22720
23013
  };
22721
23014
  const callerSignal = request.signal;
22722
23015
  if (callerSignal?.aborted) {
@@ -29040,6 +29333,12 @@ function checkedUuid2(value, field) {
29040
29333
  function checkedNullableUuid(value, field) {
29041
29334
  return value === null ? null : checkedUuid2(value, field);
29042
29335
  }
29336
+ function checkedBoolean(value, field) {
29337
+ if (typeof value !== "boolean") {
29338
+ throw new Error(`signal read returned a malformed ${field}`);
29339
+ }
29340
+ return value;
29341
+ }
29043
29342
  function checkedTimestamp(value, field) {
29044
29343
  if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
29045
29344
  throw new Error(`signal read returned a malformed ${field}`);
@@ -29119,7 +29418,29 @@ function parseSignalRecord(value, options = {}) {
29119
29418
  }),
29120
29419
  until: checkedTimestamp(row.until, "until"),
29121
29420
  created_at: checkedTimestamp(row.created_at, "created_at"),
29122
- sender_owner_relation: senderOwnerRelation
29421
+ sender_owner_relation: senderOwnerRelation,
29422
+ /* ABSENT AND NULL ARE DIFFERENT HERE, and the difference is a claim.
29423
+ *
29424
+ * `null` means the server said this signal is in no channel. Absent means
29425
+ * this reader never asked: the human REST path names the chat columns only
29426
+ * when a channel filter is set, and an edge that predates channels never
29427
+ * returns them. Normalizing absence to null, the way `to_agent` above does,
29428
+ * would put `"channel_id": null` in `cswarm feed --json` for a signal that
29429
+ * IS filed in a channel — a false statement, not a missing one. So an
29430
+ * absent key stays absent, and a present one is checked. */
29431
+ ...row.channel_id === void 0 ? {} : { channel_id: checkedNullableUuid(row.channel_id, "channel_id") },
29432
+ ...row.thread_root_id === void 0 ? {} : {
29433
+ thread_root_id: checkedNullableUuid(
29434
+ row.thread_root_id,
29435
+ "thread_root_id"
29436
+ )
29437
+ },
29438
+ ...row.broadcast_to_channel === void 0 ? {} : {
29439
+ broadcast_to_channel: checkedBoolean(
29440
+ row.broadcast_to_channel,
29441
+ "broadcast_to_channel"
29442
+ )
29443
+ }
29123
29444
  };
29124
29445
  }
29125
29446
  function cursorFromUnknown(value) {
@@ -29431,9 +29752,15 @@ async function humanSignals(target2, credential, query, options) {
29431
29752
  const url = new URL("/rest/v1/signals", target2.url);
29432
29753
  url.searchParams.set(
29433
29754
  "select",
29434
- "id,workspace_id,from,from_kind,to,to_agent,in_reply_to,about,kind,body,attachments,until,created_at"
29755
+ [
29756
+ "id,workspace_id,from,from_kind,to,to_agent,in_reply_to,about,kind,body,attachments,until,created_at",
29757
+ ...query.channelId === void 0 ? [] : ["channel_id", "thread_root_id", "broadcast_to_channel"]
29758
+ ].join(",")
29435
29759
  );
29436
29760
  url.searchParams.set("workspace_id", `eq.${query.workspaceId}`);
29761
+ if (query.channelId !== void 0) {
29762
+ url.searchParams.set("channel_id", `eq.${query.channelId}`);
29763
+ }
29437
29764
  if (query.inbox) url.searchParams.set("to", `eq.${credential.userId}`);
29438
29765
  if (!query.includeStale) {
29439
29766
  url.searchParams.set("until", "gt.now");
@@ -29506,6 +29833,11 @@ async function agentSignalPage(target2, credential, query, options, allowLegacyC
29506
29833
  about: query.about ?? null,
29507
29834
  kind: query.kind ?? null,
29508
29835
  in_reply_to: query.in_reply_to ?? null,
29836
+ /* Its OWN key, present only when asked for. The read edge groups it
29837
+ * with `chatReadKeys` and refuses a key it did not expect, and every
29838
+ * agent body already carries `in_reply_to`, so folding `channel` in
29839
+ * beside it would 400 every agent read that omits a channel. */
29840
+ ...query.channel === void 0 ? {} : { channel: query.channel },
29509
29841
  since: query.since ?? null,
29510
29842
  ...includeCursor ? {
29511
29843
  after_created_at: query.after?.created_at ?? null,
@@ -34518,6 +34850,25 @@ async function resolveBudgetAndPrompt(session, prompt, budget) {
34518
34850
  const timeoutMs = typeof budget === "number" ? budget : await budget();
34519
34851
  return await session.prompt(prompt, { timeoutMs });
34520
34852
  }
34853
+ var LISTENER_DELIVERY_MAX_LEASE_MS = 9e5;
34854
+ var LISTENER_DELIVERY_HOLD_RELEASE_REASONS = [
34855
+ "hold_budget",
34856
+ "lease_budget"
34857
+ ];
34858
+ var LISTENER_DELIVERY_HOLD_RELEASE_CLAUSES = {
34859
+ hold_budget: "it used the turn budget for one delivery",
34860
+ lease_budget: "what was left of its lease could not cover the next step"
34861
+ };
34862
+ var LISTENER_DELIVERY_HOLD_RELEASE_REMEDIES = {
34863
+ hold_budget: `a larger --turn-budget gives one delivery more of the seat. Past the ${LISTENER_DELIVERY_MAX_LEASE_MS / 6e4} minutes the service leases a delivery for it stops helping, because the turn then outlives its lease and the reply can no longer be acknowledged. The bound is read when the listener starts, so stop this listener and start it again to change it`,
34864
+ /* NOT a cap: nothing clamps the turn budget to the lease, and leaseSpent
34865
+ refuses to START a phase rather than interrupting one, so a 60m budget
34866
+ really does hold the worker for 60m. The sentence says raising past the
34867
+ lease stops helping, and why, which is what the code supports. An earlier
34868
+ version read "up to the 15 minutes the service leases it for", which a
34869
+ review arm read as a cap the code does not enforce. */
34870
+ lease_budget: "nothing needs raising: the row comes back under a new lease of full length, so the next attempt starts with the room this one ran out of. If it keeps being handed back, the service stops retrying it in the end, so look at the delivery rather than at the bound"
34871
+ };
34521
34872
 
34522
34873
  // src/listener/engine.ts
34523
34874
  var UUID_RE14 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -36739,11 +37090,11 @@ function pendingMainEntry(signal, principalId, provenance, now, options = {}) {
36739
37090
  // src/listener/runtime.ts
36740
37091
  var LISTENER_PAGE_LIMIT = 100;
36741
37092
  var LISTENER_IDLE_POLL_MS = 2e3;
36742
- var LISTENER_DELIVERY_MAX_LEASE_MS = 9e5;
36743
37093
  var LISTENER_DELIVERY_SAFETY_MARGIN_MS = 3e4;
36744
37094
  var LISTENER_ACK_ONLY_MINIMUM_MS = DELIVERY_REQUEST_TIMEOUT_MS + LISTENER_DELIVERY_SAFETY_MARGIN_MS;
36745
37095
  var LISTENER_REPLY_ONLY_MINIMUM_MS = SIGNAL_REQUEST_TIMEOUT_MS + LISTENER_ACK_ONLY_MINIMUM_MS;
36746
37096
  var LISTENER_PROMPT_START_MINIMUM_MS = SIGNAL_READ_TIMEOUT_MS + ACP_DEFAULT_REQUEST_TIMEOUT_MS + LISTENER_REPLY_ONLY_MINIMUM_MS;
37097
+ var LISTENER_DELIVERY_HOLD_BUDGET_MS = LISTENER_PROMPT_TIMEOUT_MS;
36747
37098
  var LISTENER_DELIVERY_RETRY_INITIAL_MS = 500;
36748
37099
  var LISTENER_DELIVERY_RETRY_MAX_MS = 3e4;
36749
37100
  var LISTENER_HOST_PORTS_PROBE_MS = 6e4;
@@ -37016,6 +37367,7 @@ async function runListenerRuntime(options) {
37016
37367
  const pollMs = options.pollMs ?? LISTENER_IDLE_POLL_MS;
37017
37368
  const routeMode = options.routeMode ?? "worker";
37018
37369
  const deferOverChars = options.deferOverChars ?? null;
37370
+ const deliveryHoldBudgetMs = options.deliveryHoldBudgetMs ?? LISTENER_DELIVERY_HOLD_BUDGET_MS;
37019
37371
  const abort = options.signal;
37020
37372
  const hasInstanceId = options.listenerInstanceId !== void 0;
37021
37373
  const hasJournal = options.deliveryJournal !== void 0;
@@ -37037,6 +37389,12 @@ async function runListenerRuntime(options) {
37037
37389
  new Error("an injected delivery client requires durable delivery configuration")
37038
37390
  );
37039
37391
  }
37392
+ if (!Number.isSafeInteger(deliveryHoldBudgetMs) || deliveryHoldBudgetMs <= 0) {
37393
+ return await closeBeforeStart(
37394
+ options.model,
37395
+ new Error("listener delivery hold budget must be a positive number of milliseconds")
37396
+ );
37397
+ }
37040
37398
  try {
37041
37399
  decideListenerRoute(routeMode, deferOverChars, 0);
37042
37400
  if (routeMode !== "worker" && options.pendingMainQueue === void 0) {
@@ -37630,6 +37988,8 @@ async function runListenerRuntime(options) {
37630
37988
  stop = { reason: "cancelled" };
37631
37989
  break;
37632
37990
  }
37991
+ const claimedAtMs = Date.parse(active.claimCreatedAt);
37992
+ const holdStartedAtMs = Number.isFinite(claimedAtMs) ? Math.min(claimedAtMs, now()) : now();
37633
37993
  const signal = authoritativeSignal(claimed);
37634
37994
  let terminal = null;
37635
37995
  try {
@@ -37696,22 +38056,18 @@ async function runListenerRuntime(options) {
37696
38056
  throw new Error("stored listener effect does not match the authoritative delivery");
37697
38057
  }
37698
38058
  const requiredBudget = effectPhaseBudget(before);
37699
- if (leasedUntilMs <= now() + requiredBudget) {
37700
- await sleep2(
37701
- Math.max(
37702
- 0,
37703
- leasedUntilMs + LISTENER_DELIVERY_SAFETY_MARGIN_MS - now()
37704
- ),
37705
- abort
37706
- );
37707
- if (abort?.aborted) {
37708
- stop = { reason: "cancelled" };
37709
- break;
37710
- }
37711
- if (now() >= leasedUntilMs + LISTENER_DELIVERY_SAFETY_MARGIN_MS) {
37712
- await journal.clearActive(eventTime(now));
37713
- after = null;
37714
- }
38059
+ const holdSpent = processAttempt > 0 && now() - holdStartedAtMs >= deliveryHoldBudgetMs;
38060
+ const leaseSpent = leasedUntilMs <= now() + requiredBudget;
38061
+ if (holdSpent || leaseSpent) {
38062
+ await journal.clearActive(eventTime(now));
38063
+ after = null;
38064
+ options.onEvent?.({
38065
+ type: "delivery_hold_released",
38066
+ signalId: signal.id,
38067
+ reason: holdSpent ? "hold_budget" : "lease_budget",
38068
+ heldMs: Math.max(0, now() - holdStartedAtMs),
38069
+ ts: eventTime(now)
38070
+ });
37715
38071
  break;
37716
38072
  }
37717
38073
  const processed = await engine.process(signal);
@@ -38210,6 +38566,10 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
38210
38566
  "lastAckOutcome",
38211
38567
  "consecutiveAckFailureCount",
38212
38568
  "lastAckSignalId",
38569
+ "currentDeliverySignalId",
38570
+ "currentDeliverySince",
38571
+ "heldBackDeliveries",
38572
+ "pendingDeliveryCountAt",
38213
38573
  "routeMode",
38214
38574
  "deferOverChars",
38215
38575
  "pendingForMainCount",
@@ -38254,6 +38614,30 @@ var STATUS_DELIVERY_KEYS = [
38254
38614
  "consecutiveAckFailureCount"
38255
38615
  ];
38256
38616
  var deliveryOutcomes = DELIVERY_ACK_OUTCOMES;
38617
+ var LISTENER_HELD_BACK_MAX = 16;
38618
+ function parseHeldBackDeliveries(value) {
38619
+ if (!Array.isArray(value) || value.length > LISTENER_HELD_BACK_MAX) return null;
38620
+ const parsed = [];
38621
+ for (const item of value) {
38622
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
38623
+ const entry = item;
38624
+ for (const key2 of Object.keys(entry)) {
38625
+ if (key2 !== "signalId" && key2 !== "at" && key2 !== "reason") return null;
38626
+ }
38627
+ if (typeof entry.signalId !== "string" || !UUID_RE18.test(entry.signalId) || typeof entry.at !== "string" || !Number.isFinite(Date.parse(entry.at)) || typeof entry.reason !== "string" || !LISTENER_DELIVERY_HOLD_RELEASE_REASONS.includes(
38628
+ entry.reason
38629
+ )) {
38630
+ return null;
38631
+ }
38632
+ if (parsed.some((seen) => seen.signalId === entry.signalId)) return null;
38633
+ parsed.push({
38634
+ signalId: entry.signalId,
38635
+ at: entry.at,
38636
+ reason: entry.reason
38637
+ });
38638
+ }
38639
+ return parsed;
38640
+ }
38257
38641
  function parseStatus(raw, rejectUnknownKeys = false) {
38258
38642
  let value;
38259
38643
  try {
@@ -38277,7 +38661,8 @@ function parseStatus(raw, rejectUnknownKeys = false) {
38277
38661
  const nullableCount = (candidate) => candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
38278
38662
  const nullableTimestamp3 = (candidate) => candidate === null || typeof candidate === "string" && Number.isFinite(Date.parse(candidate));
38279
38663
  const readHealth = row.readHealth === void 0 ? void 0 : parseListenerReadHealth(row.readHealth, rejectUnknownKeys);
38280
- if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE18.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE18.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE18.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.lastAckOutcome === void 0 || row.lastAckOutcome === null || typeof row.lastAckOutcome === "string" && deliveryOutcomes.has(row.lastAckOutcome)) || !(row.consecutiveAckFailureCount === void 0 || nullableCount(row.consecutiveAckFailureCount)) || !(row.lastAckSignalId === void 0 || row.lastAckSignalId === null || typeof row.lastAckSignalId === "string" && UUID_RE18.test(row.lastAckSignalId)) || !(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) || !(row.activityPublishFailures === void 0 || typeof row.activityPublishFailures === "number" && Number.isSafeInteger(row.activityPublishFailures) && row.activityPublishFailures >= 0) || !(row.activityLastErrorCode === void 0 || row.activityLastErrorCode === null || typeof row.activityLastErrorCode === "string" && STATUS_ACTIVITY_ERROR_CODES.has(
38664
+ const heldBackDeliveries = row.heldBackDeliveries === void 0 ? void 0 : parseHeldBackDeliveries(row.heldBackDeliveries);
38665
+ if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE18.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE18.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE18.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.lastAckOutcome === void 0 || row.lastAckOutcome === null || typeof row.lastAckOutcome === "string" && deliveryOutcomes.has(row.lastAckOutcome)) || !(row.consecutiveAckFailureCount === void 0 || nullableCount(row.consecutiveAckFailureCount)) || !(row.lastAckSignalId === void 0 || row.lastAckSignalId === null || typeof row.lastAckSignalId === "string" && UUID_RE18.test(row.lastAckSignalId)) || !(row.currentDeliverySignalId === void 0 || row.currentDeliverySignalId === null || typeof row.currentDeliverySignalId === "string" && UUID_RE18.test(row.currentDeliverySignalId)) || !(row.currentDeliverySince === void 0 || nullableTimestamp3(row.currentDeliverySince)) || heldBackDeliveries === null || !(row.pendingDeliveryCountAt === void 0 || nullableTimestamp3(row.pendingDeliveryCountAt)) || !(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) || !(row.activityPublishFailures === void 0 || typeof row.activityPublishFailures === "number" && Number.isSafeInteger(row.activityPublishFailures) && row.activityPublishFailures >= 0) || !(row.activityLastErrorCode === void 0 || row.activityLastErrorCode === null || typeof row.activityLastErrorCode === "string" && STATUS_ACTIVITY_ERROR_CODES.has(
38281
38666
  row.activityLastErrorCode
38282
38667
  ))) {
38283
38668
  throw new Error("stored listener status is malformed");
@@ -38303,6 +38688,16 @@ function parseStatus(raw, rejectUnknownKeys = false) {
38303
38688
  // Optional key: present only when the file carried it, so a status written
38304
38689
  // without it round-trips byte-for-byte (the routeMode pattern).
38305
38690
  ...row.lastAckSignalId === void 0 ? {} : { lastAckSignalId: row.lastAckSignalId ?? null },
38691
+ ...row.currentDeliverySignalId === void 0 ? {} : {
38692
+ currentDeliverySignalId: row.currentDeliverySignalId ?? null
38693
+ },
38694
+ ...row.currentDeliverySince === void 0 ? {} : {
38695
+ currentDeliverySince: row.currentDeliverySince ?? null
38696
+ },
38697
+ ...heldBackDeliveries === void 0 ? {} : { heldBackDeliveries },
38698
+ ...row.pendingDeliveryCountAt === void 0 ? {} : {
38699
+ pendingDeliveryCountAt: row.pendingDeliveryCountAt ?? null
38700
+ },
38306
38701
  lastErrorDetail: row.lastErrorDetail ?? null,
38307
38702
  lastWorkerStderrTail: row.lastWorkerStderrTail ?? null,
38308
38703
  providerVersion: row.providerVersion ?? null,
@@ -38379,7 +38774,10 @@ async function appendListenerEvent(paths, event) {
38379
38774
  "defer_over_chars",
38380
38775
  "body_length",
38381
38776
  "pending_main_count",
38382
- "dropped_count"
38777
+ "dropped_count",
38778
+ // How long one delivery held the worker seat, and why it gave it back.
38779
+ "held_ms",
38780
+ "release_reason"
38383
38781
  ]);
38384
38782
  const deliveryModes = /* @__PURE__ */ new Set(["durable_claim", "cursor_fallback"]);
38385
38783
  const routeModes = /* @__PURE__ */ new Set(["worker", "main", "split"]);
@@ -38441,6 +38839,14 @@ async function appendListenerEvent(paths, event) {
38441
38839
  if ((key2 === "body_length" || key2 === "pending_main_count" || key2 === "dropped_count") && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) {
38442
38840
  throw new Error("listener event main-route count is not allowed");
38443
38841
  }
38842
+ if (key2 === "held_ms" && !(typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) {
38843
+ throw new Error("listener event hold duration is not allowed");
38844
+ }
38845
+ if (key2 === "release_reason" && !(typeof value === "string" && LISTENER_DELIVERY_HOLD_RELEASE_REASONS.includes(
38846
+ value
38847
+ ))) {
38848
+ throw new Error("listener event hold release reason is not allowed");
38849
+ }
38444
38850
  if (key2 === "worker_stderr_tail" && !(typeof value === "string" && value.length > 0 && value.length <= 2048)) {
38445
38851
  throw new Error("listener event stderr tail is not allowed");
38446
38852
  }
@@ -38820,6 +39226,7 @@ async function runListenerSupervisor(options) {
38820
39226
  lastWorkerStderrTail: null,
38821
39227
  deliveryMode: null,
38822
39228
  pendingDeliveryCount: null,
39229
+ pendingDeliveryCountAt: null,
38823
39230
  lastTerminalDeliveryFailureCount: null,
38824
39231
  lastTerminalDeliveryFailureAt: null,
38825
39232
  lastClaimAt: null,
@@ -38831,6 +39238,11 @@ async function runListenerSupervisor(options) {
38831
39238
  lastAckOutcome: carried?.lastAckOutcome ?? null,
38832
39239
  consecutiveAckFailureCount: carried?.consecutiveAckFailureCount ?? null,
38833
39240
  lastAckSignalId: carried?.lastAckSignalId ?? null,
39241
+ /* Never carried across a restart: a seat this process does not hold cannot
39242
+ be reported as held, and the queue age restarts with the observations. */
39243
+ currentDeliverySignalId: null,
39244
+ currentDeliverySince: null,
39245
+ heldBackDeliveries: [],
38834
39246
  routeMode: options.routeMode ?? "worker",
38835
39247
  deferOverChars: options.deferOverChars ?? null,
38836
39248
  pendingForMainCount: 0,
@@ -38864,9 +39276,15 @@ async function runListenerSupervisor(options) {
38864
39276
  chain(() => appendListenerEvent(options.paths, event));
38865
39277
  };
38866
39278
  const transition = (state, changes = {}) => {
39279
+ const notWatching = state === "starting" || state === "stopped" || state === "failed";
38867
39280
  status = {
38868
39281
  ...status,
38869
39282
  ...changes,
39283
+ ...notWatching ? {
39284
+ currentDeliverySignalId: null,
39285
+ currentDeliverySince: null,
39286
+ heldBackDeliveries: []
39287
+ } : {},
38870
39288
  state,
38871
39289
  updatedAt: iso2(now)
38872
39290
  };
@@ -39055,6 +39473,7 @@ async function runListenerSupervisor(options) {
39055
39473
  ...status,
39056
39474
  deliveryMode: event.mode,
39057
39475
  pendingDeliveryCount: event.pendingDeliveryCount,
39476
+ pendingDeliveryCountAt: event.pendingDeliveryCount === null ? null : event.ts,
39058
39477
  updatedAt: event.ts
39059
39478
  };
39060
39479
  persist();
@@ -39067,6 +39486,7 @@ async function runListenerSupervisor(options) {
39067
39486
  return;
39068
39487
  }
39069
39488
  if (event.type === "delivery_claim") {
39489
+ const heldBack = (status.heldBackDeliveries ?? []).filter((entry) => entry.signalId !== event.signalId);
39070
39490
  status = {
39071
39491
  ...status,
39072
39492
  readHealth: recordListenerClaim(
@@ -39074,6 +39494,10 @@ async function runListenerSupervisor(options) {
39074
39494
  event.ts
39075
39495
  ),
39076
39496
  pendingDeliveryCount: event.pendingDeliveryCount,
39497
+ pendingDeliveryCountAt: event.ts,
39498
+ currentDeliverySignalId: event.signalId,
39499
+ currentDeliverySince: event.signalId === null ? null : event.ts,
39500
+ heldBackDeliveries: heldBack,
39077
39501
  lastClaimAt: event.ts,
39078
39502
  updatedAt: event.ts
39079
39503
  };
@@ -39105,6 +39529,36 @@ async function runListenerSupervisor(options) {
39105
39529
  });
39106
39530
  return;
39107
39531
  }
39532
+ if (event.type === "delivery_hold_released") {
39533
+ status = {
39534
+ ...status,
39535
+ currentDeliverySignalId: null,
39536
+ currentDeliverySince: null,
39537
+ /* Held back, NOT waiting to be claimed: the row keeps its live lease,
39538
+ so the service cannot hand it to anyone until that lease expires.
39539
+ Both review arms on 33cd24b measured the earlier wording counting it
39540
+ among deliveries "waiting to be claimed". Newest first, deduplicated
39541
+ on the id (a row can be released, redelivered and released again),
39542
+ and bounded. */
39543
+ heldBackDeliveries: [
39544
+ { signalId: event.signalId, at: event.ts, reason: event.reason },
39545
+ ...(status.heldBackDeliveries ?? []).filter(
39546
+ (entry) => entry.signalId !== event.signalId
39547
+ )
39548
+ ].slice(0, LISTENER_HELD_BACK_MAX),
39549
+ lastSignalId: event.signalId,
39550
+ updatedAt: event.ts
39551
+ };
39552
+ persist();
39553
+ log({
39554
+ ts: event.ts,
39555
+ event: "listener_delivery_hold_released",
39556
+ signal_id: event.signalId,
39557
+ release_reason: event.reason,
39558
+ held_ms: Math.max(0, Math.trunc(event.heldMs))
39559
+ });
39560
+ return;
39561
+ }
39108
39562
  if (event.type === "delivery_ack") {
39109
39563
  const failed = event.outcome === "failed_terminal";
39110
39564
  const providerProven = DELIVERY_PROVIDER_PROVEN_OUTCOMES.has(event.outcome);
@@ -39115,6 +39569,13 @@ async function runListenerSupervisor(options) {
39115
39569
  lastAckSignalId: event.signalId,
39116
39570
  consecutiveAckFailureCount: failed ? (status.consecutiveAckFailureCount ?? 0) + 1 : providerProven ? 0 : status.consecutiveAckFailureCount,
39117
39571
  pendingDeliveryCount: null,
39572
+ pendingDeliveryCountAt: null,
39573
+ currentDeliverySignalId: null,
39574
+ currentDeliverySince: null,
39575
+ // An acknowledged row is answered and gone; drop just that one.
39576
+ heldBackDeliveries: (status.heldBackDeliveries ?? []).filter(
39577
+ (entry) => entry.signalId !== event.signalId
39578
+ ),
39118
39579
  lastSignalId: event.signalId,
39119
39580
  updatedAt: event.ts
39120
39581
  };
@@ -39285,7 +39746,14 @@ async function effectiveListenerStatus(paths) {
39285
39746
  state: "failed",
39286
39747
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
39287
39748
  stoppedAt: (/* @__PURE__ */ new Date()).toISOString(),
39288
- lastErrorCode: "unclean_exit"
39749
+ lastErrorCode: "unclean_exit",
39750
+ /* The process is gone: it holds nothing and observes nothing, so every
39751
+ field whose sentence is rendered in the present tense against read
39752
+ time is cleared. pendingDeliveryCount stays, because its line already
39753
+ says it is what the service reported. */
39754
+ currentDeliverySignalId: null,
39755
+ currentDeliverySince: null,
39756
+ heldBackDeliveries: []
39289
39757
  };
39290
39758
  await writeListenerStatus(paths, failed);
39291
39759
  return failed;
@@ -42069,8 +42537,11 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
42069
42537
  "foreground",
42070
42538
  "grok-executable",
42071
42539
  "head-sha",
42540
+ "broadcast-to-channel",
42541
+ "channel",
42072
42542
  "help",
42073
42543
  "if-version",
42544
+ "include-archived",
42074
42545
  "include-stale",
42075
42546
  "include-tombstoned",
42076
42547
  "invitation-id",
@@ -42090,6 +42561,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
42090
42561
  "permissions",
42091
42562
  "principal-id",
42092
42563
  "provider",
42564
+ "purpose",
42093
42565
  "renewal-grant-id",
42094
42566
  "repo",
42095
42567
  "reveal-anon-key",
@@ -42099,6 +42571,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
42099
42571
  "site",
42100
42572
  "slug",
42101
42573
  "state-dir",
42574
+ "thread",
42102
42575
  "renewal-horizon-days",
42103
42576
  "standing",
42104
42577
  "task-id",
@@ -42119,12 +42592,14 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
42119
42592
  "agent-token-stdin",
42120
42593
  "all-devices",
42121
42594
  "allow-unattended",
42595
+ "broadcast-to-channel",
42122
42596
  "confirm-standing",
42123
42597
  "force-file-store",
42124
42598
  "follow",
42125
42599
  "force",
42126
42600
  "foreground",
42127
42601
  "help",
42602
+ "include-archived",
42128
42603
  "include-stale",
42129
42604
  "include-tombstoned",
42130
42605
  "invitation-token-stdin",
@@ -42137,13 +42612,14 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
42137
42612
  "reveal-anon-key",
42138
42613
  "repo",
42139
42614
  "standing",
42615
+ "thread",
42140
42616
  "user",
42141
42617
  "write"
42142
42618
  ]);
42143
42619
  var UUID_RE23 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
42144
42620
  function packageVersion() {
42145
- if ("0.1.53".length > 0) {
42146
- return "0.1.53";
42621
+ if ("0.1.55".length > 0) {
42622
+ return "0.1.55";
42147
42623
  }
42148
42624
  try {
42149
42625
  const value = JSON.parse(
@@ -42262,15 +42738,19 @@ Usage:
42262
42738
  cswarm whoami ${requiredAgentCredential} [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
42263
42739
  cswarm resume --agent-token-file <path> [--url <url> --anon-key <key>] --workspace-id <uuid> [--json]
42264
42740
  cswarm members [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
42265
- cswarm working-on "<what>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--about <ref>] [--until <dur>] [--json]
42266
- 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
42267
- cswarm ask "<text>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--to <member|agent>] [--about <ref>] [--attach <path> ...] [--until <dur>] [--wait <seconds>] [--json] # text: 1..8000 characters
42268
- cswarm reply <signal-id> "<text>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--attach <path> ...] [--until <dur>] [--json]
42741
+ cswarm working-on "<what>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--about <ref>] [--channel <name>] [--until <dur>] [--json]
42742
+ cswarm note "<text>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--to <member|agent>] [--about <ref>] [--channel <name>] [--attach <path> ...] [--until <dur>] [--json] # text: 1..8000 characters
42743
+ cswarm ask "<text>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--to <member|agent>] [--about <ref>] [--channel <name>] [--attach <path> ...] [--until <dur>] [--wait <seconds>] [--json] # text: 1..8000 characters
42744
+ cswarm reply <signal-id> "<text>" [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--thread [--broadcast-to-channel]] [--attach <path> ...] [--until <dur>] [--json]
42269
42745
  cswarm receipt <signal-id> ${requiredAgentCredential} [--url <url> --anon-key <key>] --workspace-id <uuid> [--json]
42270
- cswarm feed [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--about <ref>] [--kind <kind>] [--since <timestamp>] [--limit <n>] [--include-stale] [--json]
42271
- cswarm inbox [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--kind <kind>] [--about <ref>] [--since <timestamp>] [--limit <n>] [--include-stale] [--wait <seconds>] [--json]
42746
+ cswarm feed [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--about <ref>] [--kind <kind>] [--channel <name>] [--since <timestamp>] [--limit <n>] [--include-stale] [--json]
42747
+ cswarm inbox [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--kind <kind>] [--about <ref>] [--channel <name>] [--since <timestamp>] [--limit <n>] [--include-stale] [--wait <seconds>] [--json]
42272
42748
  cswarm inbox --notify ${requiredAgentCredential} [--url <url> --anon-key <key>] --workspace-id <uuid> [--json]
42273
42749
  cswarm inbox --follow --ndjson [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--kind <kind>] [--about <ref>] [--since <timestamp>] [--limit <n>] [--include-stale]
42750
+ cswarm channel create <name> [--purpose <text>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json] # purpose: at most ${CHANNEL_PURPOSE_MAX} characters
42751
+ cswarm channel ls [--include-archived] [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
42752
+ cswarm channel rename <name|channel-id> <new-name> [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
42753
+ cswarm channel archive <name|channel-id> [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
42274
42754
  cswarm file put <local-path> [--name <name>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
42275
42755
  cswarm file ls [--include-tombstoned] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
42276
42756
  cswarm file get <name|file-id> [--version <n>] [--out <local-path>] [--force] [--url <url> --anon-key <key>] [--workspace-id <uuid>] ${agentCredential2} [--json]
@@ -42327,6 +42807,11 @@ Credential selection for command/dogfood:
42327
42807
  signal command/read only -- either form
42328
42808
  receipt reads only -- either form
42329
42809
  inbox --notify persists a per-agent cursor -- needs principal_id
42810
+ channel create, channel rename, channel archive
42811
+ command only, nothing persisted -- either form
42812
+ channel ls reads swarm_read.channels over REST -- signed-in
42813
+ person only; the read service has no channels
42814
+ resource for an agent credential
42330
42815
  file put, file ls, file get, file rm, file restore,
42331
42816
  brain ls, brain get, brain put
42332
42817
  read and command, nothing persisted -- either form
@@ -42352,6 +42837,14 @@ Credential selection for command/dogfood:
42352
42837
  Found a bug or missing feature in cswarm itself? cswarm feedback sends it to the
42353
42838
  deployment's operators \u2014 agents are encouraged to report friction they hit.
42354
42839
 
42840
+ A channel is where a message is FILED, not who may read it. Everyone in the
42841
+ workspace reads every channel, and --channel changes nothing about who sees a
42842
+ signal. Archiving a channel keeps its history and its permalinks and refuses new
42843
+ messages. cswarm reply --thread answers in the open, in the thread of the signal
42844
+ you name, so it takes no recipient; add --broadcast-to-channel to send that reply
42845
+ to the thread's channel as well. Plain cswarm reply is unchanged and still
42846
+ answers the original author privately.
42847
+
42355
42848
  Signals (intention sharing) accept the same credential selection. Agent mode
42356
42849
  never opens a browser or infers a human's saved workspace. Durations use a whole
42357
42850
  number plus m, h, or d (for example 90m, 24h, or 7d) and are capped at 30d.
@@ -42368,7 +42861,10 @@ due \u2014 a turn never outlives its credential. Right after a rotation the full
42368
42861
  budget is available up to the token TTL minus 60s (about 59m on the default 1h
42369
42862
  TTL); a turn that lands just before a rotation can be clamped to the ~5m
42370
42863
  renewal lead, and if it times out there, durable delivery retries it on the
42371
- fresh credential.
42864
+ fresh credential. The same budget also bounds how long ONE delivery may hold the
42865
+ worker seat across its retries: when it is spent the listener hands the seat
42866
+ back and claims the next delivery. After the lease ends the service either
42867
+ delivers the released one again or terminates it.
42372
42868
 
42373
42869
  listen start --route worker|main|split chooses where directed messages go. worker
42374
42870
  is the unchanged default. main queues every ask or note for the interactive session.
@@ -43859,6 +44355,19 @@ function signalKind(value) {
43859
44355
  }
43860
44356
  return value;
43861
44357
  }
44358
+ function channelOption(args) {
44359
+ const value = args.optional("channel");
44360
+ if (value === void 0) return void 0;
44361
+ const problem = channelSlugProblem(value);
44362
+ if (problem !== null) throw new Error(problem);
44363
+ return normalizeChannelSlug(value);
44364
+ }
44365
+ function unknownChannelReadMessage(error, slug) {
44366
+ const details = followHttpDetails(error);
44367
+ if (details === null || details.status !== 404) return null;
44368
+ if (followErrorEnvelope(error).error !== "channel_not_found") return null;
44369
+ return `There is no channel named ${slug} in this workspace. Nothing was read. Create it with cswarm channel create ${slug}, or drop --channel to read everything.`;
44370
+ }
43862
44371
  function signalDuration(value) {
43863
44372
  if (value === void 0) return void 0;
43864
44373
  const match = /^([1-9]\d*)(m|h|d)$/.exec(value);
@@ -44160,11 +44669,13 @@ async function runPostSignal(args, kind) {
44160
44669
  ...CREDENTIAL_FLAGS,
44161
44670
  ...allowTo ? ["to"] : [],
44162
44671
  "about",
44672
+ "channel",
44163
44673
  "until",
44164
44674
  ...allowWait ? ["wait"] : [],
44165
44675
  ...allowTo ? ["attach"] : [],
44166
44676
  "json"
44167
44677
  ], 2);
44678
+ const channel = channelOption(args);
44168
44679
  const preparedAttachments = allowTo ? prepareSignalAttachments(args.all("attach")) : [];
44169
44680
  const waitSeconds = allowWait && args.optional("wait") !== void 0 ? parseWaitSeconds(args.required("wait")) : void 0;
44170
44681
  const cloud = await target(args);
@@ -44209,7 +44720,8 @@ async function runPostSignal(args, kind) {
44209
44720
  ...postSignalTargets(recipient),
44210
44721
  about: args.optional("about") === void 0 ? null : signalText(args.required("about"), "about"),
44211
44722
  ...attachments.length === 0 ? {} : { attachments },
44212
- ...untilMs2 === void 0 ? {} : { until_ms: untilMs2 }
44723
+ ...untilMs2 === void 0 ? {} : { until_ms: untilMs2 },
44724
+ ...channel === void 0 ? {} : { channel }
44213
44725
  };
44214
44726
  let result;
44215
44727
  try {
@@ -44334,15 +44846,35 @@ function replyRefusalHint(error) {
44334
44846
  if (!(error instanceof CommandHttpError) || error.status !== 403) return null;
44335
44847
  return "reply was refused (403). The most common cause is that the signal was not addressed to you \u2014 you cannot reply to your own ask; reply to the other party's signal, reach someone directly with cswarm ask --to <agent>, or post a channel-visible cswarm note. If you did receive that signal, the refusal is an authorization one instead: the credential may be revoked or expired, or it may not be a member of this workspace.";
44336
44848
  }
44849
+ function threadReplyMessage(signal, options) {
44850
+ if (!options.inThread) {
44851
+ return "Reply shared. It is immutable and addressed to the original author.";
44852
+ }
44853
+ const inThread = "Reply shared in the thread. It is immutable and readable by everyone who can read the thread.";
44854
+ if (!options.broadcastToChannel) return inThread;
44855
+ if (signal.channel_id === void 0) {
44856
+ return `${inThread} This deployment did not say which channel the thread is in, so whether it also reached a channel is unknown.`;
44857
+ }
44858
+ return signal.channel_id === null ? `${inThread} Its thread is in no channel, so --broadcast-to-channel had nothing to send it to.` : "Reply shared in the thread and sent to the thread's channel as well. It is immutable and readable by everyone who can read the thread.";
44859
+ }
44337
44860
  async function runReply(args) {
44338
44861
  args.assertShape([
44339
44862
  ...TARGET_FLAGS,
44340
44863
  "workspace-id",
44341
44864
  ...CREDENTIAL_FLAGS,
44342
44865
  "attach",
44866
+ "broadcast-to-channel",
44867
+ "thread",
44343
44868
  "until",
44344
44869
  "json"
44345
44870
  ], 3);
44871
+ const inThread = args.has("thread");
44872
+ const broadcastToChannel = args.has("broadcast-to-channel");
44873
+ if (broadcastToChannel && !inThread) {
44874
+ throw new UsageError(
44875
+ "--broadcast-to-channel sends a thread reply to its channel as well, so it needs --thread"
44876
+ );
44877
+ }
44346
44878
  const signalId = args.positionals[1];
44347
44879
  if (signalId === void 0 || !UUID_RE23.test(signalId)) {
44348
44880
  throw new Error("reply requires the signal UUID being answered");
@@ -44368,24 +44900,30 @@ async function runReply(args) {
44368
44900
  body: signalText(body, "body"),
44369
44901
  to_user_id: null,
44370
44902
  to_agent_principal_id: null,
44371
- in_reply_to: signalId.toLowerCase(),
44903
+ in_reply_to: inThread ? null : signalId.toLowerCase(),
44372
44904
  about: null,
44373
44905
  ...attachments.length === 0 ? {} : { attachments },
44374
- ...untilMs2 === void 0 ? {} : { until_ms: untilMs2 }
44906
+ ...untilMs2 === void 0 ? {} : { until_ms: untilMs2 },
44907
+ ...inThread ? { thread_root_id: signalId.toLowerCase() } : {},
44908
+ ...broadcastToChannel ? { broadcast_to_channel: true } : {}
44375
44909
  };
44376
44910
  let result;
44377
44911
  try {
44378
44912
  result = await postSignalCommand(cloud, credential, command2);
44379
44913
  } catch (error) {
44380
- const hint = replyRefusalHint(error);
44914
+ const hint = inThread ? null : replyRefusalHint(error);
44381
44915
  if (hint !== null) throw new Error(hint);
44382
44916
  throw error;
44383
44917
  }
44384
44918
  const signal = result.response.signal;
44919
+ const replyMessage = threadReplyMessage(signal, {
44920
+ inThread,
44921
+ broadcastToChannel
44922
+ });
44385
44923
  if (args.has("json")) {
44386
44924
  printJson({
44387
44925
  status: result.response.status,
44388
- message: "Reply shared. It is immutable, tenancy-scoped, and will quietly expire at its horizon.",
44926
+ message: inThread ? replyMessage : "Reply shared. It is immutable, tenancy-scoped, and will quietly expire at its horizon.",
44389
44927
  signal,
44390
44928
  retried: result.retried,
44391
44929
  attempts: result.attempts
@@ -44400,7 +44938,7 @@ async function runReply(args) {
44400
44938
  )
44401
44939
  );
44402
44940
  process.stdout.write(
44403
- `Reply shared. It is immutable and addressed to the original author.
44941
+ `${replyMessage}
44404
44942
  ${renderSignals([signal], {
44405
44943
  inbox: false,
44406
44944
  includeStale: true,
@@ -44682,6 +45220,7 @@ async function runSignalRead(args, inbox) {
44682
45220
  "workspace-id",
44683
45221
  ...CREDENTIAL_FLAGS,
44684
45222
  "about",
45223
+ "channel",
44685
45224
  "kind",
44686
45225
  ...inbox ? ["wait", "follow", "ndjson", "notify"] : [],
44687
45226
  "since",
@@ -44697,6 +45236,9 @@ async function runSignalRead(args, inbox) {
44697
45236
  if (!args.has("ndjson")) {
44698
45237
  throw new Error("inbox --follow requires --ndjson");
44699
45238
  }
45239
+ if (args.has("channel")) {
45240
+ throw new Error("inbox --follow cannot be combined with --channel");
45241
+ }
44700
45242
  if (args.optional("wait") !== void 0) {
44701
45243
  throw new Error("inbox --follow cannot be combined with --wait");
44702
45244
  }
@@ -44709,15 +45251,29 @@ async function runSignalRead(args, inbox) {
44709
45251
  if (inbox && args.has("ndjson")) {
44710
45252
  throw new Error("inbox --ndjson requires --follow");
44711
45253
  }
45254
+ const channelSlug = channelOption(args);
44712
45255
  const waitSeconds = inbox && args.optional("wait") !== void 0 ? parseWaitSeconds(args.required("wait")) : void 0;
44713
45256
  const cloud = await target(args);
44714
45257
  const selected = await commandWorkspaceAndCredential(args, cloud, {
44715
45258
  validateHumanWorkspace: true
44716
45259
  });
44717
45260
  const credential = signalCredentialOf(selected);
45261
+ let channelId;
45262
+ if (channelSlug !== void 0 && selected.kind === "human") {
45263
+ const rows4 = await listChannelsAsHuman(
45264
+ cloud,
45265
+ selected.human.accessToken,
45266
+ selected.selectedWorkspace
45267
+ );
45268
+ const match = findChannelBySlug(rows4, channelSlug);
45269
+ if (match === null) throw new Error(unknownChannelMessage(channelSlug, rows4));
45270
+ channelId = match.channel_id;
45271
+ }
44718
45272
  const queryBase = {
44719
45273
  workspaceId: selected.selectedWorkspace,
44720
45274
  inbox,
45275
+ ...channelSlug === void 0 || selected.kind !== "agent" ? {} : { channel: channelSlug },
45276
+ ...channelId === void 0 ? {} : { channelId },
44721
45277
  ...args.optional("about") === void 0 ? {} : { about: signalText(args.required("about"), "about") },
44722
45278
  ...args.optional("kind") === void 0 ? {} : { kind: signalKind(args.required("kind")) },
44723
45279
  ...args.optional("since") === void 0 ? {} : { since: args.required("since") },
@@ -44727,17 +45283,23 @@ async function runSignalRead(args, inbox) {
44727
45283
  let rows3;
44728
45284
  let timedOut = false;
44729
45285
  let waited = false;
44730
- if (waitSeconds === void 0) {
44731
- rows3 = await readSignals(cloud, credential, queryBase);
44732
- } else {
44733
- waited = true;
44734
- const deadlineMs = waitDeadlineMs(waitSeconds);
44735
- const waitResult = await pollForSignals({
44736
- deadlineMs,
44737
- read: () => readSignals(cloud, credential, queryBase, { deadlineMs })
44738
- });
44739
- rows3 = waitResult.signals;
44740
- timedOut = waitResult.timedOut;
45286
+ try {
45287
+ if (waitSeconds === void 0) {
45288
+ rows3 = await readSignals(cloud, credential, queryBase);
45289
+ } else {
45290
+ waited = true;
45291
+ const deadlineMs = waitDeadlineMs(waitSeconds);
45292
+ const waitResult = await pollForSignals({
45293
+ deadlineMs,
45294
+ read: () => readSignals(cloud, credential, queryBase, { deadlineMs })
45295
+ });
45296
+ rows3 = waitResult.signals;
45297
+ timedOut = waitResult.timedOut;
45298
+ }
45299
+ } catch (error) {
45300
+ const named = channelSlug === void 0 ? null : unknownChannelReadMessage(error, channelSlug);
45301
+ if (named !== null) throw new Error(named);
45302
+ throw error;
44741
45303
  }
44742
45304
  if (args.has("json")) {
44743
45305
  printJson(
@@ -44770,6 +45332,12 @@ async function runSignalRead(args, inbox) {
44770
45332
  );
44771
45333
  return;
44772
45334
  }
45335
+ if (channelSlug !== void 0) {
45336
+ process.stdout.write(
45337
+ `${inbox ? "Inbox" : "Feed"}, filed in ${channelSlug}:
45338
+ `
45339
+ );
45340
+ }
44773
45341
  process.stdout.write(`${renderSignals(rows3, {
44774
45342
  inbox,
44775
45343
  includeStale: args.has("include-stale"),
@@ -45391,6 +45959,12 @@ function listenerStatusJson(status, permissionMode, evidence = {
45391
45959
  lastAckOutcome: status.lastAckOutcome ?? null,
45392
45960
  consecutiveAckFailureCount: status.consecutiveAckFailureCount ?? null,
45393
45961
  lastAckSignalId: status.lastAckSignalId ?? null,
45962
+ currentDeliverySignalId: status.currentDeliverySignalId ?? null,
45963
+ currentDeliverySince: status.currentDeliverySince ?? null,
45964
+ currentDeliveryElapsedMs: status.currentDeliverySince ? Math.max(0, nowMs - Date.parse(status.currentDeliverySince)) : null,
45965
+ pendingDeliveryCountAt: status.pendingDeliveryCountAt ?? null,
45966
+ heldBackDeliveries: status.heldBackDeliveries ?? [],
45967
+ heldBackDeliveryCount: (status.heldBackDeliveries ?? []).length,
45394
45968
  routeMode: status.routeMode ?? "worker",
45395
45969
  deferOverChars: status.deferOverChars ?? null,
45396
45970
  pendingForMainCount: status.pendingForMainCount ?? 0,
@@ -45513,8 +46087,26 @@ function renderListenerStatus(status, evidence = {
45513
46087
  lines.push("Delivery mode has not been reported yet.");
45514
46088
  }
45515
46089
  if (status.pendingDeliveryCount !== null) {
46090
+ const observedAt = status.pendingDeliveryCountAt ?? null;
46091
+ lines.push(
46092
+ `Pending deliveries reported by the service: ${status.pendingDeliveryCount}.` + (observedAt === null ? " When the service reported it was not recorded." : ` The service reported that ${relativeAge(observedAt, nowMs)}.`)
46093
+ );
46094
+ }
46095
+ const currentDeliveryId = status.currentDeliverySignalId ?? null;
46096
+ const currentDeliverySince = status.currentDeliverySince ?? null;
46097
+ if (currentDeliveryId !== null && currentDeliverySince !== null) {
45516
46098
  lines.push(
45517
- `Pending deliveries reported by the service: ${status.pendingDeliveryCount}.`
46099
+ `Working on delivery ${currentDeliveryId}, claimed ${relativeAge(currentDeliverySince, nowMs)}.`
46100
+ );
46101
+ } else {
46102
+ lines.push("No delivery is being worked on right now.");
46103
+ }
46104
+ const heldBack = status.heldBackDeliveries ?? [];
46105
+ const newestHeldBack = heldBack[0];
46106
+ if (newestHeldBack !== void 0) {
46107
+ const others = heldBack.length - 1;
46108
+ lines.push(
46109
+ `Delivery ${newestHeldBack.signalId} was handed back ${relativeAge(newestHeldBack.at, nowMs)} because ${LISTENER_DELIVERY_HOLD_RELEASE_CLAUSES[newestHeldBack.reason]}.` + (others > 0 ? ` This listener is still tracking ${others} other handed-back ${others === 1 ? "delivery" : "deliveries"}.` : "") + ` This listener has not answered it. After the lease ends the service either delivers it again or terminates it. If this repeats, ${LISTENER_DELIVERY_HOLD_RELEASE_REMEDIES[newestHeldBack.reason]}.`
45518
46110
  );
45519
46111
  }
45520
46112
  lines.push(
@@ -46055,6 +46647,9 @@ async function runConfiguredListener(options) {
46055
46647
  },
46056
46648
  routeMode,
46057
46649
  deferOverChars,
46650
+ /* One delivery may hold the seat for one turn budget, not for the
46651
+ whole 15-minute lease. Same lever, so the two cannot drift. */
46652
+ deliveryHoldBudgetMs: turnBudgetMs,
46058
46653
  pendingMainQueue,
46059
46654
  fetcher: httpClient.fetch
46060
46655
  });
@@ -47301,6 +47896,174 @@ async function runFeedback(args) {
47301
47896
  "Feedback recorded for the operators of this deployment. It is stored durably with your workspace and identity attached, and it is read when they review feedback - there is no reply channel, so nothing further will happen in this session.\n"
47302
47897
  );
47303
47898
  }
47899
+ async function channelRows(context) {
47900
+ if (context.selected.kind === "agent") {
47901
+ throw new Error(CHANNEL_LIST_NEEDS_HUMAN_MESSAGE);
47902
+ }
47903
+ const read = async () => await listChannelsAsHuman(
47904
+ context.cloud,
47905
+ context.selected.human.accessToken,
47906
+ context.selected.selectedWorkspace
47907
+ );
47908
+ try {
47909
+ return await read();
47910
+ } catch (error) {
47911
+ if (error instanceof ChannelListError && error.noResponse) return await read();
47912
+ throw error;
47913
+ }
47914
+ }
47915
+ function channelSelectorKind(selector) {
47916
+ if (UUID_RE23.test(selector)) return "id";
47917
+ const problem = channelSelectorProblem(selector);
47918
+ if (problem !== null) throw new Error(problem);
47919
+ return "name";
47920
+ }
47921
+ async function resolveChannelSelector(context, selector, kind) {
47922
+ if (kind === "id") return selector.toLowerCase();
47923
+ if (context.selected.kind === "agent") {
47924
+ throw new Error(CHANNEL_SELECTOR_NEEDS_ID_MESSAGE);
47925
+ }
47926
+ const rows3 = await channelRows(context);
47927
+ const match = findChannelBySlug(rows3, selector);
47928
+ if (match === null) throw new Error(unknownChannelMessage(selector, rows3));
47929
+ return match.channel_id;
47930
+ }
47931
+ async function sendChannelCommand(context, command2) {
47932
+ const client = new ThinCommandClient(context.cloud);
47933
+ const result = await client.sendChannel({
47934
+ workspaceId: context.selected.selectedWorkspace,
47935
+ command: command2,
47936
+ credential: context.selected.bearer
47937
+ });
47938
+ return result.channel;
47939
+ }
47940
+ var CHANNEL_FLAGS = [
47941
+ ...TARGET_FLAGS,
47942
+ "workspace-id",
47943
+ ...CREDENTIAL_FLAGS,
47944
+ "json"
47945
+ ];
47946
+ async function runChannelCreate(args) {
47947
+ const name = args.positionals[2];
47948
+ if (name === void 0) {
47949
+ throw new UsageError("cswarm channel create needs a channel name");
47950
+ }
47951
+ args.assertShape([...CHANNEL_FLAGS, "purpose"], 3);
47952
+ const problem = channelSlugProblem(name);
47953
+ if (problem !== null) throw new Error(problem);
47954
+ const purposeInput = args.optional("purpose");
47955
+ const purpose = purposeInput === void 0 ? void 0 : purposeInput.trim();
47956
+ if (purpose !== void 0 && purpose.length > CHANNEL_PURPOSE_MAX) {
47957
+ throw new Error(
47958
+ `A channel purpose is at most ${CHANNEL_PURPOSE_MAX} characters.`
47959
+ );
47960
+ }
47961
+ const context = await fileContext(args, ["purpose"], 3);
47962
+ const channel = await sendChannelCommand(context, {
47963
+ kind: "channel_create",
47964
+ slug: normalizeChannelSlug(name),
47965
+ ...purpose === void 0 || purpose.length === 0 ? {} : { purpose }
47966
+ });
47967
+ if (args.has("json")) {
47968
+ printJson({ workspace_id: channel.workspace_id, channel });
47969
+ return;
47970
+ }
47971
+ process.stdout.write(
47972
+ `Channel ${channel.slug} created. Everyone in this workspace can read it and post to it; a channel is where a message is filed, not who may see it.
47973
+ Post to it with cswarm note "<text>" --channel ${channel.slug}
47974
+ Read it with cswarm feed --channel ${channel.slug}
47975
+ Its id, which rename and archive take: ${channel.channel_id}
47976
+ `
47977
+ );
47978
+ }
47979
+ async function runChannelLs(args) {
47980
+ args.assertShape([...CHANNEL_FLAGS, "include-archived"], 2);
47981
+ const context = await fileContext(args, ["include-archived"], 2);
47982
+ const rows3 = await channelRows(context);
47983
+ const includeArchived = args.has("include-archived");
47984
+ if (args.has("json")) {
47985
+ printJson({
47986
+ workspace_id: context.selected.selectedWorkspace,
47987
+ channels: includeArchived ? rows3 : rows3.filter((row) => row.archived_at === null)
47988
+ });
47989
+ return;
47990
+ }
47991
+ process.stdout.write(renderChannelList(rows3, { includeArchived }));
47992
+ }
47993
+ async function runChannelRename(args) {
47994
+ const selector = args.positionals[2];
47995
+ const nextName = args.positionals[3];
47996
+ if (selector === void 0 || nextName === void 0) {
47997
+ throw new UsageError(
47998
+ "cswarm channel rename needs the channel and its new name"
47999
+ );
48000
+ }
48001
+ args.assertShape([...CHANNEL_FLAGS], 4);
48002
+ const selectorKind = channelSelectorKind(selector);
48003
+ const problem = channelSlugProblem(nextName);
48004
+ if (problem !== null) throw new Error(problem);
48005
+ const context = await fileContext(args, [], 4);
48006
+ const channelId = await resolveChannelSelector(context, selector, selectorKind);
48007
+ const channel = await sendChannelCommand(context, {
48008
+ kind: "channel_rename",
48009
+ channel_id: channelId,
48010
+ slug: normalizeChannelSlug(nextName)
48011
+ });
48012
+ if (args.has("json")) {
48013
+ printJson({ workspace_id: channel.workspace_id, channel });
48014
+ return;
48015
+ }
48016
+ process.stdout.write(
48017
+ `Channel renamed to ${channel.slug}. Every message already filed in it is unchanged and its id has not moved.
48018
+ Post to it with cswarm note "<text>" --channel ${channel.slug}
48019
+ Its id: ${channel.channel_id}
48020
+ `
48021
+ );
48022
+ }
48023
+ async function runChannelArchive(args) {
48024
+ const selector = args.positionals[2];
48025
+ if (selector === void 0) {
48026
+ throw new UsageError("cswarm channel archive needs the channel");
48027
+ }
48028
+ args.assertShape([...CHANNEL_FLAGS], 3);
48029
+ const selectorKind = channelSelectorKind(selector);
48030
+ const context = await fileContext(args, [], 3);
48031
+ const channelId = await resolveChannelSelector(context, selector, selectorKind);
48032
+ const channel = await sendChannelCommand(context, {
48033
+ kind: "channel_archive",
48034
+ channel_id: channelId
48035
+ });
48036
+ if (args.has("json")) {
48037
+ printJson({ workspace_id: channel.workspace_id, channel });
48038
+ return;
48039
+ }
48040
+ process.stdout.write(
48041
+ `Channel ${channel.slug} is archived. It keeps its messages and its links, and it takes no new ones. Archiving it again changes nothing.
48042
+ See it with cswarm channel ls --include-archived
48043
+ Read what is in it with cswarm feed --channel ${channel.slug}
48044
+ `
48045
+ );
48046
+ }
48047
+ var CHANNEL_SUBCOMMANDS = {
48048
+ create: runChannelCreate,
48049
+ ls: runChannelLs,
48050
+ rename: runChannelRename,
48051
+ archive: runChannelArchive
48052
+ };
48053
+ var CHANNEL_SUBCOMMAND_NAMES = Object.keys(
48054
+ CHANNEL_SUBCOMMANDS
48055
+ );
48056
+ async function runChannel(args) {
48057
+ const action = args.positionals[1];
48058
+ const chosen = action === void 0 ? void 0 : CHANNEL_SUBCOMMANDS[action];
48059
+ if (chosen === void 0) {
48060
+ const names = Object.keys(CHANNEL_SUBCOMMANDS);
48061
+ throw new UsageError(
48062
+ `cswarm channel takes ${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`
48063
+ );
48064
+ }
48065
+ return await chosen(args);
48066
+ }
47304
48067
  async function runFile(args) {
47305
48068
  const action = args.positionals[1];
47306
48069
  if (action === "put") return await runFilePut(args);
@@ -47576,6 +48339,10 @@ async function main() {
47576
48339
  await runFeedback(args);
47577
48340
  return;
47578
48341
  }
48342
+ if (verb === "channel") {
48343
+ await runChannel(args);
48344
+ return;
48345
+ }
47579
48346
  if (verb === "file") {
47580
48347
  await runFile(args);
47581
48348
  return;
@@ -47717,6 +48484,7 @@ ${usage()}
47717
48484
  });
47718
48485
  // Annotate the CommonJS export names for ESM import in node:
47719
48486
  0 && (module.exports = {
48487
+ CHANNEL_SUBCOMMAND_NAMES,
47720
48488
  EXIT_RESTARTABLE,
47721
48489
  ListenerUnattendedRefusedError,
47722
48490
  TURN_BUDGET_CREDENTIAL_MARGIN_MS,
@@ -47734,5 +48502,7 @@ ${usage()}
47734
48502
  replyRefusalHint,
47735
48503
  resolveDetachedClaudeExecutable,
47736
48504
  resolveDetachedCodexExecutable,
47737
- resolveTurnBudgetOrDefer
48505
+ resolveTurnBudgetOrDefer,
48506
+ threadReplyMessage,
48507
+ usage
47738
48508
  });