commonswarm 0.1.21 → 0.1.23

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 +578 -74
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -13503,14 +13503,18 @@ 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,
13509
13511
  listenerPermissionMode: () => listenerPermissionMode,
13510
13512
  listenerStatusJson: () => listenerStatusJson,
13511
13513
  renderRoster: () => renderRoster,
13514
+ replyRefusalHint: () => replyRefusalHint,
13512
13515
  resolveDetachedClaudeExecutable: () => resolveDetachedClaudeExecutable,
13513
- resolveDetachedCodexExecutable: () => resolveDetachedCodexExecutable
13516
+ resolveDetachedCodexExecutable: () => resolveDetachedCodexExecutable,
13517
+ resolveTurnBudgetOrDefer: () => resolveTurnBudgetOrDefer
13514
13518
  });
13515
13519
  module.exports = __toCommonJS(cli_exports);
13516
13520
  var import_node_crypto19 = require("node:crypto");
@@ -22159,15 +22163,8 @@ function createWorkspaceError(status, body) {
22159
22163
  return new CreateWorkspaceError(
22160
22164
  status,
22161
22165
  code,
22162
- /* D-067/D-075. This used to end "Archiving a workspace frees its slot; the CLI cannot archive
22163
- * one yet, so ask whoever operates this deployment." Both halves were dead ends. Archiving
22164
- * is unreachable from every surface — `archived_at` exists and nothing writes it — and on a
22165
- * self-serve deployment the reader IS the operator, so it named a person who does not exist
22166
- * to perform an action that does not exist.
22167
- *
22168
- * It now states the limit, does not offer a remedy that is unimplemented, and names the one
22169
- * route that does work: someone else's invitation, which is not capped. */
22170
- `${limit === null ? "You have already created as many workspaces as this account allows." : `You have already created ${limit} workspaces, which is the limit for one account.`} Workspaces cannot be removed yet, so this limit is a ceiling rather than a queue. Workspaces you were invited to do not count against it \u2014 a collaborator can still add you to theirs.`
22166
+ /* Closing one live workspace now frees a slot; name the exact confirmed command. */
22167
+ `${limit === null ? "You have already created as many workspaces as this account allows." : `You have already created ${limit} workspaces, which is the limit for one account.`} Close one with cswarm workspace close <full-id|exact-name> --confirm <same-selector>, then try again. Workspaces you were invited to do not count against it.`
22171
22168
  );
22172
22169
  }
