@tablation/crew 0.100.1 → 0.100.2

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 +60 -3
  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
  }
@@ -21238,6 +21271,7 @@ function findSyncRoute(c, name) {
21238
21271
  function daemonConfigPaths(c) {
21239
21272
  const files = [c.configFile];
21240
21273
  for (const r of c.routes) if (r.apiKeyFile && !files.includes(r.apiKeyFile)) files.push(r.apiKeyFile);
21274
+ for (const r of c.routes) files.push(resolvedPathFor(c.ship.stateDir, r.route));
21241
21275
  return files;
21242
21276
  }
21243
21277
  var STATE_WRITING_COMMANDS = /* @__PURE__ */ new Set(["connect", "provision", "disconnect", "install", "uninstall", "reload", "release", "merge", "deploy"]);
@@ -22975,8 +23009,14 @@ ${cu.tail.trimEnd()}
22975
23009
  log: (m) => emit.warn(m)
22976
23010
  });
22977
23011
  if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
23012
+ const inFlight = /* @__PURE__ */ new Set();
22978
23013
  const stopReason = await runDaemonLoop({
23014
+ inFlight,
23015
+ onStaleDeferred: ({ runs, waitedMs }) => emit.warn(
23016
+ `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)`
23017
+ ),
22979
23018
  runPass: () => runOnePass({
23019
+ inFlight,
22980
23020
  route: route2,
22981
23021
  ship: cfg.ship,
22982
23022
  state,
@@ -25365,7 +25405,9 @@ ${where.join("\n")}`);
25365
25405
  mkdirSync28(cfg.ship.stateDir, { recursive: true });
25366
25406
  writeFileSync30(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
25367
25407
  `);
25368
- process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
25408
+ process.stdout.write(
25409
+ "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"
25410
+ );
25369
25411
  if (flag("now") && control?.stopCommand && control.startCommand) {
25370
25412
  for (const cmd of [control.stopCommand, control.startCommand]) {
25371
25413
  const res = spawnSync8(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
@@ -25699,7 +25741,9 @@ ${where.join("\n")}`);
25699
25741
  rowName: nameSync.rowName ?? null,
25700
25742
  nameDrift: nameSync.drift,
25701
25743
  // Additive (CREW-2803): the relay URL this route's tunnel is reachable at, or null.
25702
- relayUrl: routeRelayUrl(route2)
25744
+ relayUrl: routeRelayUrl(route2),
25745
+ // Additive (CREW-2887): when a `crew reload` request was written and not yet honored, else null.
25746
+ reloadRequestedAt: ((at) => at === null ? null : new Date(at).toISOString())(readReloadRequestedAt(cfg.ship.stateDir))
25703
25747
  },
25704
25748
  // Additive (CREW-1402): what the operator owes. `version` stays 1.
25705
25749
  todo: todo.map((i) => ({ ticket: displayKey(i.ticket), title: i.ticket.title ?? null, reasons: i.reasons })),
@@ -25763,6 +25807,7 @@ ${where.join("\n")}`);
25763
25807
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
25764
25808
  },
25765
25809
  ships: shipRows.map((s) => ({
25810
+ ...shipIdentityJson(s),
25766
25811
  name: s.name ?? null,
25767
25812
  lastSeen: s.last_seen ?? null,
25768
25813
  engaged: !!s.engaged,
@@ -25798,6 +25843,11 @@ watermark: ${state.watermark()}
25798
25843
  );
25799
25844
  for (const r of ["dev", "design", "qa"]) {
25800
25845
  if (state.isRolePaused(r)) process.stdout.write(`role ${r}: paused
25846
+ `);
25847
+ }
25848
+ const reloadWaitingAt = readReloadRequestedAt(cfg.ship.stateDir);
25849
+ if (reloadWaitingAt !== null) {
25850
+ 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
25851
  `);
25802
25852
  }
25803
25853
  if (projects.length) process.stdout.write(`projects: ${projects.map((p) => p.label).join(", ")}
@@ -25970,6 +26020,13 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
25970
26020
  );
25971
26021
  }
25972
26022
  }
26023
+ const reloadAt = readReloadRequestedAt(cfg.ship.stateDir);
26024
+ if (reloadAt !== null) {
26025
+ process.stdout.write(
26026
+ `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
26027
+ `
26028
+ );
26029
+ }
25973
26030
  const daemonStartedAt = readDaemonStarted(cfg.ship.stateDir);
25974
26031
  if (daemonStartedAt !== null) {
25975
26032
  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.2",
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",