@tablation/crew 0.100.1 → 0.100.3

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 +69 -8
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -18451,6 +18451,16 @@ function describeRelayProbe(url, p) {
18451
18451
  return p.ok ? `relay control: OK ${url}/control/status (${p.status}, ${p.latencyMs} ms)` : `relay control: FAILED ${url}/control/status \u2014 ${p.error}`;
18452
18452
  }
18453
18453
 
18454
+ // src/ship-status.ts
18455
+ function shipIdentityJson(s) {
18456
+ return {
18457
+ id: s.id,
18458
+ kind: s.kind ?? null,
18459
+ instanceClass: s.instance_class ?? null,
18460
+ relayUrl: s.relay_url ?? null
18461
+ };
18462
+ }
18463
+
18454
18464
  // src/passenger-containers.ts
18455
18465
  import { execFileSync as execFileSync9 } from "child_process";
18456
18466
  import { createHash as createHash7 } from "crypto";
@@ -19188,6 +19198,7 @@ import { randomBytes as randomBytes2 } from "crypto";
19188
19198
  var DEFAULT_FLOOR_MS = 5e3;
19189
19199
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
19190
19200
  var ALLOWANCE_PAUSE_MS = 5 * 60 * 1e3;
19201
+ var RELOAD_WARN_MS = 10 * 60 * 1e3;
19191
19202
  var defaultSleep = (ms3) => new Promise((resolve7) => setTimeout(resolve7, ms3));
19192
19203
  async function runDaemonLoop(o) {
19193
19204
  const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
@@ -19198,6 +19209,20 @@ async function runDaemonLoop(o) {
19198
19209
  const now = o.now ?? Date.now;
19199
19210
  let transientRun = 0;
19200
19211
  let downSince = 0;
19212
+ const staleStop = async () => {
19213
+ const since2 = now();
19214
+ let warned = false;
19215
+ while (o.inFlight && o.inFlight.size > 0) {
19216
+ if (o.signal?.aborted) return false;
19217
+ if (!warned || now() - since2 >= RELOAD_WARN_MS) {
19218
+ o.onStaleDeferred?.({ runs: o.inFlight.size, waitedMs: now() - since2 });
19219
+ warned = true;
19220
+ }
19221
+ const drained = Promise.all([...o.inFlight].map((p) => p.catch(() => void 0))).then(() => void 0);
19222
+ await Promise.race([drained, sleep(RELOAD_WARN_MS)]);
19223
+ }
19224
+ return true;
19225
+ };
19201
19226
  for (; ; ) {
19202
19227
  if (o.signal?.aborted) return "aborted";
19203
19228
  let result;
@@ -19234,12 +19259,13 @@ async function runDaemonLoop(o) {
19234
19259
  backoff = floor;
19235
19260
  o.onPass?.(result, 0);
19236
19261
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
19262
+ if (o.checkStale?.() && await staleStop()) return "stale";
19237
19263
  continue;
19238
19264
  }
19239
19265
  o.onPass?.(result, backoff);
19240
19266
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
19241
19267
  if (o.signal?.aborted) return "aborted";
19242
- if (o.checkStale?.()) return "stale";
19268
+ if (o.checkStale?.() && await staleStop()) return "stale";
19243
19269
  await sleep(backoff);
19244
19270
  backoff = Math.min(backoff * 2, ceiling);
19245
19271
  }
@@ -19269,6 +19295,13 @@ function makeStaleChecker(paths, opts = {}) {
19269
19295
  function reloadSentinelFor(stateDir) {
19270
19296
  return join42(stateDir, "reload-requested");
19271
19297
  }
19298
+ function readReloadRequestedAt(stateDir) {
19299
+ try {
19300
+ return statSync12(reloadSentinelFor(stateDir)).mtimeMs;
19301
+ } catch {
19302
+ return null;
19303
+ }
19304
+ }
19272
19305
  function daemonStampFor(stateDir) {
19273
19306
  return join42(stateDir, "daemon.started");
19274
19307
  }
@@ -19443,12 +19476,12 @@ async function runOnePassUnguarded(o) {
19443
19476
  live.delete(role);
19444
19477
  continue;
19445
19478
  }
19446
- started.push(role);
19447
- let ready = () => {
19479
+ let settle = () => {
19448
19480
  };
19449
19481
  const settled = new Promise((resolve7) => {
19450
- ready = resolve7;
19482
+ settle = resolve7;
19451
19483
  });
19484
+ const ready = (tookTicket2) => settle(tookTicket2);
19452
19485
  const run2 = spawn9(role, decision, ctx, { ready }).then(() => {
19453
19486
  breaker.recordSuccess(role);
19454
19487
  clearShipAttention(o.state, `role_parked:${role}`);
@@ -19494,18 +19527,20 @@ async function runOnePassUnguarded(o) {
19494
19527
  }
19495
19528
  }).finally(() => {
19496
19529
  lock.release();
19497
- ready(false);
19530
+ settle(void 0);
19498
19531
  });
19499
19532
  if (o.inFlight) {
19500
19533
  const set = o.inFlight;
19501
19534
  set.add(run2);
19502
19535
  void run2.finally(() => set.delete(run2));
19503
19536
  }
19537
+ const reported = TICKET_ROLES.has(role) ? await settled : true;
19538
+ const tookTicket = reported !== false;
19539
+ if (tookTicket) started.push(role);
19504
19540
  if (roleCap(role) <= 1) {
19505
19541
  live.delete(role);
19506
19542
  continue;
19507
19543
  }
19508
- const tookTicket = await settled;
19509
19544
  if (tookTicket && count + 1 < roleCap(role)) live.set(role, count + 1);
19510
19545
  else live.delete(role);
19511
19546
  }
@@ -19735,6 +19770,8 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
19735
19770
  );
19736
19771
  return;
19737
19772
  }
19773
+ } else {
19774
+ hooks?.ready(true);
19738
19775
  }
