dahrk-node 0.1.11 → 0.1.13
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.
- package/dist/main.js +111 -36
- package/package.json +3 -3
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
|
+
async function askQuestionsSequentially(questions, ask) {
|
|
474
|
+
const answers = [];
|
|
475
|
+
for (const q of questions) {
|
|
476
|
+
answers.push(await ask(toElicitQuestion(q)));
|
|
477
|
+
}
|
|
478
|
+
if (answers.length === 1) return answers[0];
|
|
479
|
+
return answers.map((a, i) => `Q${i + 1} (${questions[i].question}): ${a}`).join("\n");
|
|
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
|
|
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 askQuestionsSequentially(args.questions, deps.ask);
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
4101
|
-
|
|
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 =
|
|
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.
|
|
6106
|
+
var CLIENT_VERSION = "0.1.13";
|
|
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
|
|
6052
|
-
var
|
|
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(
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.3.5",
|
|
45
45
|
"tsx": "^4.19.0",
|
|
46
|
-
"@dahrk/
|
|
47
|
-
"@dahrk/
|
|
46
|
+
"@dahrk/edge": "0.1.0",
|
|
47
|
+
"@dahrk/executor-worktree": "0.1.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsup",
|