dahrk-node 0.1.16 → 0.1.18

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/main.js +142 -25
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -902,6 +902,10 @@ function mapUsage2(u) {
902
902
 
903
903
  // ../../packages/executor-worktree/src/codex-adapter.ts
904
904
  var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
905
+ var CODEX_COST_UNAVAILABLE_NOTE = "codex-adapter: cost reporting unavailable for the Codex runtime (the SDK reports tokens, not price); costUsd left unset, not $0\n";
906
+ function warnCostUnavailable(write = (s) => void process.stderr.write(s)) {
907
+ write(CODEX_COST_UNAVAILABLE_NOTE);
908
+ }
905
909
  function runtimeEnvOptions2(ctx) {
906
910
  if (!ctx.runtimeEnv) return {};
907
911
  const env = {};
@@ -961,6 +965,7 @@ function createCodexRunner() {
961
965
  }
962
966
  captureThreadId(t);
963
967
  if (cancelled) status = "fail";
968
+ warnCostUnavailable();
964
969
  return { status, ...sessionId ? { sessionId } : {} };
965
970
  },
966
971
  async runInteractive(ctx, turns, onTrace) {
@@ -1031,6 +1036,7 @@ function createCodexRunner() {
1031
1036
  summary = "(stage cancelled)";
1032
1037
  }
1033
1038
  captureThreadId(t);
1039
+ warnCostUnavailable();
1034
1040
  return { status, summary, ...sessionId ? { sessionId } : {} };
1035
1041
  },
1036
1042
  async summarise(ctx) {
@@ -1186,6 +1192,10 @@ function createPiRunner(deps = {}) {
1186
1192
  const captureSessionId = (s) => {
1187
1193
  if (s.sessionId) sessionId = s.sessionId;
1188
1194
  };
1195
+ const readSessionCost = (s) => {
1196
+ const cost = s.getSessionStats?.()?.cost;
1197
+ return typeof cost === "number" ? cost : void 0;
1198
+ };
1189
1199
  return {
1190
1200
  runtime: "pi",
1191
1201
  async runBatch(ctx, onTrace) {
@@ -1209,7 +1219,8 @@ function createPiRunner(deps = {}) {
1209
1219
  }
1210
1220
  captureSessionId(s);
1211
1221
  if (cancelled) status = "fail";
1212
- return { status, ...sessionId ? { sessionId } : {} };
1222
+ const costUsd = readSessionCost(s);
1223
+ return { status, ...sessionId ? { sessionId } : {}, ...costUsd !== void 0 ? { costUsd } : {} };
1213
1224
  },
1214
1225
  async runInteractive(ctx, turns, onTrace) {
1215
1226
  const emit = makeEmit("pi", onTrace);
@@ -1304,7 +1315,13 @@ function createPiRunner(deps = {}) {
1304
1315
  }
1305
1316
  unsub();
1306
1317
  captureSessionId(s);
1307
- return { status, summary, ...sessionId ? { sessionId } : {} };
1318
+ const costUsd = readSessionCost(s);
1319
+ return {
1320
+ status,
1321
+ summary,
1322
+ ...sessionId ? { sessionId } : {},
1323
+ ...costUsd !== void 0 ? { costUsd } : {}
1324
+ };
1308
1325
  },
1309
1326
  async summarise(ctx) {
1310
1327
  if (!session) return "(no summary: session not established)";
@@ -3867,6 +3884,9 @@ function createStageRunner(deps) {
3867
3884
 
3868
3885
  // ../../packages/edge/src/ws-client.ts
3869
3886
  var ENROLMENT_REJECTED_EXIT_CODE = 78;
3887
+ var RECONNECT_BASE_MS = 500;
3888
+ var RECONNECT_MAX_MS = 3e4;
3889
+ var PARK_POLL_MS = 6e4;
3870
3890
  function classifyError(e) {
3871
3891
  const msg = e instanceof Error ? e.message : String(e);
3872
3892
  if (/\bgit\b|worktree|clone|fetch|branch|checkout|remote/i.test(msg)) return "git";
@@ -3902,7 +3922,11 @@ async function startEdgeNode(opts) {
3902
3922
  let lastPongAt = 0;
3903
3923
  let shuttingDown = false;
3904
3924
  let connectCount = 0;
3925
+ let enrolToken = opts.enrolToken;
3926
+ let reconnectAttempts = 0;
3927
+ let parkTimer;
3905
3928
  let onFatal;
3929
+ let onParked;
3906
3930
  const send = (msg) => {
3907
3931
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
3908
3932
  };
@@ -4025,7 +4049,7 @@ async function startEdgeNode(opts) {
4025
4049
  const sendHello = () => {
4026
4050
  send({
4027
4051
  type: "hello",
4028
- enrolToken: opts.enrolToken ?? "",
4052
+ enrolToken: enrolToken ?? "",
4029
4053
  detectedRuntimes: currentRuntimes,
4030
4054
  servesRepoIds: opts.servesRepoIds ?? [],
4031
4055
  ...opts.credentialModeExplicit && opts.credentialMode ? { credentialMode: opts.credentialMode } : {},
@@ -4085,6 +4109,7 @@ async function startEdgeNode(opts) {
4085
4109
  const msg = decode(raw);
4086
4110
  if (msg.type === "welcome") {
4087
4111
  stageDeps.tenantId = msg.tenantId;
4112
+ reconnectAttempts = 0;
4088
4113
  if (opts.retention === void 0 && msg.retention) stageDeps.retention = msg.retention;
4089
4114
  if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
4090
4115
  startHeartbeat(ws, msg.heartbeatMs);
@@ -4269,19 +4294,59 @@ async function startEdgeNode(opts) {
4269
4294
  if (heartbeat) clearInterval(heartbeat);
4270
4295
  heartbeat = void 0;
4271
4296
  if (isEnrolmentRejection(code)) {
4272
- shuttingDown = true;
4273
4297
  const detail = reason.toString() || "enrolment rejected";
4274
- log.error({ closeCode: code, detail, fatal: true }, `EDGE_REJECTED:${code} ${detail}`);
4275
- process.exitCode = ENROLMENT_REJECTED_EXIT_CODE;
4276
- onFatal?.(new Error(`hub rejected edge enrolment (${code}): ${detail}`));
4298
+ const refresh = opts.refreshEnrolToken;
4299
+ log.error({ closeCode: code, detail, fatal: !refresh }, `EDGE_REJECTED:${code} ${detail}`);
4300
+ if (!refresh) {
4301
+ shuttingDown = true;
4302
+ process.exitCode = ENROLMENT_REJECTED_EXIT_CODE;
4303
+ onFatal?.(new Error(`hub rejected edge enrolment (${code}): ${detail}`));
4304
+ return;
4305
+ }
4306
+ park(refresh);
4277
4307
  return;
4278
4308
  }
4279
4309
  if (!shuttingDown) {
4280
- log.warn({ closeCode: code, reason: reason.toString(), connectCount }, `EDGE_DISCONNECTED:${code} reconnecting in 500ms`);
4281
- reconnectTimer = setTimeout(connect, 500);
4310
+ const delay = reconnectDelay();
4311
+ log.warn(
4312
+ { closeCode: code, reason: reason.toString(), connectCount, delayMs: delay, attempt: reconnectAttempts },
4313
+ `EDGE_DISCONNECTED:${code} reconnecting in ${delay}ms`
4314
+ );
4315
+ reconnectTimer = setTimeout(connect, delay);
4282
4316
  }
4283
4317
  });
4284
4318
  };
4319
+ const reconnectDelay = () => {
4320
+ const ceiling = Math.min(RECONNECT_BASE_MS * 2 ** reconnectAttempts, RECONNECT_MAX_MS);
4321
+ reconnectAttempts++;
4322
+ return Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
4323
+ };
4324
+ const park = (refresh) => {
4325
+ const rejected = enrolToken;
4326
+ if (parkTimer) return;
4327
+ const pollMs = opts.parkPollMs ?? PARK_POLL_MS;
4328
+ log.error(
4329
+ { pollMs },
4330
+ "EDGE_PARKED:enrolment rejected - the node is up but serving nothing. Re-enrol with `dahrk start --token <token>` (get one at https://app.dahrk.ai); it will reconnect on its own."
4331
+ );
4332
+ parkTimer = setInterval(() => {
4333
+ let fresh;
4334
+ try {
4335
+ fresh = refresh();
4336
+ } catch (e) {
4337
+ log.warn({ err: e }, `EDGE_PARK_REFRESH_FAILED ${e.message}`);
4338
+ return;
4339
+ }
4340
+ if (!fresh || fresh === rejected) return;
4341
+ if (parkTimer) clearInterval(parkTimer);
4342
+ parkTimer = void 0;
4343
+ enrolToken = fresh;
4344
+ reconnectAttempts = 0;
4345
+ log.info({}, "EDGE_UNPARKED:a new enrolment token was found, reconnecting");
4346
+ connect();
4347
+ }, pollMs);
4348
+ onParked?.();
4349
+ };
4285
4350
  opts.signal?.addEventListener(
4286
4351
  "abort",
4287
4352
  () => {
@@ -4292,6 +4357,8 @@ async function startEdgeNode(opts) {
4292
4357
  reconnectTimer = void 0;
4293
4358
  if (reprobeTimer) clearInterval(reprobeTimer);
4294
4359
  reprobeTimer = void 0;
4360
+ if (parkTimer) clearInterval(parkTimer);
4361
+ parkTimer = void 0;
4295
4362
  ws?.close(1e3, "shutting down");
4296
4363
  },
4297
4364
  { once: true }
@@ -4308,6 +4375,10 @@ async function startEdgeNode(opts) {
4308
4375
  clearInterval(t);
4309
4376
  reject(err);
4310
4377
  };
4378
+ onParked = () => {
4379
+ clearInterval(t);
4380
+ resolve3();
4381
+ };
4311
4382
  });
4312
4383
  }
4313
4384
 
@@ -5720,6 +5791,7 @@ function persistEnrolment(env, enrolment) {
5720
5791
  });
5721
5792
  }
5722
5793
  function resolveEnrolToken(env, opts = {}) {
5794
+ if (opts.supervised && !opts.ephemeral) return readPersistedToken(env) ?? env.DAHRK_ENROL_TOKEN;
5723
5795
  if (env.DAHRK_ENROL_TOKEN) return env.DAHRK_ENROL_TOKEN;
5724
5796
  if (opts.ephemeral) return void 0;
5725
5797
  return readPersistedToken(env);
@@ -5739,7 +5811,6 @@ function serviceArgv(inputs) {
5739
5811
  }
5740
5812
  function serviceEnv(inputs) {
5741
5813
  return {
5742
- DAHRK_ENROL_TOKEN: inputs.token,
5743
5814
  DAHRK_SUPERVISED: "1",
5744
5815
  ...inputs.hubUrl ? { DAHRK_HUB_URL: inputs.hubUrl } : {},
5745
5816
  ...inputs.name ? { DAHRK_NODE_NAME: inputs.name } : {},
@@ -5911,7 +5982,6 @@ async function runServiceInstall(inputs, deps = {}) {
5911
5982
  manager,
5912
5983
  nodeBin: d.nodeBin,
5913
5984
  scriptPath: d.scriptPath,
5914
- token: inputs.token,
5915
5985
  ...inputs.name ? { name: inputs.name } : {},
5916
5986
  ...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
5917
5987
  ...d.pathEnv ? { pathEnv: d.pathEnv } : {},
@@ -5979,21 +6049,19 @@ async function runNodeStart(inputs, deps = {}, opts = {}) {
5979
6049
  manager,
5980
6050
  nodeBin: d.nodeBin,
5981
6051
  scriptPath: d.scriptPath,
5982
- token: inputs.token ?? "-",
5983
6052
  ...inputs.name ? { name: inputs.name } : {},
5984
6053
  ...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
5985
6054
  ...d.pathEnv ? { pathEnv: d.pathEnv } : {},
5986
6055
  homeDir: d.homeDir,
5987
6056
  logDir: d.logDir
5988
6057
  });
5989
- const canRender = inputs.token !== void 0;
5990
- const current = canRender && unitExists && unitIsCurrent(plan, d.readFile(filePath));
6058
+ const current = unitExists && unitIsCurrent(plan, d.readFile(filePath));
5991
6059
  if (!opts.alwaysLoad) {
5992
6060
  const probe2 = unitExists ? d.capture(statusCommand(manager)) : { code: 1, stdout: "" };
5993
6061
  const status = parseServiceStatus(manager, unitExists, probe2);
5994
6062
  if (current && status.running) return { kind: "running", code: 0, already: true };
5995
6063
  }
5996
- if (canRender && !current) {
6064
+ if (!current) {
5997
6065
  try {
5998
6066
  d.mkdirp(d.logDir);
5999
6067
  d.mkdirp(dirOf(plan.filePath));
@@ -6023,7 +6091,6 @@ async function runNodeRestart(inputs, deps = {}) {
6023
6091
  manager,
6024
6092
  nodeBin: d.nodeBin,
6025
6093
  scriptPath: d.scriptPath,
6026
- token: "-",
6027
6094
  homeDir: d.homeDir,
6028
6095
  logDir: d.logDir
6029
6096
  });
@@ -6065,7 +6132,6 @@ async function runNodeStop(inputs = {}, deps = {}) {
6065
6132
  manager,
6066
6133
  nodeBin: d.nodeBin,
6067
6134
  scriptPath: d.scriptPath,
6068
- token: "-",
6069
6135
  homeDir: d.homeDir,
6070
6136
  logDir: d.logDir
6071
6137
  });
@@ -6102,7 +6168,6 @@ async function runServiceUninstall(deps = {}) {
6102
6168
  manager,
6103
6169
  nodeBin: d.nodeBin,
6104
6170
  scriptPath: d.scriptPath,
6105
- token: "-",
6106
6171
  homeDir: d.homeDir,
6107
6172
  logDir: d.logDir
6108
6173
  });
@@ -6166,9 +6231,9 @@ var defaultDeps3 = () => ({
6166
6231
  // npm-global bins) that a supervisor's minimal PATH would otherwise hide.
6167
6232
  pathEnv: process.env.PATH,
6168
6233
  mkdirp: (dir) => void mkdirSync12(dir, { recursive: true }),
6169
- // The unit's environment block carries the enrolment token, so the file is a secret: write it
6170
- // owner-only. `writeFileSync`'s `mode` applies only when it CREATES the file, so chmod explicitly
6171
- // too - re-installing over a unit an older client left at 0644 must tighten it, not keep it.
6234
+ // The unit holds no secret any more, but it stays owner-only: it names paths on this host, and there is
6235
+ // no reason for it to be world-readable. `writeFileSync`'s `mode` applies only when it CREATES the file,
6236
+ // so chmod explicitly too - re-installing over a unit an older client left at 0644 must tighten it.
6172
6237
  writeFile: (path, content) => {
6173
6238
  writeFileSync11(path, content, { mode: UNIT_FILE_MODE });
6174
6239
  chmodSync3(path, UNIT_FILE_MODE);
@@ -6510,6 +6575,12 @@ function renderStatus(f) {
6510
6575
  lines.push(kv("Work", jobLine(first, f.now)));
6511
6576
  for (const j of rest) lines.push(kv("", jobLine(j, f.now)));
6512
6577
  }
6578
+ if (isEnrolmentBlocked(f.connection)) {
6579
+ lines.push("");
6580
+ lines.push(verdict("fail", "The hub rejected this node's enrolment token; it is serving no Jobs."));
6581
+ lines.push("");
6582
+ lines.push(hint("Re-enrol with `dahrk start --token <token>`. Get one at https://app.dahrk.ai."));
6583
+ }
6513
6584
  lines.push(...presenceHints(f).flatMap((h) => ["", h]));
6514
6585
  lines.push("", dim(` State file: ${f.stateFile}`));
6515
6586
  return lines;
@@ -6532,7 +6603,7 @@ function jobLine(j, now) {
6532
6603
  return `${where} ${dim(humanDuration(now - j.startedAt))}`;
6533
6604
  }
6534
6605
  function isUnhealthy(f) {
6535
- return f.presence.kind === "crashed";
6606
+ return f.presence.kind === "crashed" || isEnrolmentBlocked(f.connection);
6536
6607
  }
6537
6608
  function resolvePresence(service, lockedPid, desired) {
6538
6609
  if (service?.running) return { kind: "running", ...service.pid ? { pid: service.pid } : {} };
@@ -6575,10 +6646,21 @@ async function gatherFacts(inputs, deps) {
6575
6646
  }
6576
6647
  var CONNECTION_MARKERS = [
6577
6648
  { prefix: "EDGE_WELCOMED", event: "welcomed", detail: false },
6649
+ // The two markers whose absence here was itself the bug. `EDGE_CONNECTED` is written when the SOCKET
6650
+ // opens, which is before the hub has looked at our token - so a node whose enrolment was rejected has
6651
+ // `EDGE_CONNECTED` as its last marker, and `status` used to read that and cheerfully report a connected
6652
+ // node while it served nothing. The rejection is the newer marker, and it is the true one.
6653
+ { prefix: "EDGE_REJECTED", event: "rejected", detail: true },
6654
+ { prefix: "EDGE_PARKED", event: "parked", detail: false },
6655
+ { prefix: "EDGE_UNPARKED", event: "reconnecting", detail: false },
6578
6656
  { prefix: "EDGE_CONNECTED", event: "connected", detail: false },
6579
6657
  { prefix: "EDGE_DISCONNECTED", event: "disconnected", detail: true },
6580
6658
  { prefix: "EDGE_STALE", event: "went stale", detail: false }
6581
6659
  ];
6660
+ var BLOCKED_EVENTS = /* @__PURE__ */ new Set(["rejected", "parked"]);
6661
+ function isEnrolmentBlocked(connection) {
6662
+ return connection !== void 0 && BLOCKED_EVENTS.has(connection.event);
6663
+ }
6582
6664
  function lastConnection(records) {
6583
6665
  for (let i = records.length - 1; i >= 0; i--) {
6584
6666
  const r = records[i];
@@ -6604,7 +6686,7 @@ async function runStatus(inputs, deps) {
6604
6686
  }
6605
6687
 
6606
6688
  // src/main.ts
6607
- var CLIENT_VERSION = "0.1.16";
6689
+ var CLIENT_VERSION = "0.1.18";
6608
6690
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
6609
6691
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
6610
6692
  var RUNTIMES2 = ["claude-code", "codex", "pi"];
@@ -6689,6 +6771,8 @@ function wantsForeground(env, flags) {
6689
6771
  async function start(flags) {
6690
6772
  const env = envWithFlags(process.env, flags);
6691
6773
  if (wantsForeground(env, flags)) return startForeground(env, flags);
6774
+ const enrolled = await enrolToDisk(env);
6775
+ if (enrolled !== 0) return enrolled;
6692
6776
  await offerUpdate(env);
6693
6777
  const outcome = await runNodeStart(serviceInputs(env));
6694
6778
  if (outcome.kind === "error") return outcome.code;
@@ -6708,6 +6792,28 @@ var serviceInputs = (env) => ({
6708
6792
  ...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
6709
6793
  ...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
6710
6794
  });
6795
+ async function enrolToDisk(env) {
6796
+ const token = resolveEnrolToken(env);
6797
+ if (!token || token === readPersistedToken(env)) return 0;
6798
+ const hubUrl = env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL;
6799
+ const probe2 = await probeHub({ hubUrl, enrolToken: token, clientVersion: CLIENT_VERSION });
6800
+ if (!probe2.ok && probe2.reason === "rejected") {
6801
+ out("");
6802
+ out(verdict("fail", `The hub rejected that enrolment token: ${probe2.detail}`));
6803
+ out("");
6804
+ out(hint("Get a fresh token at https://app.dahrk.ai, then run `dahrk start --token <token>` again."));
6805
+ return 2;
6806
+ }
6807
+ if (!probe2.ok) {
6808
+ out("");
6809
+ out(verdict("warn", `Could not reach ${hubUrl} to check the token (${probe2.detail}); using it anyway.`));
6810
+ }
6811
+ persistEnrolment(env, {
6812
+ token,
6813
+ ...probe2.ok ? { name: probe2.name, tenantId: probe2.tenantId } : {}
6814
+ });
6815
+ return 0;
6816
+ }
6711
6817
  async function restart(flags, force) {
6712
6818
  const env = envWithFlags(process.env, flags);
6713
6819
  out("");
@@ -6746,7 +6852,10 @@ async function startForeground(env, flags) {
6746
6852
  clientVersion: CLIENT_VERSION,
6747
6853
  ...env.DAHRK_CRASH_EXIT === "1" ? { exitOnCrash: true } : {}
6748
6854
  });
6749
- const token = resolveEnrolToken(env, { ephemeral: flags.ephemeral });
6855
+ const token = resolveEnrolToken(env, {
6856
+ ephemeral: flags.ephemeral,
6857
+ supervised: env.DAHRK_SUPERVISED === "1"
6858
+ });
6750
6859
  if (token) env.DAHRK_ENROL_TOKEN = token;
6751
6860
  const runtimes = await resolveRuntimes(env);
6752
6861
  if (!flags.ephemeral) {
@@ -6785,7 +6894,15 @@ async function startForeground(env, flags) {
6785
6894
  ...flags.ephemeral ? {} : { jobLedger: fileJobLedger(jobLedgerFile(stateDir(env))) },
6786
6895
  ...persist ? {
6787
6896
  onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
6788
- } : {}
6897
+ } : {},
6898
+ // How a rejected node heals. Reads `node.json` DIRECTLY rather than going through
6899
+ // `resolveEnrolToken`, and that is the whole point: the disk is where re-enrolment writes, so it is
6900
+ // the only place a *newer* token can appear. (Deliberately narrower than boot-time resolution, which
6901
+ // still lets an explicit `DAHRK_ENROL_TOKEN` win - but a stale env var must never be able to shadow
6902
+ // the fresh token on disk forever, which is precisely how a node used to wedge.)
6903
+ //
6904
+ // Only for a persistent node: an ephemeral one has no disk to heal from and must still fail fast.
6905
+ ...flags.ephemeral ? {} : { refreshEnrolToken: () => readPersistedToken(env) }
6789
6906
  });
6790
6907
  return 0;
6791
6908
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dahrk-node",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",