@mutagent/cli 0.2.207 → 0.2.209

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/dist/bin/cli.js +133 -25
  2. package/package.json +3 -3
package/dist/bin/cli.js CHANGED
@@ -28444,10 +28444,8 @@ async function* readSseEvents(body) {
28444
28444
  }
28445
28445
  }
28446
28446
 
28447
- // src/lib/sandbox-stream.ts
28448
- init_errors();
28449
-
28450
28447
  // src/lib/stream-reconnect.ts
28448
+ init_errors();
28451
28449
  var QUICK_CLOSE_MS = 1000;
28452
28450
  var CLEAN_BACKOFF_BASE_MS = 1000;
28453
28451
  var CLEAN_BACKOFF_CAP_MS = 5000;
@@ -28465,13 +28463,62 @@ class CleanClosePacer {
28465
28463
  return Math.min(CLEAN_BACKOFF_BASE_MS * 2 ** (this.streak - 2), CLEAN_BACKOFF_CAP_MS);
28466
28464
  }
28467
28465
  }
28468
- function retryFailedNotice(failedRetries, max, lastSeq) {
28469
- return `Reconnect failed (${String(failedRetries)}/${String(max)}). Retrying from seq ${String(lastSeq)}...`;
28466
+ function retryFailedNotice(failedRetries, _max, lastSeq) {
28467
+ return `Reconnect failed (attempt ${String(failedRetries)}). Retrying from seq ${String(lastSeq)}...`;
28468
+ }
28469
+ var OUTAGE_WINDOW_MS = 300000;
28470
+ var OUTAGE_BACKOFF_CAP_MS = 15000;
28471
+ var CHECK_TIMEOUT_MS = 1e4;
28472
+ function outageBackoffMs(baseMs, attempt, capMs = Infinity) {
28473
+ return Math.min(baseMs * 2 ** (attempt - 1), capMs);
28474
+ }
28475
+ async function askBounded(check, timeoutMs = CHECK_TIMEOUT_MS) {
28476
+ let timer;
28477
+ const timeout = new Promise((resolve) => {
28478
+ timer = setTimeout(() => {
28479
+ resolve({ state: "unknown", detail: `no answer within ${String(timeoutMs / 1000)} s` });
28480
+ }, timeoutMs);
28481
+ });
28482
+ try {
28483
+ return await Promise.race([
28484
+ check().catch((error) => ({
28485
+ state: "unknown",
28486
+ detail: error instanceof Error ? error.message : String(error)
28487
+ })),
28488
+ timeout
28489
+ ]);
28490
+ } finally {
28491
+ clearTimeout(timer);
28492
+ }
28493
+ }
28494
+ function lostStreamGuidance(lastSeq, checked, resume) {
28495
+ const at = `Resume from seq ${String(lastSeq)} with: ${resume}`;
28496
+ if (checked === undefined) {
28497
+ return `Could not check whether the sandbox is still running. ${at}`;
28498
+ }
28499
+ if (checked.state === "running") {
28500
+ return `The server reports the sandbox still running${checked.detail === undefined ? "" : ` (${checked.detail})`}. ${at}`;
28501
+ }
28502
+ if (checked.state === "not-running") {
28503
+ return `The sandbox is not running: ${checked.detail}. Its output up to seq ${String(lastSeq)} is recorded. ${at}`;
28504
+ }
28505
+ return `Could not check whether the sandbox is still running (${checked.detail}). ${at}`;
28506
+ }
28507
+ async function lostStreamError(input) {
28508
+ const { lastSeq } = input;
28509
+ const detail = input.cause instanceof Error ? ` (${input.cause.message})` : "";
28510
+ const guidance = input.released ? "This was a single-command run: the server deletes the sandbox when the connection drops, so nothing is left running and there is nothing to re-attach to. Run the turn again, or use --no-stream to wait for the whole result in one reply." : lostStreamGuidance(lastSeq, input.check === undefined ? undefined : await askBounded(input.check), input.resume?.(lastSeq) ?? `mutagent sandbox attach <id> --since ${String(lastSeq)}`);
28511
+ return new MutagentError("STREAM_DISCONNECTED", `Lost the sandbox stream at seq ${String(lastSeq)} and could not reconnect${detail}.`, guidance);
28470
28512
  }
28471
28513
 
28472
28514
  // src/lib/sandbox-stream.ts
28473
- var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 500 };
28474
- var MAX_TOTAL_RECONNECTS = 12;
28515
+ var DEFAULT_RETRY = {
28516
+ attempts: 40,
28517
+ baseDelayMs: 500,
28518
+ maxDelayMs: OUTAGE_BACKOFF_CAP_MS,
28519
+ windowMs: OUTAGE_WINDOW_MS
28520
+ };
28521
+ var MAX_TOTAL_RECONNECTS = 40;
28475
28522
  async function runStreamSession(options) {
28476
28523
  const retry = options.retry ?? DEFAULT_RETRY;
28477
28524
  const sleep = options.sleep ?? delay;
@@ -28484,6 +28531,7 @@ async function runStreamSession(options) {
28484
28531
  let exitCode = null;
28485
28532
  let reconnects = 0;
28486
28533
  let errorReconnects = 0;
28534
+ let outageStartedAt = 0;
28487
28535
  let gaps = 0;
28488
28536
  let attempt = 0;
28489
28537
  let sandbox;
@@ -28505,18 +28553,29 @@ async function runStreamSession(options) {
28505
28553
  const detached = () => outcome("detached");
28506
28554
  const isAborted = () => signal.aborted;
28507
28555
  const withinTotalBudget = () => errorReconnects < MAX_TOTAL_RECONNECTS;
28508
- const retryAfterError = async (error) => {
28556
+ const retryAfterError = async (error, progressed) => {
28557
+ if (progressed)
28558
+ errorReconnects = 0;
28509
28559
  attempt += 1;
28510
- if (attempt > retry.attempts || !withinTotalBudget()) {
28560
+ if (attempt === 1)
28561
+ outageStartedAt = now();
28562
+ const outageOver = retry.windowMs !== undefined && attempt > 1 && now() - outageStartedAt >= retry.windowMs;
28563
+ if (attempt > retry.attempts || outageOver || !withinTotalBudget()) {
28511
28564
  if (live !== undefined && !live.live)
28512
28565
  sink.notice(unfollowableNotice(live.reason));
28513
- throw exhausted(lastSeq, error, onDisconnect);
28566
+ throw await lostStreamError({
28567
+ lastSeq,
28568
+ cause: error,
28569
+ released: onDisconnect === "released",
28570
+ ...options.checkSandbox === undefined ? {} : { check: options.checkSandbox },
28571
+ ...options.resumeCommand === undefined ? {} : { resume: options.resumeCommand }
28572
+ });
28514
28573
  }
28515
28574
  reconnects += 1;
28516
28575
  errorReconnects += 1;
28517
28576
  if (attempt > 1)
28518
28577
  sink.notice(retryFailedNotice(attempt - 1, retry.attempts, lastSeq));
28519
- await sleep(backoffMs(retry.baseDelayMs, attempt), signal);
28578
+ await sleep(outageBackoffMs(retry.baseDelayMs, attempt, retry.maxDelayMs), signal);
28520
28579
  };
28521
28580
  const alreadyDelivered = (seq) => seq <= floor;
28522
28581
  const noteSeq = (seq) => {
@@ -28539,7 +28598,7 @@ async function runStreamSession(options) {
28539
28598
  return detached();
28540
28599
  if (reconnects === 0 && attempt === 0)
28541
28600
  throw error;
28542
- await retryAfterError(error);
28601
+ await retryAfterError(error, false);
28543
28602
  continue;
28544
28603
  }
28545
28604
  let exited = false;
@@ -28671,7 +28730,7 @@ async function runStreamSession(options) {
28671
28730
  } catch (error) {
28672
28731
  if (isAborted())
28673
28732
  return detached();
28674
- await retryAfterError(error);
28733
+ await retryAfterError(error, lastSeq > seqAtOpen);
28675
28734
  continue;
28676
28735
  }
28677
28736
  if (exited)
@@ -28690,9 +28749,6 @@ async function runStreamSession(options) {
28690
28749
  await sleep(wait, signal);
28691
28750
  }
28692
28751
  }
28693
- function backoffMs(base, attempt) {
28694
- return base * 2 ** (attempt - 1);
28695
- }
28696
28752
  function unfollowableNotice(reason) {
28697
28753
  const why = reason === "" ? "" : ` (${reason})`;
28698
28754
  return `The backend cannot follow this sandbox from the instance that answered${why}. ` + "Everything already recorded has been replayed above, and this stream will end there rather " + "than continuing live — the sandbox itself is unaffected and may still be running. " + "Retry later, or attach from the instance that owns it.";
@@ -28701,11 +28757,6 @@ function gapNotice(lastSeq, nextSeq) {
28701
28757
  const missing = nextSeq - lastSeq - 1;
28702
28758
  return `Output gap: ${String(missing)} event(s) between seq ${String(lastSeq)} and ` + `${String(nextSeq)} are unavailable — the server buffer rolled past the resume point. ` + "What follows is not continuous with what came before.";
28703
28759
  }
28704
- function exhausted(lastSeq, cause, onDisconnect) {
28705
- const detail = cause instanceof Error ? ` (${cause.message})` : "";
28706
- const guidance = onDisconnect === "released" ? "This was a single-command run: the server deletes the sandbox when the connection drops, so nothing is left running and there is nothing to re-attach to. Run the turn again, or use --no-stream to wait for the whole result in one reply." : `The sandbox is still running — nothing was destroyed. Resume with: mutagent sandbox attach <id> --since ${String(lastSeq)}`;
28707
- return new MutagentError("STREAM_DISCONNECTED", `Lost the sandbox stream and could not reconnect${detail}.`, guidance);
28708
- }
28709
28760
  function isGapError(payload) {
28710
28761
  return payload.code === "gap";
28711
28762
  }
@@ -28900,6 +28951,8 @@ async function runNativeSession(options) {
28900
28951
  notice
28901
28952
  },
28902
28953
  filtered: true,
28954
+ resumeCommand: (seq) => activeProduct().cli + " session attach " + options.reference + " --since " + String(seq),
28955
+ ...options.check === undefined ? {} : { checkSandbox: options.check },
28903
28956
  ...options.sessionId === undefined ? {} : { sessionId: options.sessionId },
28904
28957
  ...options.since === undefined ? {} : { since: options.since },
28905
28958
  signal: controller.signal
@@ -28942,6 +28995,36 @@ async function runNativeSession(options) {
28942
28995
  }
28943
28996
  }
28944
28997
 
28998
+ // src/lib/session-durability.ts
28999
+ async function checkSession(reference, findRow) {
29000
+ let row;
29001
+ try {
29002
+ row = await findRow(reference);
29003
+ } catch (error) {
29004
+ return { state: "unknown", detail: error instanceof Error ? error.message : String(error) };
29005
+ }
29006
+ if (row === undefined)
29007
+ return { state: "unknown", detail: "the session list does not show it" };
29008
+ switch (row.status) {
29009
+ case "live":
29010
+ case "restored":
29011
+ return row.idleStopHeld === undefined ? { state: "running" } : { state: "running", detail: `kept by the idle stop: ${row.idleStopHeld.reason}` };
29012
+ case "stopped-for-idling":
29013
+ return {
29014
+ state: "not-running",
29015
+ detail: "the session stopped for idling; a send wakes it from its checkpoint"
29016
+ };
29017
+ case "ended":
29018
+ return {
29019
+ state: "not-running",
29020
+ detail: `the session ended${row.exitCode === undefined || row.exitCode === null ? "" : ` (exit code ${String(row.exitCode)})`}`
29021
+ };
29022
+ }
29023
+ }
29024
+ function listedRow(routes) {
29025
+ return async (reference) => (await listHarnessSessions(routes)).sessions.find((row) => row.reference === reference);
29026
+ }
29027
+
28945
29028
  // src/commands/cloud-session/run-cloud.ts
28946
29029
  function repositoryFields(flags) {
28947
29030
  return {
@@ -29055,7 +29138,8 @@ async function attachLaunched(session, rpc, interruptRequested) {
29055
29138
  send: (line, signal) => sendSessionInput(routes, reference, line, signal),
29056
29139
  closeInput: (signal) => closeSessionInput(routes, reference, signal),
29057
29140
  interrupt: (signal) => signalSession(routes, reference, "SIGINT", signal),
29058
- interruptRequested
29141
+ interruptRequested,
29142
+ check: () => checkSession(reference, listedRow(routes))
29059
29143
  }).catch((error) => {
29060
29144
  throw markStreamStarted(error);
29061
29145
  });
@@ -30015,6 +30099,23 @@ function formatDuration(ms) {
30015
30099
  }
30016
30100
 
30017
30101
  // src/lib/sandbox-session.ts
30102
+ async function sandboxCheck(id) {
30103
+ let status;
30104
+ try {
30105
+ ({ status } = await inspectSandbox(id));
30106
+ } catch (error) {
30107
+ if (error instanceof MutagentError && error.code === "NOT_FOUND") {
30108
+ return { state: "not-running", detail: "it no longer exists" };
30109
+ }
30110
+ throw error;
30111
+ }
30112
+ if (status === undefined)
30113
+ return { state: "unknown", detail: "the server reported no status" };
30114
+ if (status === "running" || status === "starting" || status === "pending" || status === "paused") {
30115
+ return { state: "running", detail: status };
30116
+ }
30117
+ return { state: "not-running", detail: `it is ${status}` };
30118
+ }
30018
30119
  function dim(text) {
30019
30120
  return isPlainOutput() ? text : chalk24.dim(text);
30020
30121
  }
@@ -30122,7 +30223,11 @@ async function attachToSandbox(options) {
30122
30223
  signal: controller.signal,
30123
30224
  ...options.retry === undefined ? {} : { retry: options.retry },
30124
30225
  ...options.onDisconnect === undefined ? {} : { onDisconnect: options.onDisconnect },
30125
- ...options.onSandbox === undefined ? {} : { onSandbox: options.onSandbox }
30226
+ ...options.onSandbox === undefined ? {} : { onSandbox: options.onSandbox },
30227
+ ...options.id === "" ? {} : {
30228
+ checkSandbox: () => sandboxCheck(options.id),
30229
+ resumeCommand: (seq) => reattachHint(options.id, seq)
30230
+ }
30126
30231
  });
30127
30232
  reportOutcome(options.id, result, sink, options.onDisconnect ?? "survives");
30128
30233
  return result;
@@ -30148,7 +30253,7 @@ function reportOutcome(id, result, sink, onDisconnect) {
30148
30253
  sink.notice("Run the turn again to retry, or add --keep next time to hold the sandbox open.");
30149
30254
  return;
30150
30255
  }
30151
- sink.notice(`Detached. Sandbox ${sandboxId} is still running — nothing was destroyed.`);
30256
+ sink.notice(`Detached at seq ${String(result.lastSeq)}: this CLI stopped watching and did not stop sandbox ${sandboxId}.`);
30152
30257
  sink.notice(`Re-attach with: ${reattachHint(sandboxId, result.lastSeq)}`);
30153
30258
  sink.notice(`Destroy it with: mutagent sandbox delete ${sandboxId} --force`);
30154
30259
  return;
@@ -30973,6 +31078,7 @@ function registerSessionAttach(session, p) {
30973
31078
  "AI Agent Directive:",
30974
31079
  " Output is Helix's own text or JSON lines on stdout; CLI messages go to stderr. --json does not change it.",
30975
31080
  " Ctrl-C stops watching without signalling the session.",
31081
+ " A dropped stream reconnects at its cursor for up to 5 minutes; if it gives up, it prints the seq to resume from.",
30976
31082
  " A reference names one session; after a restore, take the new reference from session list.",
30977
31083
  ' When a send wakes the session, stderr carries [mutagent] {"event":"woken","reference":"hs1_..."}:',
30978
31084
  " this stream ends there; attach to that reference with --since 0."
@@ -30980,9 +31086,11 @@ function registerSessionAttach(session, p) {
30980
31086
  `))).action(async (reference, options) => {
30981
31087
  try {
30982
31088
  const since = parseSince(options.since);
31089
+ const findRow = listedRow(p);
30983
31090
  await runNativeSession({
30984
31091
  reference,
30985
31092
  rpc: false,
31093
+ check: () => checkSession(reference, findRow),
30986
31094
  ...since === undefined ? {} : { since },
30987
31095
  open: (request) => openSessionStream(p, reference, request),
30988
31096
  send: (line, signal) => sendSessionInput(p, reference, line, signal)
@@ -44673,4 +44781,4 @@ if (process.env.CLI_VERSION) {
44673
44781
  }
44674
44782
  runCli(process.argv.slice(2), cliVersion);
44675
44783
 
44676
- //# debugId=B287580A222D152F64756E2164756E21
44784
+ //# debugId=E0552D781753FD4964756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutagent/cli",
3
- "version": "0.2.207",
3
+ "version": "0.2.209",
4
4
  "description": "Bun-native CLI for the MutagenT AI platform - auth, provider config, and lifecycle-tool installer for AI-native workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@inquirer/prompts": "^8.7.2",
48
- "@mutagent/sdk": "0.12.20",
48
+ "@mutagent/sdk": "0.12.21",
49
49
  "@sinclair/typebox": "^0.34.0",
50
50
  "chalk": "^5.3.0",
51
51
  "commander": "^12.1.0",
@@ -94,5 +94,5 @@
94
94
  "bugs": {
95
95
  "url": "https://docs.mutagent.io/support"
96
96
  },
97
- "gitHead": "8ed68aa4195126d5a414d4b326883a8433ee06cb"
97
+ "gitHead": "14b9f48b84e32ae4f13253bad2d4a365cb03107d"
98
98
  }