dahrk-node 0.1.17 → 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.
- package/dist/main.js +123 -23
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -3884,6 +3884,9 @@ function createStageRunner(deps) {
|
|
|
3884
3884
|
|
|
3885
3885
|
// ../../packages/edge/src/ws-client.ts
|
|
3886
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;
|
|
3887
3890
|
function classifyError(e) {
|
|
3888
3891
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3889
3892
|
if (/\bgit\b|worktree|clone|fetch|branch|checkout|remote/i.test(msg)) return "git";
|
|
@@ -3919,7 +3922,11 @@ async function startEdgeNode(opts) {
|
|
|
3919
3922
|
let lastPongAt = 0;
|
|
3920
3923
|
let shuttingDown = false;
|
|
3921
3924
|
let connectCount = 0;
|
|
3925
|
+
let enrolToken = opts.enrolToken;
|
|
3926
|
+
let reconnectAttempts = 0;
|
|
3927
|
+
let parkTimer;
|
|
3922
3928
|
let onFatal;
|
|
3929
|
+
let onParked;
|
|
3923
3930
|
const send = (msg) => {
|
|
3924
3931
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
|
|
3925
3932
|
};
|
|
@@ -4042,7 +4049,7 @@ async function startEdgeNode(opts) {
|
|
|
4042
4049
|
const sendHello = () => {
|
|
4043
4050
|
send({
|
|
4044
4051
|
type: "hello",
|
|
4045
|
-
enrolToken:
|
|
4052
|
+
enrolToken: enrolToken ?? "",
|
|
4046
4053
|
detectedRuntimes: currentRuntimes,
|
|
4047
4054
|
servesRepoIds: opts.servesRepoIds ?? [],
|
|
4048
4055
|
...opts.credentialModeExplicit && opts.credentialMode ? { credentialMode: opts.credentialMode } : {},
|
|
@@ -4102,6 +4109,7 @@ async function startEdgeNode(opts) {
|
|
|
4102
4109
|
const msg = decode(raw);
|
|
4103
4110
|
if (msg.type === "welcome") {
|
|
4104
4111
|
stageDeps.tenantId = msg.tenantId;
|
|
4112
|
+
reconnectAttempts = 0;
|
|
4105
4113
|
if (opts.retention === void 0 && msg.retention) stageDeps.retention = msg.retention;
|
|
4106
4114
|
if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
|
|
4107
4115
|
startHeartbeat(ws, msg.heartbeatMs);
|
|
@@ -4286,19 +4294,59 @@ async function startEdgeNode(opts) {
|
|
|
4286
4294
|
if (heartbeat) clearInterval(heartbeat);
|
|
4287
4295
|
heartbeat = void 0;
|
|
4288
4296
|
if (isEnrolmentRejection(code)) {
|
|
4289
|
-
shuttingDown = true;
|
|
4290
4297
|
const detail = reason.toString() || "enrolment rejected";
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
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);
|
|
4294
4307
|
return;
|
|
4295
4308
|
}
|
|
4296
4309
|
if (!shuttingDown) {
|
|
4297
|
-
|
|
4298
|
-
|
|
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);
|
|
4299
4316
|
}
|
|
4300
4317
|
});
|
|
4301
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
|
+
};
|
|
4302
4350
|
opts.signal?.addEventListener(
|
|
4303
4351
|
"abort",
|
|
4304
4352
|
() => {
|
|
@@ -4309,6 +4357,8 @@ async function startEdgeNode(opts) {
|
|
|
4309
4357
|
reconnectTimer = void 0;
|
|
4310
4358
|
if (reprobeTimer) clearInterval(reprobeTimer);
|
|
4311
4359
|
reprobeTimer = void 0;
|
|
4360
|
+
if (parkTimer) clearInterval(parkTimer);
|
|
4361
|
+
parkTimer = void 0;
|
|
4312
4362
|
ws?.close(1e3, "shutting down");
|
|
4313
4363
|
},
|
|
4314
4364
|
{ once: true }
|
|
@@ -4325,6 +4375,10 @@ async function startEdgeNode(opts) {
|
|
|
4325
4375
|
clearInterval(t);
|
|
4326
4376
|
reject(err);
|
|
4327
4377
|
};
|
|
4378
|
+
onParked = () => {
|
|
4379
|
+
clearInterval(t);
|
|
4380
|
+
resolve3();
|
|
4381
|
+
};
|
|
4328
4382
|
});
|
|
4329
4383
|
}
|
|
4330
4384
|
|
|
@@ -5737,6 +5791,7 @@ function persistEnrolment(env, enrolment) {
|
|
|
5737
5791
|
});
|
|
5738
5792
|
}
|
|
5739
5793
|
function resolveEnrolToken(env, opts = {}) {
|
|
5794
|
+
if (opts.supervised && !opts.ephemeral) return readPersistedToken(env) ?? env.DAHRK_ENROL_TOKEN;
|
|
5740
5795
|
if (env.DAHRK_ENROL_TOKEN) return env.DAHRK_ENROL_TOKEN;
|
|
5741
5796
|
if (opts.ephemeral) return void 0;
|
|
5742
5797
|
return readPersistedToken(env);
|
|
@@ -5756,7 +5811,6 @@ function serviceArgv(inputs) {
|
|
|
5756
5811
|
}
|
|
5757
5812
|
function serviceEnv(inputs) {
|
|
5758
5813
|
return {
|
|
5759
|
-
DAHRK_ENROL_TOKEN: inputs.token,
|
|
5760
5814
|
DAHRK_SUPERVISED: "1",
|
|
5761
5815
|
...inputs.hubUrl ? { DAHRK_HUB_URL: inputs.hubUrl } : {},
|
|
5762
5816
|
...inputs.name ? { DAHRK_NODE_NAME: inputs.name } : {},
|
|
@@ -5928,7 +5982,6 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
5928
5982
|
manager,
|
|
5929
5983
|
nodeBin: d.nodeBin,
|
|
5930
5984
|
scriptPath: d.scriptPath,
|
|
5931
|
-
token: inputs.token,
|
|
5932
5985
|
...inputs.name ? { name: inputs.name } : {},
|
|
5933
5986
|
...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
|
|
5934
5987
|
...d.pathEnv ? { pathEnv: d.pathEnv } : {},
|
|
@@ -5996,21 +6049,19 @@ async function runNodeStart(inputs, deps = {}, opts = {}) {
|
|
|
5996
6049
|
manager,
|
|
5997
6050
|
nodeBin: d.nodeBin,
|
|
5998
6051
|
scriptPath: d.scriptPath,
|
|
5999
|
-
token: inputs.token ?? "-",
|
|
6000
6052
|
...inputs.name ? { name: inputs.name } : {},
|
|
6001
6053
|
...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
|
|
6002
6054
|
...d.pathEnv ? { pathEnv: d.pathEnv } : {},
|
|
6003
6055
|
homeDir: d.homeDir,
|
|
6004
6056
|
logDir: d.logDir
|
|
6005
6057
|
});
|
|
6006
|
-
const
|
|
6007
|
-
const current = canRender && unitExists && unitIsCurrent(plan, d.readFile(filePath));
|
|
6058
|
+
const current = unitExists && unitIsCurrent(plan, d.readFile(filePath));
|
|
6008
6059
|
if (!opts.alwaysLoad) {
|
|
6009
6060
|
const probe2 = unitExists ? d.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
6010
6061
|
const status = parseServiceStatus(manager, unitExists, probe2);
|
|
6011
6062
|
if (current && status.running) return { kind: "running", code: 0, already: true };
|
|
6012
6063
|
}
|
|
6013
|
-
if (
|
|
6064
|
+
if (!current) {
|
|
6014
6065
|
try {
|
|
6015
6066
|
d.mkdirp(d.logDir);
|
|
6016
6067
|
d.mkdirp(dirOf(plan.filePath));
|
|
@@ -6040,7 +6091,6 @@ async function runNodeRestart(inputs, deps = {}) {
|
|
|
6040
6091
|
manager,
|
|
6041
6092
|
nodeBin: d.nodeBin,
|
|
6042
6093
|
scriptPath: d.scriptPath,
|
|
6043
|
-
token: "-",
|
|
6044
6094
|
homeDir: d.homeDir,
|
|
6045
6095
|
logDir: d.logDir
|
|
6046
6096
|
});
|
|
@@ -6082,7 +6132,6 @@ async function runNodeStop(inputs = {}, deps = {}) {
|
|
|
6082
6132
|
manager,
|
|
6083
6133
|
nodeBin: d.nodeBin,
|
|
6084
6134
|
scriptPath: d.scriptPath,
|
|
6085
|
-
token: "-",
|
|
6086
6135
|
homeDir: d.homeDir,
|
|
6087
6136
|
logDir: d.logDir
|
|
6088
6137
|
});
|
|
@@ -6119,7 +6168,6 @@ async function runServiceUninstall(deps = {}) {
|
|
|
6119
6168
|
manager,
|
|
6120
6169
|
nodeBin: d.nodeBin,
|
|
6121
6170
|
scriptPath: d.scriptPath,
|
|
6122
|
-
token: "-",
|
|
6123
6171
|
homeDir: d.homeDir,
|
|
6124
6172
|
logDir: d.logDir
|
|
6125
6173
|
});
|
|
@@ -6183,9 +6231,9 @@ var defaultDeps3 = () => ({
|
|
|
6183
6231
|
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
6184
6232
|
pathEnv: process.env.PATH,
|
|
6185
6233
|
mkdirp: (dir) => void mkdirSync12(dir, { recursive: true }),
|
|
6186
|
-
// The unit
|
|
6187
|
-
//
|
|
6188
|
-
// too - re-installing over a unit an older client left at 0644 must tighten 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.
|
|
6189
6237
|
writeFile: (path, content) => {
|
|
6190
6238
|
writeFileSync11(path, content, { mode: UNIT_FILE_MODE });
|
|
6191
6239
|
chmodSync3(path, UNIT_FILE_MODE);
|
|
@@ -6527,6 +6575,12 @@ function renderStatus(f) {
|
|
|
6527
6575
|
lines.push(kv("Work", jobLine(first, f.now)));
|
|
6528
6576
|
for (const j of rest) lines.push(kv("", jobLine(j, f.now)));
|
|
6529
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
|
+
}
|
|
6530
6584
|
lines.push(...presenceHints(f).flatMap((h) => ["", h]));
|
|
6531
6585
|
lines.push("", dim(` State file: ${f.stateFile}`));
|
|
6532
6586
|
return lines;
|
|
@@ -6549,7 +6603,7 @@ function jobLine(j, now) {
|
|
|
6549
6603
|
return `${where} ${dim(humanDuration(now - j.startedAt))}`;
|
|
6550
6604
|
}
|
|
6551
6605
|
function isUnhealthy(f) {
|
|
6552
|
-
return f.presence.kind === "crashed";
|
|
6606
|
+
return f.presence.kind === "crashed" || isEnrolmentBlocked(f.connection);
|
|
6553
6607
|
}
|
|
6554
6608
|
function resolvePresence(service, lockedPid, desired) {
|
|
6555
6609
|
if (service?.running) return { kind: "running", ...service.pid ? { pid: service.pid } : {} };
|
|
@@ -6592,10 +6646,21 @@ async function gatherFacts(inputs, deps) {
|
|
|
6592
6646
|
}
|
|
6593
6647
|
var CONNECTION_MARKERS = [
|
|
6594
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 },
|
|
6595
6656
|
{ prefix: "EDGE_CONNECTED", event: "connected", detail: false },
|
|
6596
6657
|
{ prefix: "EDGE_DISCONNECTED", event: "disconnected", detail: true },
|
|
6597
6658
|
{ prefix: "EDGE_STALE", event: "went stale", detail: false }
|
|
6598
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
|
+
}
|
|
6599
6664
|
function lastConnection(records) {
|
|
6600
6665
|
for (let i = records.length - 1; i >= 0; i--) {
|
|
6601
6666
|
const r = records[i];
|
|
@@ -6621,7 +6686,7 @@ async function runStatus(inputs, deps) {
|
|
|
6621
6686
|
}
|
|
6622
6687
|
|
|
6623
6688
|
// src/main.ts
|
|
6624
|
-
var CLIENT_VERSION = "0.1.
|
|
6689
|
+
var CLIENT_VERSION = "0.1.18";
|
|
6625
6690
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
6626
6691
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
6627
6692
|
var RUNTIMES2 = ["claude-code", "codex", "pi"];
|
|
@@ -6706,6 +6771,8 @@ function wantsForeground(env, flags) {
|
|
|
6706
6771
|
async function start(flags) {
|
|
6707
6772
|
const env = envWithFlags(process.env, flags);
|
|
6708
6773
|
if (wantsForeground(env, flags)) return startForeground(env, flags);
|
|
6774
|
+
const enrolled = await enrolToDisk(env);
|
|
6775
|
+
if (enrolled !== 0) return enrolled;
|
|
6709
6776
|
await offerUpdate(env);
|
|
6710
6777
|
const outcome = await runNodeStart(serviceInputs(env));
|
|
6711
6778
|
if (outcome.kind === "error") return outcome.code;
|
|
@@ -6725,6 +6792,28 @@ var serviceInputs = (env) => ({
|
|
|
6725
6792
|
...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
|
|
6726
6793
|
...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
|
|
6727
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
|
+
}
|
|
6728
6817
|
async function restart(flags, force) {
|
|
6729
6818
|
const env = envWithFlags(process.env, flags);
|
|
6730
6819
|
out("");
|
|
@@ -6763,7 +6852,10 @@ async function startForeground(env, flags) {
|
|
|
6763
6852
|
clientVersion: CLIENT_VERSION,
|
|
6764
6853
|
...env.DAHRK_CRASH_EXIT === "1" ? { exitOnCrash: true } : {}
|
|
6765
6854
|
});
|
|
6766
|
-
const token = resolveEnrolToken(env, {
|
|
6855
|
+
const token = resolveEnrolToken(env, {
|
|
6856
|
+
ephemeral: flags.ephemeral,
|
|
6857
|
+
supervised: env.DAHRK_SUPERVISED === "1"
|
|
6858
|
+
});
|
|
6767
6859
|
if (token) env.DAHRK_ENROL_TOKEN = token;
|
|
6768
6860
|
const runtimes = await resolveRuntimes(env);
|
|
6769
6861
|
if (!flags.ephemeral) {
|
|
@@ -6802,7 +6894,15 @@ async function startForeground(env, flags) {
|
|
|
6802
6894
|
...flags.ephemeral ? {} : { jobLedger: fileJobLedger(jobLedgerFile(stateDir(env))) },
|
|
6803
6895
|
...persist ? {
|
|
6804
6896
|
onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
|
|
6805
|
-
} : {}
|
|
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) }
|
|
6806
6906
|
});
|
|
6807
6907
|
return 0;
|
|
6808
6908
|
}
|