dahrk-node 0.1.11 → 0.1.12

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 +111 -36
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -470,18 +470,21 @@ function toElicitQuestion(q) {
470
470
  ${lines.join("\n")}` : q.question;
471
471
  return { prompt, options, ...q.multiSelect ? { multiSelect: true } : {} };
472
472
  }
473
+ function buildElicitFromQuestions(questions) {
474
+ const first = questions[0];
475
+ const q = toElicitQuestion(first);
476
+ const prompt = questions.length > 1 ? `${q.prompt}
477
+
478
+ (Note: ${questions.length} questions were asked at once; answer this one first, then ask the rest.)` : q.prompt;
479
+ return { ...q, prompt };
480
+ }
473
481
  function createAskUserQuestionTool(deps) {
474
482
  const askTool = tool2(
475
483
  "ask_user_question",
476
484
  "Ask the human a structured multiple-choice question and wait for their selection. Use this when you need the human to choose between options before you can continue.",
477
485
  { questions: z2.array(questionSchema).min(1) },
478
486
  async (args) => {
479
- const first = args.questions[0];
480
- const question = toElicitQuestion(first);
481
- const prompt = args.questions.length > 1 ? `${question.prompt}
482
-
483
- (Note: ${args.questions.length} questions were asked at once; answer this one first, then ask the rest.)` : question.prompt;
484
- const text = await deps.ask({ ...question, prompt });
487
+ const text = await deps.ask(buildElicitFromQuestions(args.questions));
485
488
  return { content: [{ type: "text", text }] };
486
489
  }
487
490
  );
@@ -536,6 +539,10 @@ function sandboxOptions(ctx) {
536
539
  }
537
540
  };
538
541
  }
542
+ function runtimeEnvOptions(ctx) {
543
+ if (!ctx.runtimeEnv) return {};
544
+ return { env: { ...process.env, ...ctx.runtimeEnv } };
545
+ }
539
546
  function createClaudeRunner() {
540
547
  const abortController = new AbortController();
541
548
  let cancelled = false;
@@ -579,6 +586,8 @@ function createClaudeRunner() {
579
586
  // .claude/ + CLAUDE.md + .mcp.json, which is all section 9 actually requires. Claude auth is
580
587
  // keychain/OAuth and independent of settingSources, so dropping "user" does not affect it.
581
588
  settingSources: ["project", "local"],
589
+ // Brokered inference env (DHK-89), for a managed / Docker-isolated node with no ambient login.
590
+ ...runtimeEnvOptions(ctx),
582
591
  ...ctx.config.model ? { model: ctx.config.model } : {},
583
592
  ...ctx.sessionId ? { resume: ctx.sessionId } : {},
584
593
  ...ctx.config.skill ? { skills: [ctx.config.skill] } : {}
@@ -893,6 +902,12 @@ function mapUsage2(u) {
893
902
 
894
903
  // ../../packages/executor-worktree/src/codex-adapter.ts
895
904
  var COALESCE_MS2 = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
905
+ function runtimeEnvOptions2(ctx) {
906
+ if (!ctx.runtimeEnv) return {};
907
+ const env = {};
908
+ for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
909
+ return { env: { ...env, ...ctx.runtimeEnv } };
910
+ }
896
911
  function createCodexRunner() {
897
912
  const abortController = new AbortController();
898
913
  const signal = abortController.signal;
@@ -906,7 +921,7 @@ function createCodexRunner() {
906
921
  ...ctx.config.model ? { model: ctx.config.model } : {}
907
922
  });
908
923
  const openThread = (ctx) => {
909
- const codex = new Codex();
924
+ const codex = new Codex(runtimeEnvOptions2(ctx));
910
925
  const t = ctx.sessionId ? codex.resumeThread(ctx.sessionId, threadOptions(ctx)) : codex.startThread(threadOptions(ctx));
911
926
  thread = t;
912
927
  return t;
@@ -3758,6 +3773,8 @@ async function startEdgeNode(opts) {
3758
3773
  const counters = new HealthCounters();
3759
3774
  const shipper = opts.shipper;
3760
3775
  const telemetryCeiling = ceilingFromEnv(process.env);
3776
+ let currentRuntimes = opts.runtimes;
3777
+ const runtimesKey = (r) => [...r].sort().join(",");
3761
3778
  const gitLog = log.child({ component: "git" });
3762
3779
  const gitLogger = {
3763
3780
  info: (msg) => gitLog.debug(msg),
@@ -3804,7 +3821,7 @@ async function startEdgeNode(opts) {
3804
3821
  health: collectHealth({
3805
3822
  counters,
3806
3823
  clientVersion: opts.clientVersion ?? "0.0.0",
3807
- runtimes: opts.runtimes,
3824
+ runtimes: currentRuntimes,
3808
3825
  worktreesDir: gitService.worktreesDir
3809
3826
  })
3810
3827
  } : { type: "heartbeat" }
@@ -3863,6 +3880,41 @@ async function startEdgeNode(opts) {
3863
3880
  for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
3864
3881
  }).catch((e) => log.warn({ err: e }, `EDGE_REAP_ERROR:${e.message}`));
3865
3882
  const nodeId = opts.nodeId ?? randomUUID();
3883
+ const sendHello = () => {
3884
+ send({
3885
+ type: "hello",
3886
+ enrolToken: opts.enrolToken ?? "",
3887
+ detectedRuntimes: currentRuntimes,
3888
+ servesRepoIds: opts.servesRepoIds ?? [],
3889
+ ...opts.credentialModeExplicit && opts.credentialMode ? { credentialMode: opts.credentialMode } : {},
3890
+ nodeId,
3891
+ ...opts.name ? { name: opts.name } : {},
3892
+ os: osPlatform(),
3893
+ arch: osArch(),
3894
+ clientVersion: opts.clientVersion ?? "0.0.0",
3895
+ // Advertise the resolved worktree base so the hub records each run's real worktree location in
3896
+ // the projection instead of an advisory placeholder. Single-sourced from the git service so it
3897
+ // always matches where worktrees actually land.
3898
+ worktreesDir: gitService.worktreesDir
3899
+ });
3900
+ };
3901
+ let reprobeTimer;
3902
+ if (opts.reprobeRuntimes) {
3903
+ const reprobe = opts.reprobeRuntimes;
3904
+ reprobeTimer = setInterval(() => {
3905
+ void reprobe().then((detected) => {
3906
+ if (runtimesKey(detected) === runtimesKey(currentRuntimes)) return;
3907
+ const before = currentRuntimes;
3908
+ currentRuntimes = detected;
3909
+ log.warn(
3910
+ { before, after: detected },
3911
+ `EDGE_RUNTIMES_CHANGED:${before.join(",") || "none"} -> ${detected.join(",") || "none"}`
3912
+ );
3913
+ sendHello();
3914
+ }).catch((e) => log.warn({ err: e }, `EDGE_REPROBE_ERROR:${e.message}`));
3915
+ }, opts.runtimeRecheckMs ?? 6e4);
3916
+ reprobeTimer.unref?.();
3917
+ }
3866
3918
  const running = /* @__PURE__ */ new Set();
3867
3919
  const onMessage = async (raw) => {
3868
3920
  const msg = decode(raw);
@@ -4013,22 +4065,7 @@ async function startEdgeNode(opts) {
4013
4065
  connectCount++;
4014
4066
  counters.connectCount = connectCount;
4015
4067
  log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
4016
- send({
4017
- type: "hello",
4018
- enrolToken: opts.enrolToken ?? "",
4019
- detectedRuntimes: opts.runtimes,
4020
- servesRepoIds: opts.servesRepoIds ?? [],
4021
- ...opts.credentialModeExplicit && opts.credentialMode ? { credentialMode: opts.credentialMode } : {},
4022
- nodeId,
4023
- ...opts.name ? { name: opts.name } : {},
4024
- os: osPlatform(),
4025
- arch: osArch(),
4026
- clientVersion: opts.clientVersion ?? "0.0.0",
4027
- // Advertise the resolved worktree base so the hub records each run's real worktree location in
4028
- // the projection instead of an advisory placeholder. Single-sourced from the git service so it
4029
- // always matches where worktrees actually land.
4030
- worktreesDir: gitService.worktreesDir
4031
- });
4068
+ sendHello();
4032
4069
  for (const frame of lastResults.values()) send(frame);
4033
4070
  startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
4034
4071
  });
@@ -4062,6 +4099,8 @@ async function startEdgeNode(opts) {
4062
4099
  heartbeat = void 0;
4063
4100
  if (reconnectTimer) clearTimeout(reconnectTimer);
4064
4101
  reconnectTimer = void 0;
4102
+ if (reprobeTimer) clearInterval(reprobeTimer);
4103
+ reprobeTimer = void 0;
4065
4104
  ws?.close(1e3, "shutting down");
4066
4105
  },
4067
4106
  { once: true }
@@ -4088,24 +4127,37 @@ var PROBES = [
4088
4127
  { runtime: "codex", cmd: "codex" },
4089
4128
  { runtime: "pi", cmd: "pi" }
4090
4129
  ];
4091
- function probe(cmd, timeoutMs) {
4130
+ var DEFAULT_TIMEOUT_MS = 5e3;
4131
+ var DEFAULT_ATTEMPTS = 2;
4132
+ function probeOnce(cmd, timeoutMs) {
4092
4133
  return new Promise((resolve3) => {
4093
4134
  execFile(cmd, ["--version"], { timeout: timeoutMs }, (err, stdout) => {
4094
- if (err) return resolve3(void 0);
4135
+ if (err) {
4136
+ const code = err.code;
4137
+ return resolve3({ retryable: code !== "ENOENT" });
4138
+ }
4095
4139
  const line = stdout.split("\n").map((s) => s.trim()).find(Boolean);
4096
- resolve3(line ?? "");
4140
+ resolve3({ version: line ?? "" });
4097
4141
  });
4098
4142
  });
4099
4143
  }
4100
- async function probeRuntimeStatuses(timeoutMs = 3e3) {
4101
- const versions = await Promise.all(PROBES.map((p) => probe(p.cmd, timeoutMs)));
4144
+ async function probe(cmd, timeoutMs, attempts) {
4145
+ for (let attempt = 1; attempt <= attempts; attempt++) {
4146
+ const result = await probeOnce(cmd, timeoutMs);
4147
+ if ("version" in result) return result.version;
4148
+ if (!result.retryable) return void 0;
4149
+ }
4150
+ return void 0;
4151
+ }
4152
+ async function probeRuntimeStatuses(timeoutMs = DEFAULT_TIMEOUT_MS, attempts = DEFAULT_ATTEMPTS) {
4153
+ const versions = await Promise.all(PROBES.map((p) => probe(p.cmd, timeoutMs, attempts)));
4102
4154
  return PROBES.map((p, i) => {
4103
4155
  const version = versions[i];
4104
4156
  return version === void 0 ? { runtime: p.runtime, cmd: p.cmd, installed: false } : { runtime: p.runtime, cmd: p.cmd, installed: true, version };
4105
4157
  });
4106
4158
  }
4107
- async function detectRuntimes(timeoutMs = 3e3) {
4108
- const statuses = await probeRuntimeStatuses(timeoutMs);
4159
+ async function detectRuntimes(timeoutMs = DEFAULT_TIMEOUT_MS, attempts = DEFAULT_ATTEMPTS) {
4160
+ const statuses = await probeRuntimeStatuses(timeoutMs, attempts);
4109
4161
  return statuses.filter((s) => s.installed).map((s) => s.runtime);
4110
4162
  }
4111
4163
 
@@ -5289,6 +5341,8 @@ import { join as join15 } from "path";
5289
5341
  import { chmodSync, existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
5290
5342
  import { homedir as homedir5 } from "os";
5291
5343
  import { join as join14 } from "path";
5344
+ var RUNTIMES = ["claude-code", "codex", "pi"];
5345
+ var isRuntime = (v) => RUNTIMES.includes(v);
5292
5346
  var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
5293
5347
  var isDesired = (v) => v === "running" || v === "stopped";
5294
5348
  var FILE_MODE = 384;
@@ -5331,6 +5385,10 @@ function readState(file) {
5331
5385
  if (typeof value === "string" && value) state[key] = value;
5332
5386
  }
5333
5387
  if (isDesired(parsed["desired"])) state.desired = parsed["desired"];
5388
+ if (Array.isArray(parsed["runtimes"])) {
5389
+ const runtimes = parsed["runtimes"].filter(isRuntime);
5390
+ if (runtimes.length) state.runtimes = runtimes;
5391
+ }
5334
5392
  return state;
5335
5393
  } catch {
5336
5394
  return {};
@@ -6045,11 +6103,11 @@ async function runStatus(inputs, deps) {
6045
6103
  }
6046
6104
 
6047
6105
  // src/main.ts
6048
- var CLIENT_VERSION = "0.1.11";
6106
+ var CLIENT_VERSION = "0.1.12";
6049
6107
  var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
6050
6108
  var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
6051
- var RUNTIMES = ["claude-code", "codex", "pi"];
6052
- var isRuntime = (r) => RUNTIMES.includes(r);
6109
+ var RUNTIMES2 = ["claude-code", "codex", "pi"];
6110
+ var isRuntime2 = (r) => RUNTIMES2.includes(r);
6053
6111
  function resolveNodeId(env, opts = {}) {
6054
6112
  if (env.DAHRK_NODE_ID) return env.DAHRK_NODE_ID;
6055
6113
  if (opts.ephemeral) return randomUUID3();
@@ -6065,7 +6123,7 @@ function resolveNodeId(env, opts = {}) {
6065
6123
  return nodeId;
6066
6124
  }
6067
6125
  async function resolveRuntimes(env) {
6068
- const override = list(env.DAHRK_RUNTIMES).filter(isRuntime);
6126
+ const override = list(env.DAHRK_RUNTIMES).filter(isRuntime2);
6069
6127
  if (override.length > 0) return override;
6070
6128
  if ((env.DAHRK_RUNNER ?? "real") === "mock") return ["claude-code"];
6071
6129
  return detectRuntimes();
@@ -6075,7 +6133,7 @@ function buildEdgeOptions(env, resolved) {
6075
6133
  const servesRepoIds = list(env.DAHRK_REPOS);
6076
6134
  const credentialModeExplicit = env.DAHRK_CREDENTIAL_MODE != null;
6077
6135
  const credentialMode = env.DAHRK_CREDENTIAL_MODE === "brokered" ? "brokered" : "ambient";
6078
- const envRuntimes = list(env.DAHRK_RUNTIMES).filter(isRuntime);
6136
+ const envRuntimes = list(env.DAHRK_RUNTIMES).filter(isRuntime2);
6079
6137
  const runtimes = resolved ? resolved.runtimes : envRuntimes.length > 0 ? envRuntimes : ["claude-code"];
6080
6138
  const nodeId = resolved?.nodeId ?? env.DAHRK_NODE_ID;
6081
6139
  const maxRuns = env.DAHRK_RETENTION_MAX_RUNS;
@@ -6170,6 +6228,18 @@ async function startForeground(env, flags) {
6170
6228
  const token = resolveEnrolToken(env, { ephemeral: flags.ephemeral });
6171
6229
  if (token) env.DAHRK_ENROL_TOKEN = token;
6172
6230
  const runtimes = await resolveRuntimes(env);
6231
+ if (!flags.ephemeral) {
6232
+ const prior = readState(stateFile(env)).runtimes ?? [];
6233
+ const missing = prior.filter((r) => !runtimes.includes(r));
6234
+ if (missing.length > 0) {
6235
+ logger.warn(
6236
+ { prior, detected: runtimes, missing },
6237
+ `RUNTIMES_DEGRADED:${missing.join(",")} advertised last run but not detected now - the node will serve no Jobs for it until it is re-probed or restarted. Run \`dahrk doctor\` to check.`
6238
+ );
6239
+ }
6240
+ writeState(env, { runtimes });
6241
+ }
6242
+ logger.info({ runtimes }, `RUNTIMES_DETECTED:${runtimes.join(",") || "none"}`);
6173
6243
  if (runtimes.length === 0) {
6174
6244
  console.warn(
6175
6245
  "no agent runtimes detected on this host (claude/codex/pi not on PATH); the node will advertise none and serve no Jobs. Install a runtime or set DAHRK_RUNTIMES to override. Run `dahrk doctor` to check."
@@ -6183,6 +6253,11 @@ async function startForeground(env, flags) {
6183
6253
  ...buildEdgeOptions(env, resolved),
6184
6254
  logger,
6185
6255
  shipper,
6256
+ // Self-heal a transiently-degraded boot without a manual restart: the edge re-probes on this seam
6257
+ // and re-advertises when the set changes. Reuses `resolveRuntimes`, so `DAHRK_RUNTIMES` still wins
6258
+ // (a pinned override never re-probes to something else) and the mock runner path stays stable.
6259
+ reprobeRuntimes: () => resolveRuntimes(env),
6260
+ ...env.DAHRK_RUNTIME_RECHECK_MS ? { runtimeRecheckMs: Number(env.DAHRK_RUNTIME_RECHECK_MS) } : {},
6186
6261
  ...persist ? {
6187
6262
  onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
6188
6263
  } : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dahrk-node",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",