22173
22170
  if (status === 403) {
@@ -22794,6 +22791,12 @@ var CONTENT_TYPES = /* @__PURE__ */ new Map([
22794
22791
  [".md", "text/markdown"],
22795
22792
  [".txt", "text/plain"],
22796
22793
  [".csv", "text/csv"],
22794
+ // .html/.htm: a web/marketing team's deliverables (Fastio feedback 2026-08-19). Every
22795
+ // download is served Content-Disposition: attachment (§5), never rendered inline, so HTML
22796
+ // is no more dangerous than the .svg already permitted — the spec treats all downloads as
22797
+ // untrusted attachments the consumer must not execute.
22798
+ [".html", "text/html"],
22799
+ [".htm", "text/html"],
22797
22800
  [".json", "application/json"],
22798
22801
  [".yaml", "application/yaml"],
22799
22802
  [".yml", "application/yaml"],
@@ -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");
@@ -27666,6 +27730,7 @@ var DEFAULT_MEMBERSHIP_REVOKED = {
27666
27730
  message: "Your previously selected workspace is no longer available to this account. CommonSwarm cleared that saved selection."
27667
27731
  };
27668
27732
  var PROJECT_NOT_AVAILABLE = "That workspace is not available to this account. Run cswarm workspaces to see workspaces you can select.";
27733
+ var ARCHIVED_PROJECT_NOT_AVAILABLE = "That workspace is closed and cannot be selected. Run cswarm workspaces to see live workspaces you can select.";
27669
27734
  function compareText(left, right) {
27670
27735
  return left < right ? -1 : left > right ? 1 : 0;
27671
27736
  }
@@ -27703,8 +27768,8 @@ var WorkspaceResolutionError = class extends WorkspaceCliError {
27703
27768
  };
27704
27769
  var WorkspaceUnavailableError = class extends WorkspaceCliError {
27705
27770
  code = "project_not_available";
27706
- constructor() {
27707
- super(PROJECT_NOT_AVAILABLE);
27771
+ constructor(message = PROJECT_NOT_AVAILABLE) {
27772
+ super(message);
27708
27773
  this.name = "WorkspaceUnavailableError";
27709
27774
  }
27710
27775
  structured() {
@@ -27808,41 +27873,41 @@ function cloudWorkspaceDirectory(target2, fetcher = fetch) {
27808
27873
  "workspaces",
27809
27874
  {
27810
27875
  select: "workspace_id,name,archived_at",
27876
+ archived_at: "is.null",
27811
27877
  order: "workspace_id.asc"
27812
27878
  },
27813
27879
  fetcher
27814
27880
  )
27815
27881
  ]);
27816
- const names = /* @__PURE__ */ new Map();
27882
+ const roles = /* @__PURE__ */ new Map();
27883
+ for (const row of membershipRows) {
27884
+ const workspaceId2 = checkedUuid(row.workspace_id, "workspace_id");
27885
+ roles.set(workspaceId2, checkedRole(row.role));
27886
+ }
27887
+ const result = [];
27817
27888
  for (const row of workspaceRows) {
27818
27889
  const workspaceId2 = checkedUuid(row.workspace_id, "workspace_id");
27819
27890
  const archivedAt = checkedNullableTimestamp(
27820
27891
  row.archived_at,
27821
27892
  "archived_at"
27822
27893
  );
27823
- names.set(workspaceId2, {
27894
+ if (archivedAt !== null) continue;
27895
+ const role = roles.get(workspaceId2);
27896
+ if (!role) {
27897
+ throw new Error(
27898
+ "workspace read omitted the current user's live membership"
27899
+ );
27900
+ }
27901
+ result.push({
27902
+ workspace_id: workspaceId2,
27824
27903
  name: sanitizeDisplayLabel(
27825
27904
  checkedString(row.name, "workspace name"),
27826
27905
  "Unnamed workspace"
27827
27906
  ),
27828
- archived: archivedAt !== null
27907
+ role,
27908
+ archived: false
27829
27909
  });
27830
27910
  }
27831
- const result = membershipRows.map((row) => {
27832
- const workspaceId2 = checkedUuid(row.workspace_id, "workspace_id");
27833
- const project = names.get(workspaceId2);
27834
- if (!project) {
27835
- throw new Error(
27836
- "workspace read omitted a workspace for a live membership"
27837
- );
27838
- }
27839
- return {
27840
- workspace_id: workspaceId2,
27841
- name: project.name,
27842
- role: checkedRole(row.role),
27843
- archived: project.archived
27844
- };
27845
- });
27846
27911
  return sortWorkspaces(result);
27847
27912
  },
27848
27913
  async status(session, workspaceId2) {
@@ -27897,6 +27962,9 @@ function cloudWorkspaceDirectory(target2, fetcher = fetch) {
27897
27962
  you: userId === session.userId
27898
27963
  };
27899
27964
  });
27965
+ if (!members.some((member) => member.you)) {
27966
+ throw new WorkspaceUnavailableError();
27967
+ }
27900
27968
  const memberNames = new Map(
27901
27969
  members.map((member) => [member.user_id, member.name])
27902
27970
  );
@@ -28022,6 +28090,32 @@ async function clearWorkspaceDefault(store2, userId, expectedWorkspaceId) {
28022
28090
  return true;
28023
28091
  });
28024
28092
  }
28093
+ async function updateWorkspaceDefaultAfterClose(store2, userId, closedWorkspaceId, workspaces) {
28094
+ return await store2.withLock(async () => {
28095
+ const current = await store2.readProfile();
28096
+ if (current.userId !== userId || current.workspaceId !== closedWorkspaceId) {
28097
+ return {
28098
+ closedWasSelected: false,
28099
+ nextWorkspace: null,
28100
+ selectedWorkspaceId: current.userId === userId ? current.workspaceId : null
28101
+ };
28102
+ }
28103
+ const nextWorkspace = sortWorkspaces(workspaces).find(
28104
+ (workspace) => workspace.workspace_id !== closedWorkspaceId && !workspace.archived
28105
+ ) ?? null;
28106
+ await store2.writeProfile({
28107
+ ...current,
28108
+ workspaceId: nextWorkspace?.workspace_id ?? null,
28109
+ principalId: null,
28110
+ principalName: null
28111
+ });
28112
+ return {
28113
+ closedWasSelected: true,
28114
+ nextWorkspace,
28115
+ selectedWorkspaceId: nextWorkspace?.workspace_id ?? null
28116
+ };
28117
+ });
28118
+ }
28025
28119
  function workspaceOverride(explicit, environmental) {
28026
28120
  if (explicit !== void 0) {
28027
28121
  if (!UUID_RE6.test(explicit)) {
@@ -28080,6 +28174,14 @@ async function resolveWorkspace(options) {
28080
28174
  throw new WorkspaceResolutionError(workspaces);
28081
28175
  }
28082
28176
  async function selectWorkspace(selector, workspaces, store2, userId) {
28177
+ const selected = resolveWorkspaceSelector(selector, workspaces);
28178
+ if (selected.archived) {
28179
+ throw new WorkspaceUnavailableError(ARCHIVED_PROJECT_NOT_AVAILABLE);
28180
+ }
28181
+ await writeWorkspaceDefault(store2, userId, selected.workspace_id);
28182
+ return selected;
28183
+ }
28184
+ function resolveWorkspaceSelector(selector, workspaces) {
28083
28185
  const sorted = sortWorkspaces(workspaces);
28084
28186
  let selected;
28085
28187
  if (UUID_RE6.test(selector)) {
@@ -28098,7 +28200,6 @@ async function selectWorkspace(selector, workspaces, store2, userId) {
28098
28200
  selected = matches[0];
28099
28201
  }
28100
28202
  if (!selected) throw new WorkspaceUnavailableError();
28101
- await writeWorkspaceDefault(store2, userId, selected.workspace_id);
28102
28203
  return selected;
28103
28204
  }
28104
28205
  function holderLabel(holder) {
@@ -28117,13 +28218,8 @@ function relativeExpiry(expiry, now = Date.now()) {
28117
28218
  const amount = relativeMagnitude(remaining);
28118
28219
  return remaining >= 0 ? `expires in ${amount}` : `expired ${amount} ago`;
28119
28220
  }
28120
- var ARCHIVE_NOT_ENFORCED_CODE = "workspace_archive_not_enforced";
28121
- var ARCHIVE_NOT_ENFORCED_MESSAGE = "Archiving a workspace does not restrict what members or their agents can do in it: an archived workspace stays selectable, and commands against it still succeed while your membership is live. Removing a workspace from this list means ending your membership, which this CLI cannot do \u2014 ask whoever runs the workspace.";
28122
28221
  function archiveKnownGaps() {
28123
- return [{
28124
- code: ARCHIVE_NOT_ENFORCED_CODE,
28125
- message: ARCHIVE_NOT_ENFORCED_MESSAGE
28126
- }];
28222
+ return [];
28127
28223
  }
28128
28224
  function renderWorkspaces(workspaces, currentWorkspaceId) {
28129
28225
  if (workspaces.length === 0) {
@@ -28147,9 +28243,6 @@ function renderWorkspaces(workspaces, currentWorkspaceId) {
28147
28243
  "No workspace is selected. Run cswarm use <full-id|exact-name>."
28148
28244
  );
28149
28245
  }
28150
- if (workspaces.some((workspace) => workspace.archived)) {
28151
- lines.push(ARCHIVE_NOT_ENFORCED_MESSAGE);
28152
- }
28153
28246
  return lines.join("\n");
28154
28247
  }
28155
28248
  function renderStatus(options) {
@@ -29373,6 +29466,59 @@ async function runInboxFollow(options) {
29373
29466
  // src/host/opencode.ts
29374
29467
  var import_node_child_process3 = require("node:child_process");
29375
29468
  var import_node_crypto12 = require("node:crypto");
29469
+
29470
+ // src/host/stderr-tail.ts
29471
+ var RING_CAPACITY_BYTES = 4096;
29472
+ var TAIL_MAX_CHARS = 2048;
29473
+ var EXOTIC_SEPARATORS = "\\u00a0\\u1680\\u2000-\\u200d\\u2028\\u2029\\u202a-\\u202e\\u2060\\u2066-\\u2069\\u202f\\u205f\\u3000\\ufeff";
29474
+ var SEPARATOR_CLASS_SOURCE = "\\t\\n\\x0b\\f\\r " + EXOTIC_SEPARATORS;
29475
+ var ANSI_ESCAPE_GLOBAL_RE2 = new RegExp("\\u001b\\[[0-?]*[ -\\/]*[@-~]", "g");
29476
+ var CONTROL_AND_SEPARATOR_STRIP_RE = new RegExp(
29477
+ "[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f" + EXOTIC_SEPARATORS + "]",
29478
+ "g"
29479
+ );
29480
+ var CREDENTIAL_PREFIX_RE = new RegExp(
29481
+ `swm_(?:agt|inv|cap)_[^${SEPARATOR_CLASS_SOURCE}]*`,
29482
+ "gi"
29483
+ );
29484
+ function sanitizeStderrTail(raw) {
29485
+ return raw.replace(ANSI_ESCAPE_GLOBAL_RE2, "").replace(CONTROL_AND_SEPARATOR_STRIP_RE, "").replace(CREDENTIAL_PREFIX_RE, "[redacted-credential]").slice(-TAIL_MAX_CHARS).trim();
29486
+ }
29487
+ function attachStderrTailRing(stderr) {
29488
+ const chunks = [];
29489
+ let total = 0;
29490
+ let evicted = false;
29491
+ stderr.on("data", (chunk) => {
29492
+ const buffer2 = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
29493
+ chunks.push(buffer2);
29494
+ total += buffer2.length;
29495
+ while (total > RING_CAPACITY_BYTES && chunks.length > 0) {
29496
+ evicted = true;
29497
+ const head2 = chunks[0];
29498
+ const excess = total - RING_CAPACITY_BYTES;
29499
+ if (head2.length <= excess) {
29500
+ chunks.shift();
29501
+ total -= head2.length;
29502
+ } else {
29503
+ chunks[0] = head2.subarray(excess);
29504
+ total -= excess;
29505
+ }
29506
+ }
29507
+ });
29508
+ stderr.resume();
29509
+ return {
29510
+ read() {
29511
+ let text = Buffer.concat(chunks).toString("utf8");
29512
+ if (evicted) {
29513
+ const newline = text.indexOf("\n");
29514
+ text = newline === -1 ? "" : text.slice(newline + 1);
29515
+ }
29516
+ return sanitizeStderrTail(text);
29517
+ }
29518
+ };
29519
+ }
29520
+
29521
+ // src/host/opencode.ts
29376
29522
  var import_node_fs3 = require("node:fs");
29377
29523
  var import_promises4 = require("node:fs/promises");
29378
29524
  var import_node_os4 = require("node:os");
@@ -31027,8 +31173,18 @@ async function openOpenCodeAcpSession(options) {
31027
31173
  await disposeHome();
31028
31174
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
31029
31175
  }
31030
- child.stderr.on("data", () => void 0);
31031
- child.stderr.resume();
31176
+ const stderrTail = attachStderrTailRing(child.stderr);
31177
+ if (options.onStderrTail) {
31178
+ const deliverTail = options.onStderrTail;
31179
+ let tailDelivered = false;
31180
+ const publishTail = () => {
31181
+ if (tailDelivered) return;
31182
+ tailDelivered = true;
31183
+ deliverTail(stderrTail.read());
31184
+ };
31185
+ child.once("exit", publishTail);
31186
+ child.once("close", publishTail);
31187
+ }
31032
31188
  let sessionRef = null;
31033
31189
  const transport = createBoundTransport({
31034
31190
  readable: child.stdout,
@@ -31310,8 +31466,18 @@ async function openClaudeAcpSession(options) {
31310
31466
  await terminateClaudeChild(child);
31311
31467
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
31312
31468
  }
31313
- child.stderr.on("data", () => void 0);
31314
- child.stderr.resume();
31469
+ const stderrTail = attachStderrTailRing(child.stderr);
31470
+ if (options.onStderrTail) {
31471
+ const deliverTail = options.onStderrTail;
31472
+ let tailDelivered = false;
31473
+ const publishTail = () => {
31474
+ if (tailDelivered) return;
31475
+ tailDelivered = true;
31476
+ deliverTail(stderrTail.read());
31477
+ };
31478
+ child.once("exit", publishTail);
31479
+ child.once("close", publishTail);
31480
+ }
31315
31481
  let sessionRef = null;
31316
31482
  const transport = createBoundTransport({
31317
31483
  readable: child.stdout,
@@ -31586,8 +31752,18 @@ async function openCodexAcpSession(options) {
31586
31752
  await terminateCodexChild(child);
31587
31753
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
31588
31754
  }
31589
- child.stderr.on("data", () => void 0);
31590
- child.stderr.resume();
31755
+ const stderrTail = attachStderrTailRing(child.stderr);
31756
+ if (options.onStderrTail) {
31757
+ const deliverTail = options.onStderrTail;
31758
+ let tailDelivered = false;
31759
+ const publishTail = () => {
31760
+ if (tailDelivered) return;
31761
+ tailDelivered = true;
31762
+ deliverTail(stderrTail.read());
31763
+ };
31764
+ child.once("exit", publishTail);
31765
+ child.once("close", publishTail);
31766
+ }
31591
31767
  let sessionRef = null;
31592
31768
  const transport = createBoundTransport({
31593
31769
  readable: child.stdout,
@@ -31637,6 +31813,19 @@ async function openCodexAcpSession(options) {
31637
31813
  }
31638
31814
  }
31639
31815
 
31816
+ // src/listener/types.ts
31817
+ var LISTENER_PROMPT_TIMEOUT_MS = 6e5;
31818
+ var ListenerRenewalUnavailableError = class extends Error {
31819
+ constructor(message) {
31820
+ super(message);
31821
+ this.name = "renewal_unavailable";
31822
+ }
31823
+ };
31824
+ async function resolveBudgetAndPrompt(session, prompt, budget) {
31825
+ const timeoutMs = typeof budget === "number" ? budget : await budget();
31826
+ return await session.prompt(prompt, { timeoutMs });
31827
+ }
31828
+
31640
31829
  // src/listener/engine.ts
31641
31830
  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
31831
  var TERMINAL_STATES = /* @__PURE__ */ new Set(["done", "expired", "failed"]);
@@ -31751,6 +31940,7 @@ function abortError() {
31751
31940
  }
31752
31941
  function defaultRetryablePromptError(error) {
31753
31942
  if (error instanceof SenderProvenanceUnavailableError) return true;
31943
+ if (error instanceof ListenerRenewalUnavailableError) return true;
31754
31944
  if (error instanceof AcpHostError) return TRANSIENT_ACP_CODES.has(error.code);
31755
31945
  return false;
31756
31946
  }
@@ -32517,8 +32707,18 @@ async function openGrokAcpSession(options) {
32517
32707
  child.kill("SIGKILL");
32518
32708
  throw new AcpHostError("spawn_failed", "child missing stdio pipes");
32519
32709
  }
32520
- child.stderr.on("data", () => void 0);
32521
- child.stderr.resume();
32710
+ const stderrTail = attachStderrTailRing(child.stderr);
32711
+ if (options.onStderrTail) {
32712
+ const deliverTail = options.onStderrTail;
32713
+ let tailDelivered = false;
32714
+ const publishTail = () => {
32715
+ if (tailDelivered) return;
32716
+ tailDelivered = true;
32717
+ deliverTail(stderrTail.read());
32718
+ };
32719
+ child.once("exit", publishTail);
32720
+ child.once("close", publishTail);
32721
+ }
32522
32722
  let sessionRef = null;
32523
32723
  const transport = createBoundTransport({
32524
32724
  readable: child.stdout,
@@ -32583,8 +32783,9 @@ var GrokListenerModel = class {
32583
32783
  async prompt(_signal, _mode, prompt) {
32584
32784
  if (this.closed) throw new Error("listener model is closed");
32585
32785
  const worker = await this.ensureWorker();
32786
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
32586
32787
  try {
32587
- return await worker.session.prompt(prompt);
32788
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
32588
32789
  } catch (error) {
32589
32790
  if (error instanceof AcpChildExitError) {
32590
32791
  try {
@@ -32619,6 +32820,7 @@ var GrokListenerModel = class {
32619
32820
  ...this.options.model ? { model: this.options.model } : {},
32620
32821
  ...this.options.effort ? { effort: this.options.effort } : {},
32621
32822
  ...this.options.env ? { env: this.options.env } : {},
32823
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
32622
32824
  clientName: "cswarm-listener"
32623
32825
  });
32624
32826
  try {
@@ -32735,8 +32937,9 @@ var OpenCodeListenerModel = class {
32735
32937
  async prompt(_signal, _mode, prompt) {
32736
32938
  if (this.closed) throw new Error("listener model is closed");
32737
32939
  const worker = await this.ensureWorker();
32940
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
32738
32941
  try {
32739
- return await worker.session.prompt(prompt);
32942
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
32740
32943
  } catch (error) {
32741
32944
  if (error instanceof AcpChildExitError) {
32742
32945
  const home = this.workerHome;
@@ -33026,6 +33229,7 @@ var OpenCodeListenerModel = class {
33026
33229
  ...this.options.model ? { model: this.options.model } : {},
33027
33230
  ...this.options.env ? { env: this.options.env } : {},
33028
33231
  ...this.options.allowMissingAuth === true ? { allowMissingAuth: true } : {},
33232
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
33029
33233
  clientName: "cswarm-listener"
33030
33234
  });
33031
33235
  pending.phase = "opening";
@@ -33150,8 +33354,9 @@ var ClaudeListenerModel = class {
33150
33354
  async prompt(_signal, _mode, prompt) {
33151
33355
  if (this.closed) throw new Error("listener model is closed");
33152
33356
  const worker = await this.ensureWorker();
33357
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
33153
33358
  try {
33154
- return await worker.session.prompt(prompt);
33359
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
33155
33360
  } catch (error) {
33156
33361
  if (error instanceof AcpChildExitError) {
33157
33362
  try {
@@ -33233,6 +33438,7 @@ var ClaudeListenerModel = class {
33233
33438
  ...this.options.executable ? { executable: this.options.executable } : {},
33234
33439
  ...this.options.env ? { env: this.options.env } : {},
33235
33440
  signal: controller.signal,
33441
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
33236
33442
  clientName: "cswarm-listener"
33237
33443
  });
33238
33444
  this.openingHandle = handle;
@@ -33316,8 +33522,9 @@ var CodexListenerModel = class {
33316
33522
  async prompt(_signal, _mode, prompt) {
33317
33523
  if (this.closed) throw new Error("listener model is closed");
33318
33524
  const worker = await this.ensureWorker();
33525
+ const budget = this.options.promptTimeoutMs ?? LISTENER_PROMPT_TIMEOUT_MS;
33319
33526
  try {
33320
- return await worker.session.prompt(prompt);
33527
+ return await resolveBudgetAndPrompt(worker.session, prompt, budget);
33321
33528
  } catch (error) {
33322
33529
  if (error instanceof AcpChildExitError) {
33323
33530
  try {
@@ -33399,6 +33606,7 @@ var CodexListenerModel = class {
33399
33606
  ...this.options.executable ? { executable: this.options.executable } : {},
33400
33607
  ...this.options.env ? { env: this.options.env } : {},
33401
33608
  signal: controller.signal,
33609
+ ...this.options.onWorkerStderrTail ? { onStderrTail: this.options.onWorkerStderrTail } : {},
33402
33610
  clientName: "cswarm-listener"
33403
33611
  });
33404
33612
  this.openingHandle = handle;
@@ -35043,6 +35251,7 @@ var STATUS_ALLOWED_KEYS = /* @__PURE__ */ new Set([
35043
35251
  "stoppedAt",
35044
35252
  "lastSignalId",
35045
35253
  "lastErrorCode",
35254
+ "lastWorkerStderrTail",
35046
35255
  "logPath",
35047
35256
  "deliveryMode",
35048
35257
  "pendingDeliveryCount",
@@ -35097,7 +35306,7 @@ function parseStatus(raw) {
35097
35306
  const nullableUuid2 = (candidate) => candidate === null || typeof candidate === "string" && UUID_RE12.test(candidate);
35098
35307
  const nullableCount = (candidate) => candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
35099
35308
  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))) {
35309
+ if (row.version !== 1 || typeof row.instanceId !== "string" || !UUID_RE12.test(row.instanceId) || row.provider !== "grok" && row.provider !== "opencode" && row.provider !== "claude" && row.provider !== "codex" || typeof row.profileId !== "string" || typeof row.workspaceId !== "string" || !UUID_RE12.test(row.workspaceId) || typeof row.principalId !== "string" || !UUID_RE12.test(row.principalId) || !Number.isSafeInteger(row.pid) || row.pid < 1 || typeof row.state !== "string" || !["starting", "ready", "stopping", "stopped", "failed"].includes(row.state) || typeof row.startedAt !== "string" || !Number.isFinite(Date.parse(row.startedAt)) || !(row.readyAt === null || typeof row.readyAt === "string" && Number.isFinite(Date.parse(row.readyAt))) || typeof row.updatedAt !== "string" || !Number.isFinite(Date.parse(row.updatedAt)) || !(row.stoppedAt === null || typeof row.stoppedAt === "string" && Number.isFinite(Date.parse(row.stoppedAt))) || !nullableUuid2(row.lastSignalId) || !(row.lastErrorCode === null || typeof row.lastErrorCode === "string" && /^[a-z0-9_-]{1,96}$/.test(row.lastErrorCode)) || !(row.lastWorkerStderrTail === void 0 || row.lastWorkerStderrTail === null || typeof row.lastWorkerStderrTail === "string" && row.lastWorkerStderrTail.length > 0 && row.lastWorkerStderrTail.length <= 2048 && !/swm_(?:agt|inv|cap)_/i.test(row.lastWorkerStderrTail)) || typeof row.logPath !== "string" || !(0, import_node_path14.isAbsolute)(row.logPath) || !(row.deliveryMode === void 0 || row.deliveryMode === null || typeof row.deliveryMode === "string" && STATUS_DELIVERY_MODES.has(row.deliveryMode)) || !(row.pendingDeliveryCount === void 0 || nullableCount(row.pendingDeliveryCount)) || !(row.lastTerminalDeliveryFailureCount === void 0 || nullableCount(row.lastTerminalDeliveryFailureCount)) || !(row.lastTerminalDeliveryFailureAt === void 0 || nullableTimestamp(row.lastTerminalDeliveryFailureAt)) || !(row.lastClaimAt === void 0 || nullableTimestamp(row.lastClaimAt)) || !(row.lastAckAt === void 0 || nullableTimestamp(row.lastAckAt))) {
35101
35310
  throw new Error("stored listener status is malformed");
35102
35311
  }
35103
35312
  return {
@@ -35107,7 +35316,8 @@ function parseStatus(raw) {
35107
35316
  lastTerminalDeliveryFailureCount: row.lastTerminalDeliveryFailureCount ?? null,
35108
35317
  lastTerminalDeliveryFailureAt: row.lastTerminalDeliveryFailureAt ?? null,
35109
35318
  lastClaimAt: row.lastClaimAt ?? null,
35110
- lastAckAt: row.lastAckAt ?? null
35319
+ lastAckAt: row.lastAckAt ?? null,
35320
+ lastWorkerStderrTail: row.lastWorkerStderrTail ?? null
35111
35321
  };
35112
35322
  }
35113
35323
  async function writeListenerStatus(paths, status) {
@@ -35144,7 +35354,12 @@ async function appendListenerEvent(paths, event) {
35144
35354
  // D-051 companion 2: why a listener is down, and why it stopped trying.
35145
35355
  "restart_attempts",
35146
35356
  "restartable",
35147
- "restarts_exhausted"
35357
+ "restarts_exhausted",
35358
+ // D-090 family: the last lines of a dead worker's stderr, sanitized and
35359
+ // bounded by the supervisor, and the prompt-turn budget behind a timeout.
35360
+ // Local log only — this file never feeds a server payload.
35361
+ "worker_stderr_tail",
35362
+ "turn_budget_ms"
35148
35363
  ]);
35149
35364
  const deliveryModes = /* @__PURE__ */ new Set(["durable_claim", "cursor_fallback"]);
35150
35365
  const deliveryOutcomes = /* @__PURE__ */ new Set([
@@ -35166,7 +35381,16 @@ async function appendListenerEvent(paths, event) {
35166
35381
  if (key2 === "outcome" && !(value === null || typeof value === "string" && deliveryOutcomes.has(value))) {
35167
35382
  throw new Error("listener event outcome is not allowed");
35168
35383
  }
35169
- if (typeof value === "string" && (value.length > 128 || /swm_(?:agt|inv|cap)_/i.test(value))) {
35384
+ if (key2 === "worker_stderr_tail" && !(typeof value === "string" && value.length > 0 && value.length <= 2048)) {
35385
+ throw new Error("listener event stderr tail is not allowed");
35386
+ }
35387
+ if (key2 === "turn_budget_ms" && !(typeof value === "number" && Number.isSafeInteger(value) && value > 0)) {
35388
+ throw new Error("listener event turn budget is not allowed");
35389
+ }
35390
+ if (typeof value === "string" && // worker_stderr_tail is deliberately exempt from the generic 128-char
35391
+ // cap (its own bound is 2048, above); the secret scan still applies to
35392
+ // every string, the tail included.
35393
+ (key2 !== "worker_stderr_tail" && value.length > 128 || /swm_(?:agt|inv|cap)_/i.test(value))) {
35170
35394
  throw new Error("listener event contains unsafe text");
35171
35395
  }
35172
35396
  }
@@ -35443,6 +35667,23 @@ function safeErrorCode(error) {
35443
35667
  const name = error.name.toLowerCase().replace(/[^a-z0-9_-]+/g, "_");
35444
35668
  return name.slice(0, 96) || "listener_error";
35445
35669
  }
35670
+ var TAIL_SERIALIZED_BUDGET_BYTES = 3e3;
35671
+ function fitWorkerStderrTailForLog(tail) {
35672
+ let fitted = tail.trim();
35673
+ for (; ; ) {
35674
+ if (fitted.length === 0) return fitted;
35675
+ const serializedBytes = Buffer.byteLength(JSON.stringify(fitted), "utf8");
35676
+ if (fitted.length <= 2048 && serializedBytes <= TAIL_SERIALIZED_BUDGET_BYTES) {
35677
+ return fitted;
35678
+ }
35679
+ const dropChars = Math.max(
35680
+ fitted.length - 2048,
35681
+ Math.ceil((serializedBytes - TAIL_SERIALIZED_BUDGET_BYTES) / 6),
35682
+ 1
35683
+ );
35684
+ fitted = fitted.slice(dropChars);
35685
+ }
35686
+ }
35446
35687
  async function runListenerSupervisor(options) {
35447
35688
  const now = options.now ?? Date.now;
35448
35689
  const startedAt = iso2(now);
@@ -35464,6 +35705,7 @@ async function runListenerSupervisor(options) {
35464
35705
  stoppedAt: null,
35465
35706
  lastSignalId: null,
35466
35707
  lastErrorCode: null,
35708
+ lastWorkerStderrTail: null,
35467
35709
  deliveryMode: null,
35468
35710
  pendingDeliveryCount: null,
35469
35711
  lastTerminalDeliveryFailureCount: null,
@@ -35518,9 +35760,21 @@ async function runListenerSupervisor(options) {
35518
35760
  instance_id: status.instanceId,
35519
35761
  pid: process.pid
35520
35762
  });
35763
+ const takeWorkerStderrTail = options.takeWorkerStderrTail;
35764
+ const takeTail = () => {
35765
+ if (!takeWorkerStderrTail) return null;
35766
+ const tail = takeWorkerStderrTail();
35767
+ if (typeof tail !== "string") return null;
35768
+ const fitted = fitWorkerStderrTailForLog(tail);
35769
+ return fitted.length > 0 ? fitted : null;
35770
+ };
35521
35771
  const onEvent = (event) => {
35522
35772
  if (event.type === "ready") {
35523
- transition("ready", { readyAt: event.ts, lastErrorCode: null });
35773
+ transition("ready", {
35774
+ readyAt: event.ts,
35775
+ lastErrorCode: null,
35776
+ lastWorkerStderrTail: null
35777
+ });
35524
35778
  log({ ts: event.ts, event: "listener_ready" });
35525
35779
  return;
35526
35780
  }
@@ -35537,7 +35791,15 @@ async function runListenerSupervisor(options) {
35537
35791
  event: "listener_effect",
35538
35792
  signal_id: event.signalId,
35539
35793
  status: event.status,
35540
- failure_code: event.failureCode
35794
+ failure_code: event.failureCode,
35795
+ // Code comparison, not message matching (D-053). The budget rides
35796
+ // only the timeout class so a reader can see what bound was hit — and
35797
+ // it is the CLAMPED budget actually in force, not the configured cap.
35798
+ ...(() => {
35799
+ if (event.failureCode !== "acptimeouterror") return {};
35800
+ const budget = options.getTurnBudgetMs?.();
35801
+ return typeof budget === "number" && budget > 0 ? { turn_budget_ms: budget } : {};
35802
+ })()
35541
35803
  });
35542
35804
  return;
35543
35805
  }
@@ -35667,14 +35929,20 @@ async function runListenerSupervisor(options) {
35667
35929
  restarts += 1;
35668
35930
  const delayMs = nextListenerRestartMs(restarts, policy, restartRandom);
35669
35931
  const restartCode = safeErrorCode(stop.error);
35932
+ const restartStderrTail = takeTail();
35670
35933
  log({
35671
35934
  ts: iso2(now),
35672
35935
  event: "listener_restarting",
35673
35936
  attempt: restarts,
35674
35937
  delay_ms: delayMs,
35675
- failure_code: restartCode
35938
+ failure_code: restartCode,
35939
+ ...restartStderrTail !== null ? { worker_stderr_tail: restartStderrTail } : {}
35940
+ });
35941
+ transition("starting", {
35942
+ readyAt: null,
35943
+ lastErrorCode: restartCode,
35944
+ lastWorkerStderrTail: restartStderrTail
35676
35945
  });
35677
- transition("starting", { readyAt: null, lastErrorCode: restartCode });
35678
35946
  await restartSleep(delayMs, controller.signal);
35679
35947
  if (controller.signal.aborted) {
35680
35948
  stop = { reason: "cancelled" };
@@ -35685,14 +35953,17 @@ async function runListenerSupervisor(options) {
35685
35953
  if (stop.reason === "cancelled") {
35686
35954
  transition("stopped", {
35687
35955
  stoppedAt,
35688
- lastErrorCode: null
35956
+ lastErrorCode: null,
35957
+ lastWorkerStderrTail: null
35689
35958
  });
35690
35959
  log({ ts: stoppedAt, event: "listener_stopped" });
35691
35960
  } else {
35692
35961
  const code = stop.reason === "credential" ? "credential_stopped" : safeErrorCode(stop.error);
35962
+ const failedStderrTail = takeTail();
35693
35963
  transition("failed", {
35694
35964
  stoppedAt,
35695
- lastErrorCode: code
35965
+ lastErrorCode: code,
35966
+ lastWorkerStderrTail: failedStderrTail
35696
35967
  });
35697
35968
  log({
35698
35969
  ts: stoppedAt,
@@ -35700,7 +35971,8 @@ async function runListenerSupervisor(options) {
35700
35971
  failure_code: code,
35701
35972
  restart_attempts: restarts,
35702
35973
  restartable: eligible,
35703
- restarts_exhausted: exhausted
35974
+ restarts_exhausted: exhausted,
35975
+ ...failedStderrTail !== null ? { worker_stderr_tail: failedStderrTail } : {}
35704
35976
  });
35705
35977
  }
35706
35978
  } catch (error) {
@@ -35708,11 +35980,17 @@ async function runListenerSupervisor(options) {
35708
35980
  const code = safeErrorCode(
35709
35981
  error instanceof Error ? error : new Error(String(error))
35710
35982
  );
35711
- transition("failed", { stoppedAt, lastErrorCode: code });
35983
+ const failedStderrTail = takeTail();
35984
+ transition("failed", {
35985
+ stoppedAt,
35986
+ lastErrorCode: code,
35987
+ lastWorkerStderrTail: failedStderrTail
35988
+ });
35712
35989
  log({
35713
35990
  ts: stoppedAt,
35714
35991
  event: "listener_failed",
35715
- failure_code: code
35992
+ failure_code: code,
35993
+ ...failedStderrTail !== null ? { worker_stderr_tail: failedStderrTail } : {}
35716
35994
  });
35717
35995
  } finally {
35718
35996
  await writes.catch(() => void 0);
@@ -36564,7 +36842,8 @@ function buildListenerChildArgs(spec) {
36564
36842
  ...provider === "claude" && claudeExe ? ["--claude-executable", claudeExe] : [],
36565
36843
  ...provider === "codex" && codexExe ? ["--codex-executable", codexExe] : [],
36566
36844
  ...spec.model ? ["--model", spec.model] : [],
36567
- ...provider === "grok" && spec.effort ? ["--effort", spec.effort] : []
36845
+ ...provider === "grok" && spec.effort ? ["--effort", spec.effort] : [],
36846
+ ...spec.turnBudget ? ["--turn-budget", spec.turnBudget] : []
36568
36847
  ];
36569
36848
  }
36570
36849
  async function spawnDetachedListener(options) {
@@ -36649,6 +36928,7 @@ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
36649
36928
  "to",
36650
36929
  "token-id",
36651
36930
  "ttl-ms",
36931
+ "turn-budget",
36652
36932
  "uid",
36653
36933
  "until",
36654
36934
  "url",
@@ -36682,8 +36962,8 @@ var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
36682
36962
  AGENT_CREDENTIAL_MESSAGE_D088
36683
36963
  ];
36684
36964
  function packageVersion() {
36685
- if ("0.1.21".length > 0) {
36686
- return "0.1.21";
36965
+ if ("0.1.23".length > 0) {
36966
+ return "0.1.23";
36687
36967
  }
36688
36968
  try {
36689
36969
  const value = JSON.parse(
@@ -36810,7 +37090,8 @@ Usage:
36810
37090
  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
37091
  cswarm file rm <name|file-id> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
36812
37092
  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]
37093
+ cswarm feedback "<text>" --kind bug|idea|friction [--about <ref>] [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--agent-token-stdin] [--json]
37094
+ cswarm listen start --agent-token-stdin [--url <url> --anon-key <key>] --workspace-id <uuid> --provider grok|opencode|claude|codex [--cwd <absolute-path>] [--model <model>] [--effort <level>] [--permissions deny|allow] [--grok-executable <path>] [--opencode-executable <path>] [--claude-executable <path>] [--codex-executable <path>] [--turn-budget <duration>] [--foreground] [--json]
36814
37095
  cswarm listen status [--url <url> --anon-key <key>] --workspace-id <uuid> --principal-id <uuid> [--json]
36815
37096
  cswarm listen stop [--url <url> --anon-key <key>] --workspace-id <uuid> --principal-id <uuid> [--json]
36816
37097
  cswarm new "<workspace name>" [--url <url> --anon-key <key>] [--json]
@@ -36820,6 +37101,7 @@ Usage:
36820
37101
  cswarm invite [--url <url> --anon-key <key>] [--workspace-id <uuid>] --email <email>
36821
37102
  cswarm invite revoke [--url <url> --anon-key <key>] [--workspace-id <uuid>] --invitation-id <uuid> [--json]
36822
37103
  cswarm member remove <full-user-id|exact-name> --confirm <same-selector> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
37104
+ cswarm workspace close <full-id|exact-name> --confirm <same-selector> [--url <url> --anon-key <key>] [--json]
36823
37105
  cswarm accept --link-stdin [--name <name>] [--no-browser] [--json]
36824
37106
  cswarm accept <https://...#invite=...|cswarm://accept/...> [--name <name>] [--no-browser] [--json] # unsafe: shell history/process list
36825
37107
  cswarm accept --invitation-token-stdin [--url <url> --anon-key <key>]
@@ -36845,15 +37127,33 @@ Credential selection for command/dogfood:
36845
37127
  members reads only -- either form
36846
37128
  file put, file ls, file get, file rm, file restore
36847
37129
  read and command, nothing persisted -- either form
37130
+ feedback command only, nothing persisted -- either form
36848
37131
  listen start persists durable state, rotates -- needs expires_at
36849
37132
  token revoke names what it revokes -- needs token_id
37133
+ workspace close is human-session-only; it never accepts an agent token
37134
+
37135
+ Found a bug or missing feature in cswarm itself? cswarm feedback sends it to the
37136
+ deployment's operators \u2014 agents are encouraged to report friction they hit.
36850
37137
 
36851
37138
  Signals (intention sharing) accept the same credential selection. Agent mode
36852
37139
  never opens a browser or infers a human's saved workspace. Durations use a whole
36853
37140
  number plus m, h, or d (for example 90m, 24h, or 7d) and are capped at 30d.
36854
37141
  Place -- before signal text that itself begins with -- to stop option parsing.
37142
+ Signal text is at most 2000 characters and --about at most 500; a longer body is
37143
+ refused locally before any network call, so compose within the limit.
36855
37144
 
36856
- Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, and new require a
37145
+ listen start --turn-budget bounds ONE worker prompt turn (default 10m): how long
37146
+ the worker may think and use tools on a single message before the turn times out
37147
+ and durable delivery retries it. A whole number plus s, m, or h (for example
37148
+ 90s, 5m, 1h), at least 30s and at most 60m. Each turn is additionally clamped
37149
+ to the live credential's remaining lifetime minus 60s, after renewing it when
37150
+ due \u2014 a turn never outlives its credential. Right after a rotation the full
37151
+ budget is available up to the token TTL minus 60s (about 59m on the default 1h
37152
+ TTL); a turn that lands just before a rotation can be clamped to the ~5m
37153
+ renewal lead, and if it times out there, durable delivery retries it on the
37154
+ fresh credential.
37155
+
37156
+ Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, new, and workspace close require a
36857
37157
  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
37158
  registers one principal. Invitation links, agent credentials, and capability links
36859
37159
  appear only in fresh success responses.
@@ -37548,6 +37848,53 @@ async function runMember(args) {
37548
37848
  `);
37549
37849
  }
37550
37850
  }
37851
+ async function runWorkspace(args) {
37852
+ args.assertShape([...TARGET_FLAGS, "confirm", "json"], 3);
37853
+ if (args.positionals[1] !== "close") {
37854
+ throw new UsageError(
37855
+ `unknown workspace command: ${args.positionals[1] ?? "(missing)"}`
37856
+ );
37857
+ }
37858
+ const selector = args.positionals[2];
37859
+ if (args.required("confirm") !== selector) {
37860
+ throw new Error(
37861
+ "--confirm must exactly repeat the workspace selector; no request was sent"
37862
+ );
37863
+ }
37864
+ const cloud = await target(args);
37865
+ const human = await humanCredential(args, cloud);
37866
+ const directory = cloudWorkspaceDirectory(cloud);
37867
+ const projects = await directory.list(human);
37868
+ const selected = resolveWorkspaceSelector(selector, projects);
37869
+ const response = (await sendConnectWithPending(
37870
+ new ThinCommandClient(cloud),
37871
+ human,
37872
+ selected.workspace_id,
37873
+ { kind: "archive_workspace" }
37874
+ )).response;
37875
+ if (response.status !== "accepted") {
37876
+ throw new Error(
37877
+ `Workspace close was rejected: ${response.reason ?? "domain rejection"}. The workspace is still open.`
37878
+ );
37879
+ }
37880
+ const { closedWasSelected, nextWorkspace, selectedWorkspaceId } = await updateWorkspaceDefaultAfterClose(
37881
+ human.store,
37882
+ human.userId,
37883
+ selected.workspace_id,
37884
+ projects
37885
+ );
37886
+ const message = nextWorkspace ? `Closed workspace ${selected.name} (${selected.workspace_id}). It is hidden for everyone, and ${nextWorkspace.name} (${nextWorkspace.workspace_id}) is now selected.` : closedWasSelected ? `Closed workspace ${selected.name} (${selected.workspace_id}). It is hidden for everyone. No live workspace remains, so the selected workspace was cleared.` : `Closed workspace ${selected.name} (${selected.workspace_id}). It is hidden for everyone. Your selected workspace was not changed.`;
37887
+ const output = {
37888
+ message,
37889
+ status: response.status,
37890
+ workspace_id: selected.workspace_id,
37891
+ selected_workspace_id: selectedWorkspaceId,
37892
+ command_event_ids: response.event_ids
37893
+ };
37894
+ if (args.has("json")) printJson(output);
37895
+ else process.stdout.write(`${message}
37896
+ `);
37897
+ }
37551
37898
  async function runLegacyAccept(args) {
37552
37899
  const invitationToken = await invitationCredential(args);
37553
37900
  const cloud = await target(args);
@@ -38208,8 +38555,42 @@ function signalDuration(value) {
38208
38555
  }
38209
38556
  return milliseconds;
38210
38557
  }
38558
+ function listenerTurnBudgetMs(value) {
38559
+ if (value === void 0) return LISTENER_PROMPT_TIMEOUT_MS;
38560
+ const match = /^([1-9]\d*)(s|m|h)$/.exec(value);
38561
+ if (!match) {
38562
+ throw new Error("--turn-budget must be a duration such as 90s, 5m, or 1h");
38563
+ }
38564
+ const unit = match[2] === "s" ? 1e3 : match[2] === "m" ? 6e4 : 36e5;
38565
+ const milliseconds = Number(match[1]) * unit;
38566
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 3e4 || milliseconds > 36e5) {
38567
+ throw new Error("--turn-budget must be between 30s and 60m");
38568
+ }
38569
+ return milliseconds;
38570
+ }
38571
+ var TURN_BUDGET_CREDENTIAL_MARGIN_MS = 6e4;
38572
+ function clampTurnBudgetToCredential(budgetMs, credentialExpiresAt, nowMs) {
38573
+ if (credentialExpiresAt === null) return budgetMs;
38574
+ const horizonMs = credentialExpiresAt - nowMs - TURN_BUDGET_CREDENTIAL_MARGIN_MS;
38575
+ return Math.max(1e3, Math.min(budgetMs, horizonMs));
38576
+ }
38577
+ function resolveTurnBudgetOrDefer(configuredBudgetMs, credentialExpiresAt, nowMs, renewalFailed) {
38578
+ if (renewalFailed) {
38579
+ throw new ListenerRenewalUnavailableError(
38580
+ "the worker credential could not be renewed before this turn; deferring the ask for durable redelivery"
38581
+ );
38582
+ }
38583
+ if (credentialExpiresAt !== null && credentialExpiresAt - nowMs <= TURN_BUDGET_CREDENTIAL_MARGIN_MS) {
38584
+ throw new ListenerRenewalUnavailableError(
38585
+ "the live worker credential is inside its rotation margin and was not renewed; deferring the ask for durable redelivery"
38586
+ );
38587
+ }
38588
+ return clampTurnBudgetToCredential(configuredBudgetMs, credentialExpiresAt, nowMs);
38589
+ }
38590
+ var SIGNAL_BODY_MAX = 2e3;
38591
+ var SIGNAL_ABOUT_MAX = 500;
38211
38592
  function signalText(value, label) {
38212
- const maximum = label === "body" ? 2e3 : 500;
38593
+ const maximum = label === "body" ? SIGNAL_BODY_MAX : SIGNAL_ABOUT_MAX;
38213
38594
  if (value.length < (label === "body" ? 1 : 0) || value.length > maximum) {
38214
38595
  throw new Error(
38215
38596
  `${label === "body" ? "signal text" : "--about"} must be ${label === "body" ? "1.." : "at most "}${maximum} characters`
@@ -38468,6 +38849,10 @@ Check whether you are about to do the same work.
38468
38849
  `}`
38469
38850
  );
38470
38851
  }
38852
+ function replyRefusalHint(error) {
38853
+ if (!(error instanceof CommandHttpError) || error.status !== 403) return null;
38854
+ 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.";
38855
+ }
38471
38856
  async function runReply(args) {
38472
38857
  args.assertShape([
38473
38858
  ...TARGET_FLAGS,
@@ -38499,7 +38884,14 @@ async function runReply(args) {
38499
38884
  about: null,
38500
38885
  ...untilMs2 === void 0 ? {} : { until_ms: untilMs2 }
38501
38886
  };
38502
- const result = await postSignalCommand(cloud, credential, command2);
38887
+ let result;
38888
+ try {
38889
+ result = await postSignalCommand(cloud, credential, command2);
38890
+ } catch (error) {
38891
+ const hint = replyRefusalHint(error);
38892
+ if (hint !== null) throw new Error(hint);
38893
+ throw error;
38894
+ }
38503
38895
  const signal = result.response.signal;
38504
38896
  if (args.has("json")) {
38505
38897
  printJson({
@@ -38990,6 +39382,13 @@ function renderListenerStatus(status) {
38990
39382
  status.lastSignalId ? `Last handled signal: ${status.lastSignalId}.` : "No signal has been handled yet.",
38991
39383
  status.lastErrorCode ? `Last status code: ${status.lastErrorCode}.` : "No listener error is recorded."
38992
39384
  ];
39385
+ if (status.lastWorkerStderrTail) {
39386
+ const tailLines = status.lastWorkerStderrTail.split("\n").filter((line) => line.trim().length > 0);
39387
+ lines.push("Worker stderr (local log only):");
39388
+ for (const line of tailLines.slice(-3)) {
39389
+ lines.push(` ${line}`);
39390
+ }
39391
+ }
38993
39392
  if (status.deliveryMode === "durable_claim") {
38994
39393
  lines.push("Delivery mode: durable claim and acknowledgement.");
38995
39394
  } else if (status.deliveryMode === "cursor_fallback") {
@@ -39169,22 +39568,58 @@ async function runConfiguredListener(options) {
39169
39568
  principalId: options.principalId,
39170
39569
  ...options.stateDirectory ? { stateDirectory: options.stateDirectory } : {}
39171
39570
  });
39571
+ const turnBudgetMs = options.turnBudgetMs ?? LISTENER_PROMPT_TIMEOUT_MS;
39572
+ let lastAppliedTurnBudgetMs = null;
39573
+ const resolveTurnBudgetMs = async () => {
39574
+ let renewalFailed = false;
39575
+ try {
39576
+ await credentialSession.bearer();
39577
+ } catch {
39578
+ renewalFailed = true;
39579
+ }
39580
+ const applied = resolveTurnBudgetOrDefer(
39581
+ turnBudgetMs,
39582
+ credentialSession.expiry,
39583
+ Date.now(),
39584
+ renewalFailed
39585
+ );
39586
+ lastAppliedTurnBudgetMs = applied;
39587
+ return applied;
39588
+ };
39589
+ let lastWorkerStderrTail = null;
39590
+ let workerStderrGeneration = 0;
39591
+ const newWorkerStderrTailSink = () => {
39592
+ const generation = ++workerStderrGeneration;
39593
+ lastWorkerStderrTail = null;
39594
+ return (tail) => {
39595
+ if (generation !== workerStderrGeneration) return;
39596
+ lastWorkerStderrTail = tail.length > 0 ? tail : null;
39597
+ };
39598
+ };
39172
39599
  const newModel = () => options.provider === "opencode" ? new OpenCodeListenerModel({
39173
39600
  cwd: options.cwd,
39174
39601
  permissionMode: options.permissionMode,
39602
+ promptTimeoutMs: resolveTurnBudgetMs,
39603
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39175
39604
  ...options.model ? { model: options.model } : {},
39176
39605
  ...options.opencodeExecutable ? { executable: options.opencodeExecutable } : options.executable ? { executable: options.executable } : {}
39177
39606
  }) : options.provider === "claude" ? new ClaudeListenerModel({
39178
39607
  cwd: options.cwd,
39179
39608
  permissionMode: options.permissionMode,
39609
+ promptTimeoutMs: resolveTurnBudgetMs,
39610
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39180
39611
  ...options.claudeExecutable ? { executable: options.claudeExecutable } : options.executable ? { executable: options.executable } : {}
39181
39612
  }) : options.provider === "codex" ? new CodexListenerModel({
39182
39613
  cwd: options.cwd,
39183
39614
  permissionMode: options.permissionMode,
39615
+ promptTimeoutMs: resolveTurnBudgetMs,
39616
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39184
39617
  ...options.codexExecutable ? { executable: options.codexExecutable } : options.executable ? { executable: options.executable } : {}
39185
39618
  }) : new GrokListenerModel({
39186
39619
  cwd: options.cwd,
39187
39620
  permissionMode: options.permissionMode,
39621
+ promptTimeoutMs: resolveTurnBudgetMs,
39622
+ onWorkerStderrTail: newWorkerStderrTailSink(),
39188
39623
  ...options.model ? { model: options.model } : {},
39189
39624
  ...options.effort ? { effort: options.effort } : {},
39190
39625
  ...options.executable ? { executable: options.executable } : {}
@@ -39204,6 +39639,14 @@ async function runConfiguredListener(options) {
39204
39639
  principalId: options.principalId,
39205
39640
  provider: options.provider,
39206
39641
  permissionMode: options.permissionMode,
39642
+ // The bound a timeout event reports: the last turn's clamped budget when
39643
+ // one has run, else the configured cap.
39644
+ getTurnBudgetMs: () => lastAppliedTurnBudgetMs ?? turnBudgetMs,
39645
+ takeWorkerStderrTail: () => {
39646
+ const tail = lastWorkerStderrTail;
39647
+ lastWorkerStderrTail = null;
39648
+ return tail;
39649
+ },
39207
39650
  prepare: async (proposedInstanceId) => {
39208
39651
  const selected = await openListenerDeliveryJournal({
39209
39652
  profileId: options.cloud.profileId,
@@ -39258,6 +39701,7 @@ async function runListenStart(args) {
39258
39701
  "claude-executable",
39259
39702
  "codex-executable",
39260
39703
  "state-dir",
39704
+ "turn-budget",
39261
39705
  "foreground",
39262
39706
  "json"
39263
39707
  ], 2);
@@ -39268,6 +39712,7 @@ async function runListenStart(args) {
39268
39712
  }
39269
39713
  const provider = listenerProvider(args);
39270
39714
  validateListenerProviderFlags(args, provider);
39715
+ const turnBudgetMs = listenerTurnBudgetMs(args.optional("turn-budget"));
39271
39716
  const cloud = await target(args);
39272
39717
  const workspaceId2 = listenerUuid(
39273
39718
  args.optional("workspace-id") ?? process.env.SWARM_CLOUD_WORKSPACE_ID,
@@ -39302,6 +39747,7 @@ async function runListenStart(args) {
39302
39747
  cwd,
39303
39748
  permissionMode,
39304
39749
  provider,
39750
+ turnBudgetMs,
39305
39751
  ...args.optional("model") ? { model: args.required("model") } : {},
39306
39752
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39307
39753
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
@@ -39352,6 +39798,7 @@ async function runListenStart(args) {
39352
39798
  ...stateDirectory2 ? { stateDirectory: stateDirectory2 } : {},
39353
39799
  ...args.optional("model") ? { model: args.required("model") } : {},
39354
39800
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39801
+ ...args.optional("turn-budget") ? { turnBudget: args.required("turn-budget") } : {},
39355
39802
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
39356
39803
  ...opencodeExecutable ? { opencodeExecutable } : {},
39357
39804
  ...claudeExecutable ? { claudeExecutable } : {},
@@ -39409,10 +39856,12 @@ async function runListenSupervisor(args) {
39409
39856
  "opencode-executable",
39410
39857
  "claude-executable",
39411
39858
  "codex-executable",
39412
- "state-dir"
39859
+ "state-dir",
39860
+ "turn-budget"
39413
39861
  ], 1);
39414
39862
  const provider = listenerProvider(args);
39415
39863
  validateListenerProviderFlags(args, provider);
39864
+ const turnBudgetMs = listenerTurnBudgetMs(args.optional("turn-budget"));
39416
39865
  const cloud = await target(args);
39417
39866
  const workspaceId2 = listenerUuid(args.optional("workspace-id"), "workspace-id");
39418
39867
  const principalId = listenerUuid(args.optional("principal-id"), "principal-id");
@@ -39428,6 +39877,7 @@ async function runListenSupervisor(args) {
39428
39877
  cwd,
39429
39878
  permissionMode: listenerPermissionMode(args.optional("permissions")),
39430
39879
  provider,
39880
+ turnBudgetMs,
39431
39881
  ...args.optional("model") ? { model: args.required("model") } : {},
39432
39882
  ...args.optional("effort") ? { effort: args.required("effort") } : {},
39433
39883
  ...args.optional("grok-executable") ? { executable: args.required("grok-executable") } : {},
@@ -39709,6 +40159,48 @@ async function runFileRestore(args) {
39709
40159
  `
39710
40160
  );
39711
40161
  }
40162
+ async function runFeedback(args) {
40163
+ const body = args.positionals[1];
40164
+ if (!body) {
40165
+ throw new UsageError(
40166
+ 'cswarm feedback needs the feedback text: cswarm feedback "<text>" --kind bug|idea|friction'
40167
+ );
40168
+ }
40169
+ const kind = args.required("kind");
40170
+ if (kind !== "bug" && kind !== "idea" && kind !== "friction") {
40171
+ throw new UsageError("--kind must be bug, idea, or friction");
40172
+ }
40173
+ const about = args.optional("about");
40174
+ const context = await fileContext(args, ["kind", "about"], 2);
40175
+ const submitted = await submitFeedback({
40176
+ target: context.cloud,
40177
+ workspaceId: context.selected.selectedWorkspace,
40178
+ credential: context.selected.bearer
40179
+ }, {
40180
+ category: kind,
40181
+ body,
40182
+ context: {
40183
+ surface: "cli",
40184
+ cswarm_version: CLI_BUILD_VERSION,
40185
+ platform: process.platform,
40186
+ ...about ? { about } : {}
40187
+ }
40188
+ });
40189
+ if (args.has("json")) {
40190
+ process.stdout.write(`${JSON.stringify(submitted, null, 2)}
40191
+ `);
40192
+ return;
40193
+ }
40194
+ if (submitted.duplicate === true) {
40195
+ process.stdout.write(
40196
+ "This matches feedback you sent within the hour, so it was not recorded twice. It is already with the operators of this deployment.\n"
40197
+ );
40198
+ return;
40199
+ }
40200
+ process.stdout.write(
40201
+ "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"
40202
+ );
40203
+ }
39712
40204
  async function runFile(args) {
39713
40205
  const action = args.positionals[1];
39714
40206
  if (action === "put") return await runFilePut(args);
@@ -39956,6 +40448,10 @@ async function main() {
39956
40448
  await runMember(args);
39957
40449
  return;
39958
40450
  }
40451
+ if (verb === "workspace") {
40452
+ await runWorkspace(args);
40453
+ return;
40454
+ }
39959
40455
  if (verb === "target") {
39960
40456
  await runTarget(args);
39961
40457
  return;
@@ -39964,6 +40460,10 @@ async function main() {
39964
40460
  await runStatus(args);
39965
40461
  return;
39966
40462
  }
40463
+ if (verb === "feedback") {
40464
+ await runFeedback(args);
40465
+ return;
40466
+ }
39967
40467
  if (verb === "file") {
39968
40468
  await runFile(args);
39969
40469
  return;
@@ -40089,12 +40589,16 @@ ${usage()}
40089
40589
  // Annotate the CommonJS export names for ESM import in node:
40090
40590
  0 && (module.exports = {
40091
40591
  EXIT_RESTARTABLE,
40592
+ TURN_BUDGET_CREDENTIAL_MARGIN_MS,
40593
+ clampTurnBudgetToCredential,
40092
40594
  describeAudience,
40093
40595
  listenerFailureMessage,
40094
40596
  listenerHostLimits,
40095
40597
  listenerPermissionMode,
40096
40598
  listenerStatusJson,
40097
40599
  renderRoster,
40600
+ replyRefusalHint,
40098
40601
  resolveDetachedClaudeExecutable,
40099
- resolveDetachedCodexExecutable
40602
+ resolveDetachedCodexExecutable,
40603
+ resolveTurnBudgetOrDefer
40100
40604
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commonswarm",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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"