commonswarm 0.1.21 → 0.1.22

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 +438 -32
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -13503,6 +13503,8 @@ var require_main3 = __commonJS({
13503
13503
  var cli_exports = {};
13504
13504
  __export(cli_exports, {
13505
13505
  EXIT_RESTARTABLE: () => EXIT_RESTARTABLE,
13506
+ TURN_BUDGET_CREDENTIAL_MARGIN_MS: () => TURN_BUDGET_CREDENTIAL_MARGIN_MS,
13507
+ clampTurnBudgetToCredential: () => clampTurnBudgetToCredential,
13506
13508
  describeAudience: () => describeAudience,
13507
13509
  listenerFailureMessage: () => listenerFailureMessage,
13508
13510
  listenerHostLimits: () => listenerHostLimits,
@@ -13510,7 +13512,8 @@ __export(cli_exports, {
13510
13512
  listenerStatusJson: () => listenerStatusJson,
13511
13513
  renderRoster: () => renderRoster,
13512
13514
  resolveDetachedClaudeExecutable: () => resolveDetachedClaudeExecutable,
13513
- resolveDetachedCodexExecutable: () => resolveDetachedCodexExecutable
13515
+ resolveDetachedCodexExecutable: () => resolveDetachedCodexExecutable,
13516
+ resolveTurnBudgetOrDefer: () => resolveTurnBudgetOrDefer
13514
13517
  });
13515
13518
  module.exports = __toCommonJS(cli_exports);
13516
13519
  var import_node_crypto19 = require("node:crypto");
@@ -23058,6 +23061,67 @@ async function listFilesAsHuman(target2, accessToken, workspaceId2, fetcher = fe
23058
23061
  return body;
23059
23062
  }
23060
23063
 
23064
+ // src/cloud/feedback.ts
23065
+ var FeedbackTransportError = class extends Error {
23066
+ name = "FeedbackTransportError";
23067
+ };
23068
+ var FeedbackRefusedError = class extends Error {
23069
+ constructor(code, message) {
23070
+ super(message);
23071
+ this.code = code;
23072
+ }
23073
+ code;
23074
+ name = "FeedbackRefusedError";
23075
+ };
23076
+ var REQUEST_TIMEOUT_MS2 = 3e4;
23077
+ async function submitFeedback(options, request) {
23078
+ const fetcher = options.fetcher ?? fetch;
23079
+ const controller = new AbortController();
23080
+ const timer2 = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
23081
+ let response;
23082
+ try {
23083
+ response = await fetcher(commandEndpoint(options.target), {
23084
+ method: "POST",
23085
+ headers: {
23086
+ authorization: `Bearer ${options.credential}`,
23087
+ apikey: options.target.anonKey,
23088
+ "content-type": "application/json"
23089
+ },
23090
+ body: JSON.stringify({
23091
+ command_id: newCommandId(),
23092
+ client_version: "0.1.0",
23093
+ workspace_id: options.workspaceId,
23094
+ stream: { kind: "workspace" },
23095
+ command: {
23096
+ kind: "submit_feedback",
23097
+ feedback_id: crypto.randomUUID(),
23098
+ category: request.category,
23099
+ body: request.body,
23100
+ context: request.context ?? null
23101
+ }
23102
+ }),
23103
+ signal: controller.signal
23104
+ });
23105
+ } catch (error) {
23106
+ if (error.name === "AbortError") {
23107
+ throw new FeedbackTransportError("feedback submission timed out");
23108
+ }
23109
+ throw new FeedbackTransportError("feedback submission failed before a response");
23110
+ } finally {
23111
+ clearTimeout(timer2);
23112
+ }
23113
+ const body = await response.json().catch(() => null);
23114
+ if (!response.ok) {
23115
+ const code = typeof body?.error === "string" ? body.error : "http_error";
23116
+ const message = typeof body?.message === "string" ? body.message : `feedback submission was refused (HTTP ${response.status})`;
23117
+ throw new FeedbackRefusedError(code, message);
23118
+ }
23119
+ if (body === null || typeof body.status !== "string") {
23120
+ throw new FeedbackTransportError("the deployment answered without a readable result");
23121
+ }
23122
+ return body;
23123
+ }
23124
+
23061
23125
  // src/cloud/current-target.ts
23062
23126
  var import_node_crypto6 = require("node:crypto");
23063
23127
  var import_promises3 = require("node:fs/promises");
@@ -29373,6 +29437,59 @@ async function runInboxFollow(options) {
29373
29437
  // src/host/opencode.ts
29374
29438
  var import_node_child_process3 = require("node:child_process");
29375
29439
  var import_node_crypto12 = require("node:crypto");
29440
+
29441
+ // src/host/stderr-tail.ts
29442
+ var RING_CAPACITY_BYTES = 4096;
29443
+ var TAIL_MAX_CHARS = 2048;
29444
+ var EXOTIC_SEPARATORS = "\\u00a0\\u1680\\u2000-\\u200d\\u2028\\u2029\\u202a-\\u202e\\u2060\\u2066-\\u2069\\u202f\\u205f\\u3000\\ufeff";
29445
+ var SEPARATOR_CLASS_SOURCE = "\\t\\n\\x0b\\f\\r " + EXOTIC_SEPARATORS;
29446
+ var ANSI_ESCAPE_GLOBAL_RE2 = new RegExp("\\u001b\\[[0-?]*[ -\\/]*[@-~]", "g");
29447
+ var CONTROL_AND_SEPARATOR_STRIP_RE = new RegExp(
29448
+ "[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f" + EXOTIC_SEPARATORS + "]",
29449
+ "g"
29450
+ );
29451
+ var CREDENTIAL_PREFIX_RE = new RegExp(
29452
+ `swm_(?:agt|inv|cap)_[^${SEPARATOR_CLASS_SOURCE}]*`,
29453
+ "gi"
29454
+ );
29455
+ function sanitizeStderrTail(raw) {
29456
+ return raw.replace(ANSI_ESCAPE_GLOBAL_RE2, "").replace(CONTROL_AND_SEPARATOR_STRIP_RE, "").replace(CREDENTIAL_PREFIX_RE, "[redacted-credential]").slice(-TAIL_MAX_CHARS).trim();
29457
+ }
29458
+ function attachStderrTailRing(stderr) {
29459
+ const chunks = [];
29460
+ let total = 0;
29461
+ let evicted = false;
29462
+ stderr.on("data", (chunk) => {
29463
+ const buffer2 = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
29464
+ chunks.push(buffer2);
29465
+ total += buffer2.length;
29466
+ while (total > RING_CAPACITY_BYTES && chunks.length > 0) {
29467
+ evicted = true;
29468
+ const head2 = chunks[0];
29469
+ const excess = total - RING_CAPACITY_BYTES;
29470
+ if (head2.length <= excess) {
29471
+ chunks.shift();
29472
+ total -= head2.length;
29473
+ } else {
29474
+ chunks[0] = head2.subarray(excess);
29475
+ total -= excess;
29476
+ }
29477
+ }
29478
+ });
29479
+ stderr.resume();
29480
+ return {
29481
+ read() {
29482
+ let text = Buffer.concat(chunks).toString("utf8");
29483
+ if (evicted) {
29484
+ const newline = text.indexOf("\n");
29485
+ text = newline === -1 ? "" : text.slice(newline + 1);
29486
+ }
29487
+ return sanitizeStderrTail(text);
29488
+ }
29489
+ };
29490
+ }
29491
+
29492
+ // src/host/opencode.ts
29376
29493
  var import_node_fs3 = require("node:fs");
29377
29494
  var import_promises4 = require("node:fs/promises");
29378
29495
  var import_node_os4 = require("node:os");
@@ -31027,8 +31144,18 @@ async function openOpenCodeAcpSession(options) {
31027
31144
  await disposeHome();
31028
31145
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
31029
31146
  }
31030
- child.stderr.on("data", () => void 0);
31031
- child.stderr.resume();
31147
+ const stderrTail = attachStderrTailRing(child.stderr);
31148
+ if (options.onStderrTail) {
31149
+ const deliverTail = options.onStderrTail;
31150
+ let tailDelivered = false;
31151
+ const publishTail = () => {
31152
+ if (tailDelivered) return;
31153
+ tailDelivered = true;
31154
+ deliverTail(stderrTail.read());
31155
+ };
31156
+ child.once("exit", publishTail);
31157
+ child.once("close", publishTail);
31158
+ }
31032
31159
  let sessionRef = null;
31033
31160
  const transport = createBoundTransport({
31034
31161
  readable: child.stdout,
@@ -31310,8 +31437,18 @@ async function openClaudeAcpSession(options) {
31310
31437
  await terminateClaudeChild(child);
31311
31438
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
31312
31439
  }
31313
- child.stderr.on("data", () => void 0);
31314
- child.stderr.resume();
31440
+ const stderrTail = attachStderrTailRing(child.stderr);
31441
+ if (options.onStderrTail) {
31442
+ const deliverTail = options.onStderrTail;
31443
+ let tailDelivered = false;
31444
+ const publishTail = () => {
31445
+ if (tailDelivered) return;
31446
+ tailDelivered = true;
31447
+ deliverTail(stderrTail.read());
31448
+ };
31449
+ child.once("exit", publishTail);
31450
+ child.once("close", publishTail);
31451
+ }
31315
31452
  let sessionRef = null;
31316
31453
  const transport = createBoundTransport({
31317
31454
  readable: child.stdout,
@@ -31586,8 +31723,18 @@ async function openCodexAcpSession(options) {
31586
31723
  await terminateCodexChild(child);
31587
31724
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
31588
31725
  }
31589
- child.stderr.on("data", () => void 0);
31590
- child.stderr.resume();
31726
+ const stderrTail = attachStderrTailRing(child.stderr);
31727
+ if (options.onStderrTail) {
31728
+ const deliverTail = options.onStderrTail;
31729
+ let tailDelivered = false;
31730
+ const publishTail = () => {
31731
+ if (tailDelivered) return;
31732
+ tailDelivered = true;
31733
+ deliverTail(stderrTail.read());
31734
+ };
31735
+ child.once("exit", publishTail);
31736
+ child.once("close", publishTail);
31737
+ }
31591
31738
  let sessionRef = null;
31592
31739
  const transport = createBoundTransport({
31593
31740
  readable: child.stdout,
@@ -31637,6 +31784,19 @@ async function openCodexAcpSession(options) {
31637
31784
  }
31638
31785
  }
31639
31786
 
31787
+ // src/listener/types.ts
31788
+ var LISTENER_PROMPT_TIMEOUT_MS = 6e5;
31789
+ var ListenerRenewalUnavailableError = class extends Error {
31790
+ constructor(message) {
31791
+ super(message);
31792
+ this.name = "renewal_unavailable";
31793
+ }
31794
+ };
31795
+ async function resolveBudgetAndPrompt(session, prompt, budget) {
31796
+ const timeoutMs = typeof budget === "number" ? budget : await budget();
31797
+ return await session.prompt(prompt, { timeoutMs });
31798
+ }
31799
+
31640
31800
  // src/listener/engine.ts
31641
31801
  var UUID_RE8 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
31642
31802
  var TERMINAL_STATES = /* @__PURE__ */ new Set(["done", "expired", "failed"]);
@@ -31751,6 +31911,7 @@ function abortError() {
31751
31911
  }
31752
31912
  function defaultRetryablePromptError(error) {
31753
31913
  if (error instanceof SenderProvenanceUnavailableError) return true;
31914
+ if (error instanceof ListenerRenewalUnavailableError) return true;
31754
31915
  if (error instanceof AcpHostError) return TRANSIENT_ACP_CODES.has(error.code);
31755
31916
  return false;
31756
31917
  }
@@ -32517,8 +32678,18 @@ async function openGrokAcpSession(options) {
32517
32678
  child.kill("SIGKILL");
32518
32679
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
32519
32680
  }
32520
- child.stderr.on("data", () => void 0);
32521
- child.stderr.resume();
32681
+ const stderrTail = attachStderrTailRing(child.stderr);
32682
+ if (options.onStderrTail) {
32683
+ const deliverTail = options.onStderrTail;
32684
+ let tailDelivered = false;
32685
+ const publishTail = () => {
32686
+ if (tailDelivered) return;
32687
+ tailDelivered = true;
32688
+ deliverTail(stderrTail.read());
32689
+ };
32690
+ child.once("exit", publishTail);
32691
+ child.once("close", publishTail);
32692
+ }
32522
32693
  let sessionRef = null;
32523
32694
  const transport = createBoundTransport({
32524
32695
  readable: child.stdout,
@@ -32583,8 +32754,9 @@ var GrokListenerModel = class {
32583
32754
  async prompt(_signal, _mode, prompt) {
32584
32755
  if (this.closed) throw new Error("listener model is closed");
32585
32756
  const worker = await this.ensureWorker();
32757
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
32586
32758
  try {
32587
- return await worker.session.prompt(prompt);
32759
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
32588
32760
  } catch (error) {
32589
32761
  if (error instanceof AcpChildExitError) {
32590
32762
  try {
@@ -32619,6 +32791,7 @@ var GrokListenerModel = class {
32619
32791
  ...this.options.model ? { model: this.options.model } : {},
32620
32792
  ...this.options.effort ? { effort: this.options.effort } : {},
32621
32793
  ...this.options.env ? { env: this.options.env } : {},
32794
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
32622
32795
  clientName: "cswarm-listener"
32623
32796
  });
32624
32797
  try {
@@ -32735,8 +32908,9 @@ var OpenCodeListenerModel = class {
32735
32908
  async prompt(_signal, _mode, prompt) {
32736
32909
  if (this.closed) throw new Error("listener model is closed");
32737
32910
  const worker = await this.ensureWorker();
32911
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
32738
32912
  try {
32739
- return await worker.session.prompt(prompt);
32913
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
32740
32914
  } catch (error) {
32741
32915
  if (error instanceof AcpChildExitError) {
32742
32916
  const home = this.workerHome;
@@ -33026,6 +33200,7 @@ var OpenCodeListenerModel = class {
33026
33200
  ...this.options.model ? { model: this.options.model } : {},
33027
33201
  ...this.options.env ? { env: this.options.env } : {},
33028
33202
  ...this.options.allowMissingAuth === true ? { allowMissingAuth: true } : {},
33203
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
33029
33204
  clientName: "cswarm-listener"
33030
33205
  });
33031
33206
  pending.phase = "opening";
@@ -33150,8 +33325,9 @@ var ClaudeListenerModel = class {
33150
33325
  async prompt(_signal, _mode, prompt) {
33151
33326
  if (this.closed) throw new Error("listener model is closed");
33152
33327
  const worker = await this.ensureWorker();
33328
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
33153
33329
  try {
33154
- return await worker.session.prompt(prompt);
33330
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
33155
33331
  } catch (error) {
33156
33332
  if (error instanceof AcpChildExitError) {
33157
33333
  try {
@@ -33233,6 +33409,7 @@ var ClaudeListenerModel = class {
33233
33409
  ...this.options.executable ? { executable: this.options.executable } : {},
33234
33410
  ...this.options.env ? { env: this.options.env } : {},
33235
33411
  signal: controller.signal,
33412
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
33236
33413
  clientName: "cswarm-listener"
33237
33414
  });
33238
33415
  this.openingHandle = handle;
@@ -33316,8 +33493,9 @@ var CodexListenerModel = class {
33316
33493
  async prompt(_signal, _mode, prompt) {
33317
33494
  if (this.closed) throw new Error("listener model is closed");
33318
33495
  const worker = await this.ensureWorker();
33496
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
33319
33497
  try {
33320
- return await worker.session.prompt(prompt);
33498
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
33321
33499
  } catch (error) {
33322
33500
  if (error instanceof AcpChildExitError) {
33323
33501
  try {
@@ -33399,6 +33577,7 @@ var CodexListenerModel = class {
33399
33577
  ...this.options.executable ? { executable: this.options.executable } : {},
33400
33578
  ...this.options.env ? { env: this.options.env } : {},
33401
33579
  signal: controller.signal,
33580
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
33402
33581
  clientName: "cswarm-listener"
33403
33582
  });
33404
33583
  this.openingHandle = handle;
@@ -35043,6 +35222,7 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
35043
35222
  "stoppedAt",
35044
35223
  "lastSignalId",
35045
35224
  "lastErrorCode",
35225
+ "lastWorkerStderrTail",
35046
35226
  "logPath",
35047
35227
  "deliveryMode",
35048
35228
  "pendingDeliveryCount",
@@ -35097,7 +35277,7 @@ function parseStatus(raw) {
35097
35277
  const nullableUuid2 = (candidate) => candidate === null || typeof candidate === "string" && UUID_RE12.test(candidate);
35098
35278
  const nullableCount = (candidate) => candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
35099
35279
  const nullableTimestamp = (candidate) => candidate === null || typeof candidate === "string" && Number.isFinite(Date.parse(candidate));
35100
- if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE12.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE12.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE12.test(row.principalId) || !Number.isSafeInteger(row.pid) || row.pid < 1 || typeof row.state !== "string" || !["starting", "ready", "stopping", "stopped", "failed"].includes(row.state) || typeof row.startedAt !== "string" || !Number.isFinite(Date.parse(row.startedAt)) || !(row.readyAt === null || typeof row.readyAt === "string" && Number.isFinite(Date.parse(row.readyAt))) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt)) || !(row.stoppedAt === null || typeof row.stoppedAt === "string" && Number.isFinite(Date.parse(row.stoppedAt))) || !nullableUuid2(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || typeof row.logPath !== "string" || !(0, import_node_path14.isAbsolute)(row.logPath) || !(row.deliveryMode === void 0 || row.deliveryMode === null || typeof row.deliveryMode === "string" && STATUS_DELIVERY_MODES.has(row.deliveryMode)) || !(row.pendingDeliveryCount === void 0 || nullableCount(row.pendingDeliveryCount)) || !(row.lastTerminalDeliveryFailureCount === void 0 || nullableCount(row.lastTerminalDeliveryFailureCount)) || !(row.lastTerminalDeliveryFailureAt === void 0 || nullableTimestamp(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp(row.lastAckAt))) {
35280
+ if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE12.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE12.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE12.test(row.principalId) || !Number.isSafeInteger(row.pid) || row.pid < 1 || typeof row.state !== "string" || !["starting", "ready", "stopping", "stopped", "failed"].includes(row.state) || typeof row.startedAt !== "string" || !Number.isFinite(Date.parse(row.startedAt)) || !(row.readyAt === null || typeof row.readyAt === "string" && Number.isFinite(Date.parse(row.readyAt))) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt)) || !(row.stoppedAt === null || typeof row.stoppedAt === "string" && Number.isFinite(Date.parse(row.stoppedAt))) || !nullableUuid2(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || !(row.lastWorkerStderrTail === void 0 || row.lastWorkerStderrTail === null || typeof row.lastWorkerStderrTail === "string" && row.lastWorkerStderrTail.length > 0 && row.lastWorkerStderrTail.length <= 2048 && !/swm_(?:agt|inv|cap)_/i.test(row.lastWorkerStderrTail)) || typeof row.logPath !== "string" || !(0, import_node_path14.isAbsolute)(row.logPath) || !(row.deliveryMode === void 0 || row.deliveryMode === null || typeof row.deliveryMode === "string" && STATUS_DELIVERY_MODES.has(row.deliveryMode)) || !(row.pendingDeliveryCount === void 0 || nullableCount(row.pendingDeliveryCount)) || !(row.lastTerminalDeliveryFailureCount === void 0 || nullableCount(row.lastTerminalDeliveryFailureCount)) || !(row.lastTerminalDeliveryFailureAt === void 0 || nullableTimestamp(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp(row.lastAckAt))) {
35101
35281
  throw new Error("stored listener status is malformed");
35102
35282
  }
35103
35283
  return {
@@ -35107,7 +35287,8 @@ function parseStatus(raw) {
35107
35287
  lastTerminalDeliveryFailureCount: row.lastTerminalDeliveryFailureCount ?? null,
35108
35288
  lastTerminalDeliveryFailureAt: row.lastTerminalDeliveryFailureAt ?? null,
35109
35289
  lastClaimAt: row.lastClaimAt ?? null,
35110
- lastAckAt: row.lastAckAt ?? null
35290
+ lastAckAt: row.lastAckAt ?? null,
35291
+ lastWorkerStderrTail: row.lastWorkerStderrTail ?? null
35111
35292
  };
35112
35293
  }
35113
35294
  async function writeListenerStatus(paths, status) {
@@ -35144,7 +35325,12 @@ async function appendListenerEvent(paths, event) {
35144
35325
  // D-051 companion 2: why a listener is down, and why it stopped trying.
35145
35326
  "restart_attempts",
35146
35327
  "restartable",
35147
- "restarts_exhausted"
35328
+ "restarts_exhausted",
35329
+ // D-090 family: the last lines of a dead worker's stderr, sanitized and
35330
+ // bounded by the supervisor, and the prompt-turn budget behind a timeout.
35331
+ // Local log only — this file never feeds a server payload.
35332
+ "worker_stderr_tail",
35333
+ "turn_budget_ms"
35148
35334
  ]);
35149
35335
  const deliveryModes = /* @__PURE__ */ new Set(["durable_claim", "cursor_fallback"]);
35150
35336
  const deliveryOutcomes = /* @__PURE__ */ new Set([
@@ -35166,7 +35352,16 @@ async function appendListenerEvent(paths, event) {
35166
35352
  if (key2 === "outcome" && !(value === null || typeof value === "string" && deliveryOutcomes.has(value))) {
35167
35353
  throw new Error("listener event outcome is not allowed");
35168
35354
  }
35169
- if (typeof value === "string" && (value.length > 128 || /swm_(?:agt|inv|cap)_/i.test(value))) {
35355
+ if (key2 === "worker_stderr_tail" && !(typeof value === "string" && value.length > 0 && value.length <= 2048)) {
35356
+ throw new Error("listener event stderr tail is not allowed");
35357
+ }
35358
+ if (key2 === "turn_budget_ms" && !(typeof value === "number" && Number.isSafeInteger(value) && value > 0)) {
35359
+ throw new Error("listener event turn budget is not allowed");
35360
+ }
35361
+ if (typeof value === "string" && // worker_stderr_tail is deliberately exempt from the generic 128-char
35362
+ // cap (its own bound is 2048, above); the secret scan still applies to
35363
+ // every string, the tail included.
35364
+ (key2 !== "worker_stderr_tail" && value.length > 128 || /swm_(?:agt|inv|cap)_/i.test(value))) {
35170
35365
  throw new Error("listener event contains unsafe text");
35171
35366
  }
35172
35367
  }
@@ -35443,6 +35638,23 @@ function safeErrorCode(error) {
35443
35638
  const name = error.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
35444
35639
  return name.slice(0, 96) || "listener_error";
35445
35640
  }
35641
+ var TAIL_SERIALIZED_BUDGET_BYTES = 3e3;
35642
+ function fitWorkerStderrTailForLog(tail) {
35643
+ let fitted = tail.trim();
35644
+ for (; ; ) {
35645
+ if (fitted.length === 0) return fitted;
35646
+ const serializedBytes = Buffer.byteLength(JSON.stringify(fitted), "utf8");
35647
+ if (fitted.length <= 2048 && serializedBytes <= TAIL_SERIALIZED_BUDGET_BYTES) {
35648
+ return fitted;
35649
+ }
35650
+ const dropChars = Math.max(
35651
+ fitted.length - 2048,
35652
+ Math.ceil((serializedBytes - TAIL_SERIALIZED_BUDGET_BYTES) / 6),
35653
+ 1
35654
+ );
35655
+ fitted = fitted.slice(dropChars);
35656
+ }
35657
+ }
35446
35658
  async function runListenerSupervisor(options) {
35447
35659
  const now = options.now ?? Date.now;
35448
35660
  const startedAt = iso2(now);
@@ -35464,6 +35676,7 @@ async function runListenerSupervisor(options) {
35464
35676
  stoppedAt: null,
35465
35677
  lastSignalId: null,
35466
35678
  lastErrorCode: null,
35679
+ lastWorkerStderrTail: null,
35467
35680
  deliveryMode: null,
35468
35681
  pendingDeliveryCount: null,
35469
35682
  lastTerminalDeliveryFailureCount: null,
@@ -35518,9 +35731,21 @@ async function runListenerSupervisor(options) {
35518
35731
  instance_id: status.instanceId,
35519
35732
  pid: process.pid
35520
35733
  });
35734
+ const takeWorkerStderrTail = options.takeWorkerStderrTail;
35735
+ const takeTail = () => {
35736
+ if (!takeWorkerStderrTail) return null;
35737
+ const tail = takeWorkerStderrTail();
35738
+ if (typeof tail !== "string") return null;
35739
+ const fitted = fitWorkerStderrTailForLog(tail);
35740
+ return fitted.length > 0 ? fitted : null;
35741
+ };
35521
35742
  const onEvent = (event) => {
35522
35743
  if (event.type === "ready") {
35523
- transition("ready", { readyAt: event.ts, lastErrorCode: null });
35744
+ transition("ready", {
35745
+ readyAt: event.ts,
35746
+ lastErrorCode: null,
35747
+ lastWorkerStderrTail: null
35748
+ });
35524
35749
  log({ ts: event.ts, event: "listener_ready" });
35525
35750
  return;
35526
35751
  }
@@ -35537,7 +35762,15 @@ async function runListenerSupervisor(options) {
35537
35762
  event: "listener_effect",
35538
35763
  signal_id: event.signalId,
35539
35764
  status: event.status,
35540
- failure_code: event.failureCode
35765
+ failure_code: event.failureCode,
35766
+ // Code comparison, not message matching (D-053). The budget rides
35767
+ // only the timeout class so a reader can see what bound was hit — and
35768
+ // it is the CLAMPED budget actually in force, not the configured cap.
35769
+ ...(() => {
35770
+ if (event.failureCode !== "acptimeouterror") return {};
35771
+ const budget = options.getTurnBudgetMs?.();
35772
+ return typeof budget === "number" && budget > 0 ? { turn_budget_ms: budget } : {};
35773
+ })()
35541
35774
  });
35542
35775
  return;
35543
35776
  }
@@ -35667,14 +35900,20 @@ async function runListenerSupervisor(options) {
35667
35900
  restarts += 1;
35668
35901
  const delayMs = nextListenerRestartMs(restarts, policy, restartRandom);
35669
35902
  const restartCode = safeErrorCode(stop.error);
35903
+ const restartStderrTail = takeTail();
35670
35904
  log({
35671
35905
  ts: iso2(now),
35672
35906
  event: "listener_restarting",
35673
35907
  attempt: restarts,
35674
35908
  delay_ms: delayMs,
35675
- failure_code: restartCode
35909
+ failure_code: restartCode,
35910
+ ...restartStderrTail !== null ? { worker_stderr_tail: restartStderrTail } : {}
35911
+ });
35912
+ transition("starting", {
35913
+ readyAt: null,
35914
+ lastErrorCode: restartCode,
35915
+ lastWorkerStderrTail: restartStderrTail
35676
35916
  });
35677
- transition("starting", { readyAt: null, lastErrorCode: restartCode });
35678
35917
  await restartSleep(delayMs, controller.signal);
35679
35918
  if (controller.signal.aborted) {
35680
35919
  stop = { reason: "cancelled" };
@@ -35685,14 +35924,17 @@ async function runListenerSupervisor(options) {
35685
35924
  if (stop.reason === "cancelled") {
35686
35925
  transition("stopped", {
35687
35926
  stoppedAt,
35688
- lastErrorCode: null
35927
+ lastErrorCode: null,
35928
+ lastWorkerStderrTail: null
35689
35929
  });
35690
35930
  log({ ts: stoppedAt, event: "listener_stopped" });
35691
35931
  } else {
35692
35932
  const code = stop.reason === "credential" ? "credential_stopped" : safeErrorCode(stop.error);
35933
+ const failedStderrTail = takeTail();
35693
35934
  transition("failed", {
35694
35935
  stoppedAt,
35695
- lastErrorCode: code
35936
+ lastErrorCode: code,
35937
+ lastWorkerStderrTail: failedStderrTail
35696
35938
  });
35697
35939
  log({
35698
35940
  ts: stoppedAt,
@@ -35700,7 +35942,8 @@ async function runListenerSupervisor(options) {
35700
35942
  failure_code: code,
35701
35943
  restart_attempts: restarts,
35702
35944
  restartable: eligible,
35703
- restarts_exhausted: exhausted
35945
+ restarts_exhausted: exhausted,
35946
+ ...failedStderrTail !== null ? { worker_stderr_tail: failedStderrTail } : {}
35704
35947
  });
35705
35948
  }
35706
35949
  } catch (error) {
@@ -35708,11 +35951,17 @@ async function runListenerSupervisor(options) {
35708
35951
  const code = safeErrorCode(
35709
35952
  error instanceof Error ? error : new Error(String(error))
35710
35953
  );
35711
- transition("failed", { stoppedAt, lastErrorCode: code });
35954
+ const failedStderrTail = takeTail();
35955
+ transition("failed", {
35956
+ stoppedAt,
35957
+ lastErrorCode: code,
35958
+ lastWorkerStderrTail: failedStderrTail
35959
+ });
35712
35960
  log({
35713
35961
  ts: stoppedAt,
35714
35962
  event: "listener_failed",
35715
- failure_code: code
35963
+ failure_code: code,
35964
+ ...failedStderrTail !== null ? { worker_stderr_tail: failedStderrTail } : {}
35716
35965
  });
35717
35966
  } finally {
35718
35967
  await writes.catch(() => void 0);
@@ -36564,7 +36813,8 @@ function buildListenerChildArgs(spec) {
36564
36813
  ...provider === "claude" && claudeExe ? ["--claude-executable", claudeExe] : [],
36565
36814
  ...provider === "codex" && codexExe ? ["--codex-executable", codexExe] : [],
36566
36815
  ...spec.model ? ["--model", spec.model] : [],
36567
- ...provider === "grok" && spec.effort ? ["--effort", spec.effort] : []
36816
+ ...provider === "grok" && spec.effort ? ["--effort", spec.effort] : [],
36817
+ ...spec.turnBudget ? ["--turn-budget", spec.turnBudget] : []
36568
36818
  ];
36569
36819
  }
36570
36820
  async function spawnDetachedListener(options) {
@@ -36649,6 +36899,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
36649
36899
  "to",
36650
36900
  "token-id",
36651
36901
  "ttl-ms",
36902
+ "turn-budget",
36652
36903
  "uid",
36653
36904
  "until",
36654
36905
  "url",
@@ -36682,8 +36933,8 @@ var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
36682
36933
  AGENT_CREDENTIAL_MESSAGE_D088
36683
36934
  ];
36684
36935
  function packageVersion() {
36685
- if ("0.1.21".length > 0) {
36686
- return "0.1.21";
36936
+ if ("0.1.22".length > 0) {
36937
+ return "0.1.22";
36687
36938
  }
36688
36939
  try {
36689
36940
  const value = JSON.parse(
@@ -36810,7 +37061,8 @@ Usage:
36810
37061
  cswarm file get <name|file-id> [--version <n>] [--out <local-path>] [--force] [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
36811
37062
  cswarm file rm <name|file-id> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
36812
37063
  cswarm file restore <name|file-id> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
36813
- cswarm listen start --agent-token-stdin [--url <url> --anon-key <key>] --workspace-id <uuid> --provider grok|opencode|claude|codex [--cwd <absolute-path>] [--model <model>] [--effort <level>] [--permissions deny|allow] [--grok-executable <path>] [--opencode-executable <path>] [--claude-executable <path>] [--codex-executable <path>] [--foreground] [--json]
37064
+ cswarm feedback "<text>" --kind bug|idea|friction [--about <ref>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
37065
+ cswarm listen start --agent-token-stdin [--url <url> --anon-key <key>] --workspace-id <uuid> --provider grok|opencode|claude|codex [--cwd <absolute-path>] [--model <model>] [--effort <level>] [--permissions deny|allow] [--grok-executable <path>] [--opencode-executable <path>] [--claude-executable <path>] [--codex-executable <path>] [--turn-budget <duration>] [--foreground] [--json]
36814
37066
  cswarm listen status [--url <url> --anon-key <key>] --workspace-id <uuid> --principal-id <uuid> [--json]
36815
37067
  cswarm listen stop [--url <url> --anon-key <key>] --workspace-id <uuid> --principal-id <uuid> [--json]
36816
37068
  cswarm new "<workspace name>" [--url <url> --anon-key <key>] [--json]
@@ -36845,14 +37097,29 @@ Credential selection for command/dogfood:
36845
37097
  members reads only -- either form
36846
37098
  file put, file ls, file get, file rm, file restore
36847
37099
  read and command, nothing persisted -- either form
37100
+ feedback command only, nothing persisted -- either form
36848
37101
  listen start persists durable state, rotates -- needs expires_at
36849
37102
  token revoke names what it revokes -- needs token_id
36850
37103
 
37104
+ Found a bug or missing feature in cswarm itself? cswarm feedback sends it to the
37105
+ deployment's operators \u2014 agents are encouraged to report friction they hit.
37106
+
36851
37107
  Signals (intention sharing) accept the same credential selection. Agent mode
36852
37108
  never opens a browser or infers a human's saved workspace. Durations use a whole
36853
37109
  number plus m, h, or d (for example 90m, 24h, or 7d) and are capped at 30d.
36854
37110
  Place -- before signal text that itself begins with -- to stop option parsing.
36855
37111
 
37112
+ listen start --turn-budget bounds ONE worker prompt turn (default 10m): how long
37113
+ the worker may think and use tools on a single message before the turn times out
37114
+ and durable delivery retries it. A whole number plus s, m, or h (for example
37115
+ 90s, 5m, 1h), at least 30s and at most 60m. Each turn is additionally clamped
37116
+ to the live credential's remaining lifetime minus 60s, after renewing it when
37117
+ due \u2014 a turn never outlives its credential. Right after a rotation the full
37118
+ budget is available up to the token TTL minus 60s (about 59m on the default 1h
37119
+ TTL); a turn that lands just before a rotation can be clamped to the ~5m
37120
+ renewal lead, and if it times out there, durable delivery retries it on the
37121
+ fresh credential.
37122
+
36856
37123
  Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, and new require a
36857
37124
  stored human login. Agent self-surrender of a token uses --agent-token-stdin and never takes the secret on argv. Invite-link accept signs in when needed, then accepts and
36858
37125
  registers one principal. Invitation links, agent credentials, and capability links
@@ -38208,6 +38475,38 @@ function signalDuration(value) {
38208
38475
  }
38209
38476
  return milliseconds;
38210
38477
  }
38478
+ function listenerTurnBudgetMs(value) {
38479
+ if (value === void 0) return LISTENER_PROMPT_TIMEOUT_MS;
38480
+ const match = /^([1-9]\d*)(s|m|h)$/.exec(value);
38481
+ if (!match) {
38482
+ throw new Error("--turn-budget must be a duration such as 90s, 5m, or 1h");
38483
+ }
38484
+ const unit = match[2] === "s" ? 1e3 : match[2] === "m" ? 6e4 : 36e5;
38485
+ const milliseconds = Number(match[1]) * unit;
38486
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 3e4 || milliseconds > 36e5) {
38487
+ throw new Error("--turn-budget must be between 30s and 60m");
38488
+ }
38489
+ return milliseconds;
38490
+ }
38491
+ var TURN_BUDGET_CREDENTIAL_MARGIN_MS = 6e4;
38492
+ function clampTurnBudgetToCredential(budgetMs, credentialExpiresAt, nowMs) {
38493
+ if (credentialExpiresAt === null) return budgetMs;
38494
+ const horizonMs = credentialExpiresAt - nowMs - TURN_BUDGET_CREDENTIAL_MARGIN_MS;
38495
+ return Math.max(1e3, Math.min(budgetMs, horizonMs));
38496
+ }
38497
+ function resolveTurnBudgetOrDefer(configuredBudgetMs, credentialExpiresAt, nowMs, renewalFailed) {
38498
+ if (renewalFailed) {
38499
+ throw new ListenerRenewalUnavailableError(
38500
+ "the worker credential could not be renewed before this turn; deferring the ask for durable redelivery"
38501
+ );
38502
+ }
38503
+ if (credentialExpiresAt !== null && credentialExpiresAt - nowMs <= TURN_BUDGET_CREDENTIAL_MARGIN_MS) {
38504
+ throw new ListenerRenewalUnavailableError(
38505
+ "the live worker credential is inside its rotation margin and was not renewed; deferring the ask for durable redelivery"
38506
+ );
38507
+ }
38508
+ return clampTurnBudgetToCredential(configuredBudgetMs, credentialExpiresAt, nowMs);
38509
+ }
38211
38510
  function signalText(value, label) {
38212
38511
  const maximum = label === "body" ? 2e3 : 500;
38213
38512
  if (value.length < (label === "body" ? 1 : 0) || value.length > maximum) {
@@ -38990,6 +39289,13 @@ function renderListenerStatus(status) {
38990
39289
  status.lastSignalId ? `Last handled signal: ${status.lastSignalId}.` : "No signal has been handled yet.",
38991
39290
  status.lastErrorCode ? `Last status code: ${status.lastErrorCode}.` : "No listener error is recorded."
38992
39291
  ];
39292
+ if (status.lastWorkerStderrTail) {
39293
+ const tailLines = status.lastWorkerStderrTail.split("\n").filter((line) => line.trim().length > 0);
39294
+ lines.push("Worker stderr (local log only):");
39295
+ for (const line of tailLines.slice(-3)) {
39296
+ lines.push(` ${line}`);
39297
+ }
39298
+ }
38993
39299
  if (status.deliveryMode === "durable_claim") {
38994
39300
  lines.push("Delivery mode: durable claim and acknowledgement.");
38995
39301
  } else if (status.deliveryMode === "cursor_fallback") {
@@ -39169,22 +39475,58 @@ async function runConfiguredListener(options) {
39169
39475
  principalId: options.principalId,
39170
39476
  ...options.stateDirectory ? { stateDirectory: options.stateDirectory } : {}
39171
39477
  });
39478
+ const turnBudgetMs = options.turnBudgetMs ?? LISTENER_PROMPT_TIMEOUT_MS;
39479
+ let lastAppliedTurnBudgetMs = null;
39480
+ const resolveTurnBudgetMs = async () => {
39481
+ let renewalFailed = false;
39482
+ try {
39483
+ await credentialSession.bearer();
39484
+ } catch {
39485
+ renewalFailed = true;
39486
+ }
39487
+ const applied = resolveTurnBudgetOrDefer(
39488
+ turnBudgetMs,
39489
+ credentialSession.expiry,
39490
+ Date.now(),
39491
+ renewalFailed
39492
+ );
39493
+ lastAppliedTurnBudgetMs = applied;
39494
+ return applied;
39495
+ };
39496
+ let lastWorkerStderrTail = null;
39497
+ let workerStderrGeneration = 0;
39498
+ const newWorkerStderrTailSink = () => {
39499
+ const generation = ++workerStderrGeneration;
39500
+ lastWorkerStderrTail = null;
39501
+ return (tail) => {
39502
+ if (generation !== workerStderrGeneration) return;
39503
+ lastWorkerStderrTail = tail.length > 0 ? tail : null;
39504
+ };
39505
+ };
39172
39506
  const newModel = () => options.provider === "opencode" ? new OpenCodeListenerModel({
39173
39507
  cwd: options.cwd,
39174
39508
  permissionMode: options.permissionMode,
39509
+ promptTimeoutMs: resolveTurnBudgetMs,
39510
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39175
39511
  ...options.model ? { model: options.model } : {},
39176
39512
  ...options.opencodeExecutable ? { executable: options.opencodeExecutable } : options.executable ? { executable: options.executable } : {}
39177
39513
  }) : options.provider === "claude" ? new ClaudeListenerModel({
39178
39514
  cwd: options.cwd,
39179
39515
  permissionMode: options.permissionMode,
39516
+ promptTimeoutMs: resolveTurnBudgetMs,
39517
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39180
39518
  ...options.claudeExecutable ? { executable: options.claudeExecutable } : options.executable ? { executable: options.executable } : {}
39181
39519
  }) : options.provider === "codex" ? new CodexListenerModel({
39182
39520
  cwd: options.cwd,
39183
39521
  permissionMode: options.permissionMode,
39522
+ promptTimeoutMs: resolveTurnBudgetMs,
39523
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39184
39524
  ...options.codexExecutable ? { executable: options.codexExecutable } : options.executable ? { executable: options.executable } : {}
39185
39525
  }) : new GrokListenerModel({
39186
39526
  cwd: options.cwd,
39187
39527
  permissionMode: options.permissionMode,
39528
+ promptTimeoutMs: resolveTurnBudgetMs,
39529
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39188
39530
  ...options.model ? { model: options.model } : {},
39189
39531
  ...options.effort ? { effort: options.effort } : {},
39190
39532
  ...options.executable ? { executable: options.executable } : {}
@@ -39204,6 +39546,14 @@ async function runConfiguredListener(options) {
39204
39546
  principalId: options.principalId,
39205
39547
  provider: options.provider,
39206
39548
  permissionMode: options.permissionMode,
39549
+ // The bound a timeout event reports: the last turn's clamped budget when
39550
+ // one has run, else the configured cap.
39551
+ getTurnBudgetMs: () => lastAppliedTurnBudgetMs ?? turnBudgetMs,
39552
+ takeWorkerStderrTail: () => {
39553
+ const tail = lastWorkerStderrTail;
39554
+ lastWorkerStderrTail = null;
39555
+ return tail;
39556
+ },
39207
39557
  prepare: async (proposedInstanceId) => {
39208
39558
  const selected = await openListenerDeliveryJournal({
39209
39559
  profileId: options.cloud.profileId,
@@ -39258,6 +39608,7 @@ async function runListenStart(args) {
39258
39608
  "claude-executable",
39259
39609
  "codex-executable",
39260
39610
  "state-dir",
39611
+ "turn-budget",
39261
39612
  "foreground",
39262
39613
  "json"
39263
39614
  ], 2);
@@ -39268,6 +39619,7 @@ async function runListenStart(args) {
39268
39619
  }
39269
39620
  const provider = listenerProvider(args);
39270
39621
  validateListenerProviderFlags(args, provider);
39622
+ const turnBudgetMs = listenerTurnBudgetMs(args.optional("turn-budget"));
39271
39623
  const cloud = await target(args);
39272
39624
  const workspaceId2 = listenerUuid(
39273
39625
  args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
@@ -39302,6 +39654,7 @@ async function runListenStart(args) {
39302
39654
  cwd,
39303
39655
  permissionMode,
39304
39656
  provider,
39657
+ turnBudgetMs,
39305
39658
  ...args.optional("model") ? { model: args.required("model") } : {},
39306
39659
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39307
39660
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
@@ -39352,6 +39705,7 @@ async function runListenStart(args) {
39352
39705
  ...stateDirectory2 ? { stateDirectory: stateDirectory2 } : {},
39353
39706
  ...args.optional("model") ? { model: args.required("model") } : {},
39354
39707
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39708
+ ...args.optional("turn-budget") ? { turnBudget: args.required("turn-budget") } : {},
39355
39709
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
39356
39710
  ...opencodeExecutable ? { opencodeExecutable } : {},
39357
39711
  ...claudeExecutable ? { claudeExecutable } : {},
@@ -39409,10 +39763,12 @@ async function runListenSupervisor(args) {
39409
39763
  "opencode-executable",
39410
39764
  "claude-executable",
39411
39765
  "codex-executable",
39412
- "state-dir"
39766
+ "state-dir",
39767
+ "turn-budget"
39413
39768
  ], 1);
39414
39769
  const provider = listenerProvider(args);
39415
39770
  validateListenerProviderFlags(args, provider);
39771
+ const turnBudgetMs = listenerTurnBudgetMs(args.optional("turn-budget"));
39416
39772
  const cloud = await target(args);
39417
39773
  const workspaceId2 = listenerUuid(args.optional("workspace-id"), "workspace-id");
39418
39774
  const principalId = listenerUuid(args.optional("principal-id"), "principal-id");
@@ -39428,6 +39784,7 @@ async function runListenSupervisor(args) {
39428
39784
  cwd,
39429
39785
  permissionMode: listenerPermissionMode(args.optional("permissions")),
39430
39786
  provider,
39787
+ turnBudgetMs,
39431
39788
  ...args.optional("model") ? { model: args.required("model") } : {},
39432
39789
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39433
39790
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
@@ -39709,6 +40066,48 @@ async function runFileRestore(args) {
39709
40066
  `
39710
40067
  );
39711
40068
  }
40069
+ async function runFeedback(args) {
40070
+ const body = args.positionals[1];
40071
+ if (!body) {
40072
+ throw new UsageError(
40073
+ 'cswarm feedback needs the feedback text: cswarm feedback "<text>" --kind bug|idea|friction'
40074
+ );
40075
+ }
40076
+ const kind = args.required("kind");
40077
+ if (kind !== "bug" && kind !== "idea" && kind !== "friction") {
40078
+ throw new UsageError("--kind must be bug, idea, or friction");
40079
+ }
40080
+ const about = args.optional("about");
40081
+ const context = await fileContext(args, ["kind", "about"], 2);
40082
+ const submitted = await submitFeedback({
40083
+ target: context.cloud,
40084
+ workspaceId: context.selected.selectedWorkspace,
40085
+ credential: context.selected.bearer
40086
+ }, {
40087
+ category: kind,
40088
+ body,
40089
+ context: {
40090
+ surface: "cli",
40091
+ cswarm_version: CLI_BUILD_VERSION,
40092
+ platform: process.platform,
40093
+ ...about ? { about } : {}
40094
+ }
40095
+ });
40096
+ if (args.has("json")) {
40097
+ process.stdout.write(`${JSON.stringify(submitted, null, 2)}
40098
+ `);
40099
+ return;
40100
+ }
40101
+ if (submitted.duplicate === true) {
40102
+ process.stdout.write(
40103
+ "This matches feedback you sent within the hour, so it was not recorded twice. It is already with the operators of this deployment.\n"
40104
+ );
40105
+ return;
40106
+ }
40107
+ process.stdout.write(
40108
+ "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"
40109
+ );
40110
+ }
39712
40111
  async function runFile(args) {
39713
40112
  const action = args.positionals[1];
39714
40113
  if (action === "put") return await runFilePut(args);
@@ -39964,6 +40363,10 @@ async function main() {
39964
40363
  await runStatus(args);
39965
40364
  return;
39966
40365
  }
40366
+ if (verb === "feedback") {
40367
+ await runFeedback(args);
40368
+ return;
40369
+ }
39967
40370
  if (verb === "file") {
39968
40371
  await runFile(args);
39969
40372
  return;
@@ -40089,6 +40492,8 @@ ${usage()}
40089
40492
  // Annotate the CommonJS export names for ESM import in node:
40090
40493
  0 && (module.exports = {
40091
40494
  EXIT_RESTARTABLE,
40495
+ TURN_BUDGET_CREDENTIAL_MARGIN_MS,
40496
+ clampTurnBudgetToCredential,
40092
40497
  describeAudience,
40093
40498
  listenerFailureMessage,
40094
40499
  listenerHostLimits,
@@ -40096,5 +40501,6 @@ ${usage()}
40096
40501
  listenerStatusJson,
40097
40502
  renderRoster,
40098
40503
  resolveDetachedClaudeExecutable,
40099
- resolveDetachedCodexExecutable
40504
+ resolveDetachedCodexExecutable,
40505
+ resolveTurnBudgetOrDefer
40100
40506
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commonswarm",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "CommonSwarm CLI — coordination for teams where people and AI agents work side by side.",
5
5
  "bin": {
6
6
  "cswarm": "cswarm.cjs"