19739
19776
  }
19740
19777
  const plan = planAgentRun({
@@ -21238,6 +21275,7 @@ function findSyncRoute(c, name) {
21238
21275
  function daemonConfigPaths(c) {
21239
21276
  const files = [c.configFile];
21240
21277
  for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
21278
+ for (const r of c.routes) files.push(resolvedPathFor(c.ship.stateDir, r.route));
21241
21279
  return files;
21242
21280
  }
21243
21281
  var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "provision", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
@@ -22975,8 +23013,14 @@ ${cu.tail.trimEnd()}
22975
23013
  log: (m) => emit.warn(m)
22976
23014
  });
22977
23015
  if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
23016
+ const inFlight = /* @__PURE__ */ new Set();
22978
23017
  const stopReason = await runDaemonLoop({
23018
+ inFlight,
23019
+ onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
23020
+ `reload or update pending for ${Math.round(waitedMs / 1e3)}s \u2014 waiting for ${runs} agent run(s) to end before relaunching (\`crew reload --now\` forces it)`
23021
+ ),
22979
23022
  runPass: () => runOnePass({
23023
+ inFlight,
22980
23024
  route: route2,
22981
23025
  ship: cfg.ship,
22982
23026
  state,
@@ -25365,7 +25409,9 @@ ${where.join("\n")}`);
25365
25409
  mkdirSync28(cfg.ship.stateDir, { recursive: true });
25366
25410
  writeFileSync30(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
25367
25411
  `);
25368
- process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
25412
+ process.stdout.write(
25413
+ "crew reload: requested \u2014 the daemon relaunches with the new config once its running agents end (it starts no new work meanwhile); if it is busy, `crew reload --now` forces the relaunch and interrupts them\n"
25414
+ );
25369
25415
  if (flag("now") && control?.stopCommand && control.startCommand) {
25370
25416
  for (const cmd of [control.stopCommand, control.startCommand]) {
25371
25417
  const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
@@ -25699,7 +25745,9 @@ ${where.join("\n")}`);
25699
25745
  rowName: nameSync.rowName ?? null,
25700
25746
  nameDrift: nameSync.drift,
25701
25747
  // Additive (CREW-2803): the relay URL this route's tunnel is reachable at, or null.
25702
- relayUrl: routeRelayUrl(route2)
25748
+ relayUrl: routeRelayUrl(route2),
25749
+ // Additive (CREW-2887): when a `crew reload` request was written and not yet honored, else null.
25750
+ reloadRequestedAt: ((at) => at === null ? null : new Date(at).toISOString())(readReloadRequestedAt(cfg.ship.stateDir))
25703
25751
  },
25704
25752
  // Additive (CREW-1402): what the operator owes. `version` stays 1.
25705
25753
  todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
@@ -25763,6 +25811,7 @@ ${where.join("\n")}`);
25763
25811
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
25764
25812
  },
25765
25813
  ships: shipRows.map((s) => ({
25814
+ ...shipIdentityJson(s),
25766
25815
  name: s.name ?? null,
25767
25816
  lastSeen: s.last_seen ?? null,
25768
25817
  engaged: !!s.engaged,
@@ -25798,6 +25847,11 @@ watermark: ${state.watermark()}
25798
25847
  );
25799
25848
  for (const r of ["dev", "design", "qa"]) {
25800
25849
  if (state.isRolePaused(r)) process.stdout.write(`role ${r}: paused
25850
+ `);
25851
+ }
25852
+ const reloadWaitingAt = readReloadRequestedAt(cfg.ship.stateDir);
25853
+ if (reloadWaitingAt !== null) {
25854
+ process.stdout.write(`reload: requested ${Math.max(0, Math.round((Date.now() - reloadWaitingAt) / 1e3))}s ago, not yet honored \u2014 \`crew reload --now\` forces it
25801
25855
  `);
25802
25856
  }
25803
25857
  if (projects.length) process.stdout.write(`projects: ${projects.map((p) => p.label).join(", ")}
@@ -25970,6 +26024,13 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
25970
26024
  );
25971
26025
  }
25972
26026
  }
26027
+ const reloadAt = readReloadRequestedAt(cfg.ship.stateDir);
26028
+ if (reloadAt !== null) {
26029
+ process.stdout.write(
26030
+ `reload request: WARN pending for ${Math.max(0, Math.round((Date.now() - reloadAt) / 1e3))}s \u2014 the daemon is waiting for its running agents to end; \`crew reload --now\` forces it
26031
+ `
26032
+ );
26033
+ }
25973
26034
  const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
25974
26035
  if (daemonStartedAt !== null) {
25975
26036
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.100.1",
3
+ "version": "0.100.3",
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",