@tablation/crew 0.69.1 → 0.70.0

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/cli.js +68 -0
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15265,6 +15265,37 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
15265
15265
  return endpoints;
15266
15266
  }
15267
15267
 
15268
+ // src/outage.ts
15269
+ var TRANSIENT_BACKOFF_MS = [15e3, 3e4, 6e4];
15270
+ var OUTAGE_ESCALATE_AFTER = 5;
15271
+ var TRACKER_OUTAGE_KEY = "tracker_unreachable";
15272
+ var NETWORK_CODES = ["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "EAI_AGAIN"];
15273
+ function transientBackoffMs(n) {
15274
+ return TRANSIENT_BACKOFF_MS[Math.min(Math.max(n, 1), TRANSIENT_BACKOFF_MS.length) - 1];
15275
+ }
15276
+ function codeOf(e) {
15277
+ const code = e?.code;
15278
+ return typeof code === "string" ? code : void 0;
15279
+ }
15280
+ function isTransientError(err) {
15281
+ if (!(err instanceof Error)) return false;
15282
+ const msg = err.message;
15283
+ if (msg === "fetch failed" || msg.startsWith("fetch failed")) return true;
15284
+ for (let e = err, depth = 0; e && depth < 4; e = e.cause, depth++) {
15285
+ const code = codeOf(e);
15286
+ if (code && NETWORK_CODES.includes(code)) return true;
15287
+ }
15288
+ if (NETWORK_CODES.some((c) => msg.includes(c))) return true;
15289
+ if (/\b(?:HTTP|status)\s*:?\s*(502|503|504)\b/i.test(msg) || /^(502|503|504)\b/.test(msg)) return true;
15290
+ if (err instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
15291
+ return false;
15292
+ }
15293
+ function describeCause(err) {
15294
+ const first = (err instanceof Error ? err.message : String(err)).split("\n")[0];
15295
+ const code = codeOf(err?.cause);
15296
+ return code && !first.includes(code) ? `${first} (${code})` : first;
15297
+ }
15298
+
15268
15299
  // src/daemon.ts
15269
15300
  import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync26, readdirSync as readdirSync13, rmSync as rmSync13, statSync as statSync9, writeFileSync as writeFileSync20 } from "fs";
15270
15301
  import { join as join33 } from "path";
@@ -15364,12 +15395,32 @@ async function runDaemonLoop(o) {
15364
15395
  const sleep = o.sleep ?? defaultSleep;
15365
15396
  let backoff = floor;
15366
15397
  let passes = 0;
15398
+ const now = o.now ?? Date.now;
15399
+ let transientRun = 0;
15400
+ let downSince = 0;
15367
15401
  for (; ; ) {
15368
15402
  if (o.signal?.aborted) return "aborted";
15369
15403
  let result;
15370
15404
  try {
15371
15405
  result = await o.runPass();
15406
+ if (transientRun > 0) {
15407
+ o.onRecovered?.({ downMs: now() - downSince });
15408
+ transientRun = 0;
15409
+ }
15372
15410
  } catch (err) {
15411
+ if (isTransientError(err)) {
15412
+ passes++;
15413
+ if (transientRun === 0) downSince = now();
15414
+ transientRun++;
15415
+ const wait = transientBackoffMs(transientRun);
15416
+ const downMs = now() - downSince;
15417
+ o.onTransient?.({ err, consecutive: transientRun, backoffMs: wait, downMs });
15418
+ if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err, consecutive: transientRun, downMs });
15419
+ if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
15420
+ if (o.signal?.aborted) return "aborted";
15421
+ await sleep(wait);
15422
+ continue;
15423
+ }
15373
15424
  passes++;
15374
15425
  o.onPassError?.(err);
15375
15426
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
@@ -17466,6 +17517,23 @@ ${changed2.map((c) => ` - ${c}
17466
17517
  `)
17467
17518
  })
17468
17519
  }),
17520
+ onTransient: ({ err, consecutive, backoffMs }) => emit.warn(
17521
+ `tracker unavailable (${describeCause(err)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
17522
+ ),
17523
+ onOutage: ({ err, downMs }) => {
17524
+ const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err)}`;
17525
+ emit.error(msg);
17526
+ void raiseShipAttention({ state, emit, route, ship: cfg.ship }, {
17527
+ kind: "tracker_unreachable",
17528
+ key: TRACKER_OUTAGE_KEY,
17529
+ message: msg,
17530
+ since: (/* @__PURE__ */ new Date()).toISOString()
17531
+ });
17532
+ },
17533
+ onRecovered: ({ downMs }) => {
17534
+ clearShipAttention(state, TRACKER_OUTAGE_KEY);
17535
+ emit.emit(`tracker is back after ${Math.max(1, Math.round(downMs / 1e3))}s`);
17536
+ },
17469
17537
  onPassError: (err) => emit.error(
17470
17538
  `daemon pass failed, continuing: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
17471
17539
  ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.69.1",
3
+ "version": "0.70.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",