dahrk-node 0.1.8 → 0.1.10
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 +2109 -505
- package/package.json +3 -2
package/dist/main.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { execFileSync as
|
|
5
|
-
import { existsSync as
|
|
4
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
5
|
+
import { existsSync as existsSync12, realpathSync as realpathSync4 } from "fs";
|
|
6
6
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7
7
|
import { homedir as homedir5, platform as osPlatform4 } from "os";
|
|
8
|
-
import { basename, join as
|
|
8
|
+
import { basename as basename2, join as join14 } from "path";
|
|
9
9
|
import { pathToFileURL } from "url";
|
|
10
10
|
|
|
11
11
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -1333,7 +1333,7 @@ var PROVIDER_BY_ENV = {
|
|
|
1333
1333
|
import { execFileSync } from "child_process";
|
|
1334
1334
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
1335
1335
|
import { homedir, tmpdir } from "os";
|
|
1336
|
-
import { dirname, isAbsolute, join as join2 } from "path";
|
|
1336
|
+
import { basename, dirname, isAbsolute, join as join2 } from "path";
|
|
1337
1337
|
var noopLogger = { info: () => {
|
|
1338
1338
|
}, warn: () => {
|
|
1339
1339
|
} };
|
|
@@ -1349,12 +1349,16 @@ function sanitizeBranchName(name) {
|
|
|
1349
1349
|
function resolveWorktreesDir(override) {
|
|
1350
1350
|
return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
|
|
1351
1351
|
}
|
|
1352
|
+
function resolveMirrorsDir(override) {
|
|
1353
|
+
return override ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
|
|
1354
|
+
}
|
|
1352
1355
|
function createGitService(opts = {}) {
|
|
1353
1356
|
const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
|
|
1354
|
-
const mirrorsDir = opts.mirrorsDir
|
|
1357
|
+
const mirrorsDir = resolveMirrorsDir(opts.mirrorsDir);
|
|
1355
1358
|
const authorName = opts.authorName ?? process.env.DAHRK_GIT_AUTHOR_NAME ?? "Dahrk";
|
|
1356
1359
|
const authorEmail = opts.authorEmail ?? process.env.DAHRK_GIT_AUTHOR_EMAIL ?? "noreply@dahrk.ai";
|
|
1357
|
-
const
|
|
1360
|
+
const isBusy = opts.isBusy;
|
|
1361
|
+
const log = opts.logger ?? noopLogger;
|
|
1358
1362
|
const git = (cwd, args, env) => execFileSync("git", args, {
|
|
1359
1363
|
cwd,
|
|
1360
1364
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1366,7 +1370,7 @@ function createGitService(opts = {}) {
|
|
|
1366
1370
|
encoding: "utf-8",
|
|
1367
1371
|
...env ? { env } : {}
|
|
1368
1372
|
});
|
|
1369
|
-
const
|
|
1373
|
+
const gitOk2 = (cwd, args) => {
|
|
1370
1374
|
try {
|
|
1371
1375
|
git(cwd, args);
|
|
1372
1376
|
return true;
|
|
@@ -1393,14 +1397,14 @@ function createGitService(opts = {}) {
|
|
|
1393
1397
|
writeFileSync(excludePath, `${existing}${sep3}${entry}
|
|
1394
1398
|
`);
|
|
1395
1399
|
} catch (e) {
|
|
1396
|
-
|
|
1400
|
+
log.warn(`could not set worktree scratch exclude at ${worktreePath}: ${e.message}`);
|
|
1397
1401
|
}
|
|
1398
1402
|
};
|
|
1399
1403
|
const commitPending = (worktreePath, message) => {
|
|
1400
1404
|
excludeScratchLocally(worktreePath);
|
|
1401
1405
|
git(worktreePath, ["rm", "-r", "--cached", "--ignore-unmatch", "--quiet", SCRATCH_DIR]);
|
|
1402
1406
|
git(worktreePath, ["add", "-A", "--", "."]);
|
|
1403
|
-
const dirty = !
|
|
1407
|
+
const dirty = !gitOk2(worktreePath, ["diff", "--cached", "--quiet"]);
|
|
1404
1408
|
if (dirty) {
|
|
1405
1409
|
git(worktreePath, [
|
|
1406
1410
|
"-c",
|
|
@@ -1426,22 +1430,108 @@ function createGitService(opts = {}) {
|
|
|
1426
1430
|
const netEnv = (authEnv) => authEnv ?? { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
1427
1431
|
const withTokenUser = (gitUrl) => /^https:\/\/[^@/]+@/.test(gitUrl) ? gitUrl : gitUrl.replace(/^https:\/\//, "https://x-access-token@");
|
|
1428
1432
|
const mirrorPathFor = (repoId) => join2(mirrorsDir, sanitizeBranchName(repoId));
|
|
1433
|
+
const listWorktrees = (mirror) => {
|
|
1434
|
+
let out;
|
|
1435
|
+
try {
|
|
1436
|
+
out = git(mirror, ["worktree", "list", "--porcelain"]);
|
|
1437
|
+
} catch {
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
const entries = [];
|
|
1441
|
+
let path = "";
|
|
1442
|
+
for (const line of out.split("\n")) {
|
|
1443
|
+
if (line.startsWith("worktree ")) path = line.slice(9).trim();
|
|
1444
|
+
else if (line.startsWith("branch ")) {
|
|
1445
|
+
const branch = line.slice(7).trim().replace(/^refs\/heads\//, "");
|
|
1446
|
+
if (path) entries.push({ path, branch });
|
|
1447
|
+
} else if (line.trim() === "") path = "";
|
|
1448
|
+
}
|
|
1449
|
+
return entries;
|
|
1450
|
+
};
|
|
1451
|
+
const teardownWorktreePath = (mirror, worktreePath) => {
|
|
1452
|
+
try {
|
|
1453
|
+
git(mirror, ["worktree", "remove", "--force", worktreePath]);
|
|
1454
|
+
} catch (e) {
|
|
1455
|
+
log.warn(`git worktree remove failed for ${worktreePath}: ${e.message}`);
|
|
1456
|
+
}
|
|
1457
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
1458
|
+
gitOk2(mirror, ["worktree", "prune"]);
|
|
1459
|
+
};
|
|
1460
|
+
const TRACKING_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
|
|
1461
|
+
const migrateMirrorConfig = (mirror, repoId) => {
|
|
1462
|
+
const isMirror = gitOk2(mirror, ["config", "--get", "remote.origin.mirror"]);
|
|
1463
|
+
const refspecs = (() => {
|
|
1464
|
+
try {
|
|
1465
|
+
return git(mirror, ["config", "--get-all", "remote.origin.fetch"]).split("\n").map((s) => s.trim());
|
|
1466
|
+
} catch {
|
|
1467
|
+
return [];
|
|
1468
|
+
}
|
|
1469
|
+
})();
|
|
1470
|
+
if (!isMirror && refspecs.length === 1 && refspecs[0] === TRACKING_REFSPEC) return;
|
|
1471
|
+
log.info(`mirror ${repoId}: migrating to the tracking-refspec layout (was mirror=${isMirror})`);
|
|
1472
|
+
for (const args of [
|
|
1473
|
+
["config", "--unset-all", "remote.origin.mirror"],
|
|
1474
|
+
["config", "--unset-all", "remote.origin.push"]
|
|
1475
|
+
]) {
|
|
1476
|
+
gitOk2(mirror, args);
|
|
1477
|
+
}
|
|
1478
|
+
git(mirror, ["config", "--replace-all", "remote.origin.fetch", TRACKING_REFSPEC]);
|
|
1479
|
+
};
|
|
1480
|
+
const salvageOrphanedTip = (mirror, branchName, start2) => {
|
|
1481
|
+
try {
|
|
1482
|
+
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", `refs/heads/${branchName}`])) return;
|
|
1483
|
+
if (gitOk2(mirror, ["merge-base", "--is-ancestor", `refs/heads/${branchName}`, start2])) return;
|
|
1484
|
+
const sha = git(mirror, ["rev-parse", `refs/heads/${branchName}`]).trim();
|
|
1485
|
+
const ref = `refs/dahrk/salvage/${branchName}/${sha.slice(0, 12)}`;
|
|
1486
|
+
git(mirror, ["update-ref", ref, sha]);
|
|
1487
|
+
log.warn(`parked orphaned tip of ${branchName} (${sha.slice(0, 8)}) at ${ref} before reset`);
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
log.warn(`could not salvage tip of ${branchName}: ${e.message}`);
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
const gcShadowHeads = (mirror) => {
|
|
1493
|
+
let heads;
|
|
1494
|
+
try {
|
|
1495
|
+
heads = git(mirror, ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1496
|
+
} catch {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const held = new Set(listWorktrees(mirror).map((w) => w.branch));
|
|
1500
|
+
const ownHead = (() => {
|
|
1501
|
+
try {
|
|
1502
|
+
return git(mirror, ["symbolic-ref", "--quiet", "--short", "HEAD"]).trim();
|
|
1503
|
+
} catch {
|
|
1504
|
+
return "";
|
|
1505
|
+
}
|
|
1506
|
+
})();
|
|
1507
|
+
for (const h of heads) {
|
|
1508
|
+
if (h === ownHead || held.has(h)) continue;
|
|
1509
|
+
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", `refs/remotes/origin/${h}`])) continue;
|
|
1510
|
+
if (!gitOk2(mirror, ["merge-base", "--is-ancestor", `refs/heads/${h}`, `refs/remotes/origin/${h}`])) continue;
|
|
1511
|
+
gitOk2(mirror, ["branch", "-D", h]);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1429
1514
|
const ensureMirror = (repoId, gitUrl, authEnv) => {
|
|
1430
1515
|
const mirror = mirrorPathFor(repoId);
|
|
1431
|
-
if (existsSync(mirror) &&
|
|
1432
|
-
|
|
1516
|
+
if (existsSync(mirror) && gitOk2(mirror, ["rev-parse", "--git-dir"])) {
|
|
1517
|
+
log.info(`refreshing mirror ${repoId}`);
|
|
1433
1518
|
try {
|
|
1434
|
-
|
|
1519
|
+
migrateMirrorConfig(mirror, repoId);
|
|
1520
|
+
git(mirror, ["fetch", "--prune", "origin"], netEnv(authEnv));
|
|
1521
|
+
gcShadowHeads(mirror);
|
|
1435
1522
|
return { mirror, refreshed: true };
|
|
1436
1523
|
} catch (e) {
|
|
1437
|
-
|
|
1524
|
+
log.warn(`mirror fetch failed for ${repoId}: ${e.message}`);
|
|
1438
1525
|
return { mirror, refreshed: false };
|
|
1439
1526
|
}
|
|
1440
1527
|
}
|
|
1441
|
-
mkdirSync(
|
|
1528
|
+
mkdirSync(mirror, { recursive: true });
|
|
1442
1529
|
const cloneUrl = authEnv ? withTokenUser(gitUrl) : gitUrl;
|
|
1443
|
-
|
|
1444
|
-
gitBare(["
|
|
1530
|
+
log.info(`cloning mirror ${repoId} from ${gitUrl}`);
|
|
1531
|
+
gitBare(["init", "--bare", "--quiet", mirror]);
|
|
1532
|
+
git(mirror, ["remote", "add", "origin", cloneUrl]);
|
|
1533
|
+
git(mirror, ["config", "--replace-all", "remote.origin.fetch", TRACKING_REFSPEC]);
|
|
1534
|
+
git(mirror, ["fetch", "--prune", "origin"], netEnv(authEnv));
|
|
1445
1535
|
return { mirror, refreshed: true };
|
|
1446
1536
|
};
|
|
1447
1537
|
const refFor = (spec, worktreePath) => ({
|
|
@@ -1459,28 +1549,42 @@ function createGitService(opts = {}) {
|
|
|
1459
1549
|
const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
|
|
1460
1550
|
const worktreePath = join2(worktreesDir, runId);
|
|
1461
1551
|
mkdirSync(worktreesDir, { recursive: true });
|
|
1462
|
-
if (existsSync(worktreePath) &&
|
|
1463
|
-
|
|
1552
|
+
if (existsSync(worktreePath) && gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1553
|
+
log.info(`reusing existing worktree at ${worktreePath}`);
|
|
1464
1554
|
mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
|
|
1465
1555
|
return refFor(spec, worktreePath);
|
|
1466
1556
|
}
|
|
1467
1557
|
const auth = spec.credentialToken ? setupAuth(spec.credentialToken) : void 0;
|
|
1468
1558
|
try {
|
|
1469
1559
|
const { mirror, refreshed } = ensureMirror(repoId, gitUrl, auth?.env);
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1560
|
+
gitOk2(mirror, ["worktree", "prune"]);
|
|
1561
|
+
for (const w of listWorktrees(mirror)) {
|
|
1562
|
+
if (w.branch !== branchName || w.path === worktreePath) continue;
|
|
1563
|
+
const holder = basename(w.path);
|
|
1564
|
+
if (isBusy?.(holder)) {
|
|
1565
|
+
throw new Error(`branch ${branchName} is held by in-flight run ${holder} (${w.path})`);
|
|
1476
1566
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1567
|
+
log.warn(`clearing stale worktree ${w.path} which still claims ${branchName}`);
|
|
1568
|
+
teardownWorktreePath(mirror, w.path);
|
|
1569
|
+
}
|
|
1570
|
+
const remoteBranch = `refs/remotes/origin/${branchName}`;
|
|
1571
|
+
const remoteBase = `refs/remotes/origin/${baseBranch}`;
|
|
1572
|
+
if (!refreshed) {
|
|
1573
|
+
log.info(`mirror refresh failed; fetching base ${baseBranch} before branching ${branchName}`);
|
|
1574
|
+
git(mirror, ["fetch", "origin", `+refs/heads/${baseBranch}:${remoteBase}`], netEnv(auth?.env));
|
|
1575
|
+
}
|
|
1576
|
+
const seed = spec.seedRef && gitOk2(mirror, ["rev-parse", "--verify", "-q", spec.seedRef]) ? spec.seedRef : void 0;
|
|
1577
|
+
const start2 = seed ?? (gitOk2(mirror, ["rev-parse", "--verify", "-q", remoteBranch]) ? remoteBranch : remoteBase);
|
|
1578
|
+
if (!gitOk2(mirror, ["rev-parse", "--verify", "-q", start2])) {
|
|
1579
|
+
throw new Error(`start point '${start2}' does not resolve in mirror ${repoId}`);
|
|
1479
1580
|
}
|
|
1581
|
+
salvageOrphanedTip(mirror, branchName, start2);
|
|
1582
|
+
log.info(`creating worktree at ${worktreePath} from ${start2} on ${branchName}`);
|
|
1583
|
+
git(mirror, ["worktree", "add", "--force", "-B", branchName, worktreePath, start2]);
|
|
1480
1584
|
} finally {
|
|
1481
1585
|
auth?.cleanup();
|
|
1482
1586
|
}
|
|
1483
|
-
if (!
|
|
1587
|
+
if (!gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "HEAD"])) {
|
|
1484
1588
|
throw new Error(`base '${baseBranch}' did not materialise into ${worktreePath} (unborn HEAD)`);
|
|
1485
1589
|
}
|
|
1486
1590
|
mkdirSync(join2(worktreePath, ".skakel", "scratch"), { recursive: true });
|
|
@@ -1488,14 +1592,14 @@ function createGitService(opts = {}) {
|
|
|
1488
1592
|
},
|
|
1489
1593
|
async commitAndPush(ref, opts2) {
|
|
1490
1594
|
const { worktreePath } = ref;
|
|
1491
|
-
if (!existsSync(worktreePath) || !
|
|
1595
|
+
if (!existsSync(worktreePath) || !gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1492
1596
|
throw new Error(`worktree missing for push: ${worktreePath}`);
|
|
1493
1597
|
}
|
|
1494
1598
|
const branch = sanitizeBranchName(opts2.branch);
|
|
1495
1599
|
const { headSha: committedSha, dirty } = commitPending(worktreePath, opts2.message);
|
|
1496
1600
|
let headSha = committedSha;
|
|
1497
1601
|
let commitsAhead = 0;
|
|
1498
|
-
for (const baseRef of [
|
|
1602
|
+
for (const baseRef of ["FETCH_HEAD", `origin/${opts2.base}`, opts2.base, `refs/heads/${opts2.base}`]) {
|
|
1499
1603
|
if (!baseRef) continue;
|
|
1500
1604
|
try {
|
|
1501
1605
|
commitsAhead = Number.parseInt(git(worktreePath, ["rev-list", "--count", `${baseRef}..HEAD`]).trim(), 10) || 0;
|
|
@@ -1514,15 +1618,15 @@ function createGitService(opts = {}) {
|
|
|
1514
1618
|
git(worktreePath, ["fetch", remote, opts2.base], netEnv(auth?.env));
|
|
1515
1619
|
fetched = true;
|
|
1516
1620
|
} catch (e) {
|
|
1517
|
-
|
|
1621
|
+
log.warn(`base fetch failed for ${opts2.base}; skipping push-time integration: ${e.message}`);
|
|
1518
1622
|
}
|
|
1519
1623
|
}
|
|
1520
1624
|
if (fetched) {
|
|
1521
|
-
if (!
|
|
1625
|
+
if (!gitOk2(worktreePath, ["merge-base", "HEAD", "FETCH_HEAD"])) {
|
|
1522
1626
|
return { headSha, pushed: false, nothingToCommit: !dirty, commitsAhead, integration: "diverged" };
|
|
1523
1627
|
}
|
|
1524
1628
|
const delta = git(worktreePath, ["diff", "--name-only", "FETCH_HEAD...HEAD"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1525
|
-
const isScratchPath = (p) => p === SCRATCH_DIR || p.startsWith(`${SCRATCH_DIR}/`) ||
|
|
1629
|
+
const isScratchPath = (p) => p === SCRATCH_DIR || p.startsWith(`${SCRATCH_DIR}/`) || gitOk2(worktreePath, ["check-ignore", "-q", "--no-index", "--", p]);
|
|
1526
1630
|
if (!delta.some((p) => !isScratchPath(p))) {
|
|
1527
1631
|
return { headSha, pushed: false, nothingToCommit: true, commitsAhead, integration: "noop" };
|
|
1528
1632
|
}
|
|
@@ -1539,7 +1643,7 @@ function createGitService(opts = {}) {
|
|
|
1539
1643
|
integration = "clean";
|
|
1540
1644
|
headSha = git(worktreePath, ["rev-parse", "HEAD"]).trim();
|
|
1541
1645
|
} catch (mergeErr) {
|
|
1542
|
-
const inMerge =
|
|
1646
|
+
const inMerge = gitOk2(worktreePath, ["rev-parse", "--verify", "-q", "MERGE_HEAD"]);
|
|
1543
1647
|
if (inMerge) {
|
|
1544
1648
|
const conflictFiles = git(worktreePath, ["diff", "--name-only", "--diff-filter=U"]).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1545
1649
|
git(worktreePath, ["merge", "--abort"]);
|
|
@@ -1561,7 +1665,7 @@ function createGitService(opts = {}) {
|
|
|
1561
1665
|
},
|
|
1562
1666
|
async backupPush(ref, opts2) {
|
|
1563
1667
|
const { worktreePath } = ref;
|
|
1564
|
-
if (!existsSync(worktreePath) || !
|
|
1668
|
+
if (!existsSync(worktreePath) || !gitOk2(worktreePath, ["rev-parse", "--git-dir"])) {
|
|
1565
1669
|
throw new Error(`worktree missing for backup push: ${worktreePath}`);
|
|
1566
1670
|
}
|
|
1567
1671
|
const wipRef = sanitizeBranchName(opts2.branch);
|
|
@@ -1618,18 +1722,145 @@ function createGitService(opts = {}) {
|
|
|
1618
1722
|
}
|
|
1619
1723
|
},
|
|
1620
1724
|
async teardownWorktree(ref) {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1725
|
+
teardownWorktreePath(mirrorPathFor(ref.repoId), ref.worktreePath);
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ../../packages/executor-worktree/src/worktree-reaper.ts
|
|
1731
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1732
|
+
import { existsSync as existsSync2, readdirSync, realpathSync, rmSync as rmSync2, statSync } from "fs";
|
|
1733
|
+
import { join as join3 } from "path";
|
|
1734
|
+
var MINUTE = 6e4;
|
|
1735
|
+
var HOUR = 60 * MINUTE;
|
|
1736
|
+
var DEFAULTS = {
|
|
1737
|
+
// Deliberately non-optional defaults. "No policy configured" used to mean "never collect anything",
|
|
1738
|
+
// which is precisely how the disk reached 65 GB. Absent config must mean sane collection, not none.
|
|
1739
|
+
maxRuns: 20,
|
|
1740
|
+
maxIdleMs: 6 * HOUR,
|
|
1741
|
+
activityGraceMs: 30 * MINUTE
|
|
1742
|
+
};
|
|
1743
|
+
var noop = { info: () => {
|
|
1744
|
+
}, warn: () => {
|
|
1745
|
+
} };
|
|
1746
|
+
var gitOk = (cwd, args) => {
|
|
1747
|
+
try {
|
|
1748
|
+
execFileSync2("git", args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
1749
|
+
return true;
|
|
1750
|
+
} catch {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
var gitOut = (cwd, args) => execFileSync2("git", args, { cwd, stdio: ["pipe", "pipe", "pipe"], encoding: "utf-8" });
|
|
1755
|
+
var canonical = (p) => {
|
|
1756
|
+
try {
|
|
1757
|
+
return realpathSync(p);
|
|
1758
|
+
} catch {
|
|
1759
|
+
return p;
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
function lastUsedMs(worktreePath) {
|
|
1763
|
+
const candidates = [join3(worktreePath, ".skakel", "scratch", "state.json"), worktreePath];
|
|
1764
|
+
let newest = 0;
|
|
1765
|
+
for (const p of candidates) {
|
|
1766
|
+
try {
|
|
1767
|
+
newest = Math.max(newest, statSync(p).mtimeMs);
|
|
1768
|
+
} catch {
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return newest;
|
|
1772
|
+
}
|
|
1773
|
+
function registeredWorktrees(mirror) {
|
|
1774
|
+
let out;
|
|
1775
|
+
try {
|
|
1776
|
+
out = gitOut(mirror, ["worktree", "list", "--porcelain"]);
|
|
1777
|
+
} catch {
|
|
1778
|
+
return [];
|
|
1779
|
+
}
|
|
1780
|
+
return out.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => canonical(l.slice(9).trim())).filter((p) => p && p !== canonical(mirror));
|
|
1781
|
+
}
|
|
1782
|
+
function createWorktreeReaper(opts) {
|
|
1783
|
+
const log = opts.logger ?? noop;
|
|
1784
|
+
const mirrors = () => {
|
|
1785
|
+
try {
|
|
1786
|
+
return readdirSync(opts.mirrorsDir).map((d) => join3(opts.mirrorsDir, d)).filter((m) => gitOk(m, ["rev-parse", "--git-dir"]));
|
|
1787
|
+
} catch {
|
|
1788
|
+
return [];
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
const ownerOf = (worktreePath, all) => {
|
|
1792
|
+
for (const [mirror, paths] of all) if (paths.includes(worktreePath)) return mirror;
|
|
1793
|
+
return void 0;
|
|
1794
|
+
};
|
|
1795
|
+
return {
|
|
1796
|
+
async reap(policy = {}) {
|
|
1797
|
+
const maxRuns = policy.maxRuns ?? DEFAULTS.maxRuns;
|
|
1798
|
+
const maxIdleMs = policy.maxIdleMs ?? DEFAULTS.maxIdleMs;
|
|
1799
|
+
const graceMs = policy.activityGraceMs ?? DEFAULTS.activityGraceMs;
|
|
1800
|
+
const dryRun = policy.dryRun ?? false;
|
|
1801
|
+
const report = { scanned: 0, reaped: [], skipped: 0, errors: [] };
|
|
1802
|
+
const now = Date.now();
|
|
1803
|
+
const registered = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const m of mirrors()) {
|
|
1805
|
+
gitOk(m, ["worktree", "prune"]);
|
|
1806
|
+
registered.set(m, registeredWorktrees(m));
|
|
1627
1807
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1808
|
+
const onDisk = (() => {
|
|
1809
|
+
try {
|
|
1810
|
+
return readdirSync(opts.worktreesDir).map((d) => canonical(join3(opts.worktreesDir, d)));
|
|
1811
|
+
} catch {
|
|
1812
|
+
return [];
|
|
1813
|
+
}
|
|
1814
|
+
})();
|
|
1815
|
+
const candidates = [.../* @__PURE__ */ new Set([...onDisk, ...[...registered.values()].flat()])];
|
|
1816
|
+
const entries = [];
|
|
1817
|
+
for (const path of candidates) {
|
|
1818
|
+
report.scanned++;
|
|
1819
|
+
const runId = path.split("/").pop() ?? path;
|
|
1820
|
+
if (opts.isBusy?.(runId)) {
|
|
1821
|
+
report.skipped++;
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
const idleMs = now - lastUsedMs(path);
|
|
1825
|
+
if (idleMs < graceMs) {
|
|
1826
|
+
report.skipped++;
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
const broken = !existsSync2(path) || !gitOk(path, ["rev-parse", "--verify", "-q", "HEAD"]);
|
|
1830
|
+
entries.push({ path, runId, idleMs, broken, ...ownerOf(path, registered) ? { mirror: ownerOf(path, registered) } : {} });
|
|
1831
|
+
}
|
|
1832
|
+
entries.sort((a, b) => b.idleMs - a.idleMs);
|
|
1833
|
+
const keep = entries.filter((e) => !e.broken && e.idleMs <= maxIdleMs);
|
|
1834
|
+
const doomed = [];
|
|
1835
|
+
for (const e of entries) {
|
|
1836
|
+
if (e.broken) doomed.push({ ...e, reason: "broken" });
|
|
1837
|
+
else if (e.idleMs > maxIdleMs) doomed.push({ ...e, reason: "idle" });
|
|
1838
|
+
}
|
|
1839
|
+
const survivors = keep.filter((e) => !doomed.some((d) => d.path === e.path));
|
|
1840
|
+
for (const e of survivors.slice(0, Math.max(0, survivors.length - maxRuns))) {
|
|
1841
|
+
doomed.push({ ...e, reason: "over-count" });
|
|
1632
1842
|
}
|
|
1843
|
+
for (const d of doomed) {
|
|
1844
|
+
if (dryRun) {
|
|
1845
|
+
log.info(`reaper (dry-run): would reap ${d.runId} (${d.reason}, idle ${Math.round(d.idleMs / MINUTE)}m)`);
|
|
1846
|
+
report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
try {
|
|
1850
|
+
if (d.mirror) {
|
|
1851
|
+
gitOk(d.mirror, ["worktree", "remove", "--force", d.path]);
|
|
1852
|
+
}
|
|
1853
|
+
rmSync2(d.path, { recursive: true, force: true });
|
|
1854
|
+
if (d.mirror) gitOk(d.mirror, ["worktree", "prune"]);
|
|
1855
|
+
report.reaped.push({ runId: d.runId, path: d.path, reason: d.reason });
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
report.errors.push(`${d.runId}: ${e.message}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (report.reaped.length) {
|
|
1861
|
+
log.info(`reaper: reaped ${report.reaped.length} worktree(s), skipped ${report.skipped}`);
|
|
1862
|
+
}
|
|
1863
|
+
return report;
|
|
1633
1864
|
}
|
|
1634
1865
|
};
|
|
1635
1866
|
}
|
|
@@ -1637,15 +1868,15 @@ function createGitService(opts = {}) {
|
|
|
1637
1868
|
// ../../packages/executor-worktree/src/trace-writer.ts
|
|
1638
1869
|
import { createHash } from "crypto";
|
|
1639
1870
|
import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1640
|
-
import { join as
|
|
1871
|
+
import { join as join4 } from "path";
|
|
1641
1872
|
var DEFAULT_SPILL_BYTES = 8192;
|
|
1642
1873
|
function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
1643
1874
|
const spillBytes = opts.spillBytes ?? DEFAULT_SPILL_BYTES;
|
|
1644
|
-
const dir =
|
|
1645
|
-
mkdirSync2(
|
|
1646
|
-
mkdirSync2(
|
|
1647
|
-
const tracePath =
|
|
1648
|
-
const metaPath =
|
|
1875
|
+
const dir = join4(scratchPath, "traces", meta.stageId, `attempt-${meta.attempt}`);
|
|
1876
|
+
mkdirSync2(join4(dir, "blobs"), { recursive: true });
|
|
1877
|
+
mkdirSync2(join4(dir, "raw"), { recursive: true });
|
|
1878
|
+
const tracePath = join4(dir, "trace.jsonl");
|
|
1879
|
+
const metaPath = join4(dir, "meta.json");
|
|
1649
1880
|
let current = { ...meta };
|
|
1650
1881
|
let nextSeq = 0;
|
|
1651
1882
|
let rawCount = 0;
|
|
@@ -1657,8 +1888,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
1657
1888
|
const spillValue = (value) => {
|
|
1658
1889
|
const data = typeof value === "string" ? value : JSON.stringify(value);
|
|
1659
1890
|
const sha = createHash("sha256").update(data).digest("hex");
|
|
1660
|
-
writeFileSync2(
|
|
1661
|
-
return
|
|
1891
|
+
writeFileSync2(join4(dir, "blobs", sha), data);
|
|
1892
|
+
return join4("blobs", sha);
|
|
1662
1893
|
};
|
|
1663
1894
|
const spill = (event) => {
|
|
1664
1895
|
if (event.type === "thought" && event.text !== void 0 && tooBig(event.text)) {
|
|
@@ -1688,8 +1919,8 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
1688
1919
|
return written;
|
|
1689
1920
|
},
|
|
1690
1921
|
writeRaw(record) {
|
|
1691
|
-
const rel =
|
|
1692
|
-
writeFileSync2(
|
|
1922
|
+
const rel = join4("raw", `${rawCount++}.json`);
|
|
1923
|
+
writeFileSync2(join4(dir, rel), JSON.stringify(record, null, 2));
|
|
1693
1924
|
return rel;
|
|
1694
1925
|
},
|
|
1695
1926
|
finalise(patch = {}) {
|
|
@@ -1704,13 +1935,13 @@ function createTraceWriter(scratchPath, meta, opts = {}) {
|
|
|
1704
1935
|
|
|
1705
1936
|
// ../../packages/executor-worktree/src/pack-cache.ts
|
|
1706
1937
|
import { createHash as createHash2 } from "crypto";
|
|
1707
|
-
import { cpSync, existsSync as
|
|
1708
|
-
import { dirname as dirname2, join as
|
|
1938
|
+
import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1939
|
+
import { dirname as dirname2, join as join5, relative, sep } from "path";
|
|
1709
1940
|
function readManifestFiles(dir) {
|
|
1710
1941
|
const out = [];
|
|
1711
1942
|
const walk = (cur) => {
|
|
1712
|
-
for (const entry of
|
|
1713
|
-
const abs =
|
|
1943
|
+
for (const entry of readdirSync2(cur, { withFileTypes: true })) {
|
|
1944
|
+
const abs = join5(cur, entry.name);
|
|
1714
1945
|
if (entry.isDirectory()) walk(abs);
|
|
1715
1946
|
else out.push(relative(dir, abs).split(sep).join("/"));
|
|
1716
1947
|
}
|
|
@@ -1720,8 +1951,8 @@ function readManifestFiles(dir) {
|
|
|
1720
1951
|
}
|
|
1721
1952
|
|
|
1722
1953
|
// ../../packages/executor-worktree/src/overlay.ts
|
|
1723
|
-
import { existsSync as
|
|
1724
|
-
import { dirname as dirname3, join as
|
|
1954
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1955
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
1725
1956
|
function sameBytes(dest, bytes) {
|
|
1726
1957
|
try {
|
|
1727
1958
|
return readFileSync3(dest).equals(bytes);
|
|
@@ -1741,10 +1972,10 @@ async function overlayComponents(opts) {
|
|
|
1741
1972
|
}
|
|
1742
1973
|
const { dir } = await cache.materialise(ref);
|
|
1743
1974
|
for (const relPath of readManifestFiles(dir)) {
|
|
1744
|
-
const src =
|
|
1745
|
-
const dest =
|
|
1975
|
+
const src = join6(dir, relPath);
|
|
1976
|
+
const dest = join6(worktreePath, relPath);
|
|
1746
1977
|
const bytes = readFileSync3(src);
|
|
1747
|
-
if (
|
|
1978
|
+
if (existsSync4(dest)) {
|
|
1748
1979
|
if (sameBytes(dest, bytes)) continue;
|
|
1749
1980
|
result.skippedRepoLocal.push(relPath);
|
|
1750
1981
|
continue;
|
|
@@ -1773,6 +2004,386 @@ function makeRunner(runtime) {
|
|
|
1773
2004
|
return runtime === "codex" ? createCodexRunner() : createClaudeRunner();
|
|
1774
2005
|
}
|
|
1775
2006
|
|
|
2007
|
+
// ../../packages/edge/src/health.ts
|
|
2008
|
+
import { statfsSync } from "fs";
|
|
2009
|
+
var HealthCounters = class {
|
|
2010
|
+
startedAt = Date.now();
|
|
2011
|
+
errors = /* @__PURE__ */ new Map();
|
|
2012
|
+
crashes = 0;
|
|
2013
|
+
/** Connections made this process lifetime. The flapping signal: this climbing while uptime ALSO climbs
|
|
2014
|
+
* means the socket keeps dropping under a process that is otherwise fine. */
|
|
2015
|
+
connectCount = 0;
|
|
2016
|
+
/** Jobs in flight. A node stuck at 1 for an hour is wedged, and nothing else reveals it. */
|
|
2017
|
+
activeJobs = 0;
|
|
2018
|
+
/** Worktrees on disk. Climbing without bound was the shape of the leak DHK-371 fixed. */
|
|
2019
|
+
worktreeCount = 0;
|
|
2020
|
+
/** Bucket a stage failure. The CLASS only - the message would carry their paths. */
|
|
2021
|
+
recordError(kind) {
|
|
2022
|
+
this.errors.set(kind, (this.errors.get(kind) ?? 0) + 1);
|
|
2023
|
+
}
|
|
2024
|
+
recordCrash() {
|
|
2025
|
+
this.crashes++;
|
|
2026
|
+
}
|
|
2027
|
+
uptimeSec() {
|
|
2028
|
+
return Math.round((Date.now() - this.startedAt) / 1e3);
|
|
2029
|
+
}
|
|
2030
|
+
errorCounts() {
|
|
2031
|
+
return Object.fromEntries(this.errors);
|
|
2032
|
+
}
|
|
2033
|
+
crashCount() {
|
|
2034
|
+
return this.crashes;
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
function diskFreeBytes(path) {
|
|
2038
|
+
try {
|
|
2039
|
+
const st = statfsSync(path);
|
|
2040
|
+
return Number(st.bfree) * Number(st.bsize);
|
|
2041
|
+
} catch {
|
|
2042
|
+
return void 0;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function collectHealth(inputs) {
|
|
2046
|
+
const { counters } = inputs;
|
|
2047
|
+
const free = diskFreeBytes(inputs.worktreesDir);
|
|
2048
|
+
const errors = counters.errorCounts();
|
|
2049
|
+
return {
|
|
2050
|
+
uptimeSec: counters.uptimeSec(),
|
|
2051
|
+
clientVersion: inputs.clientVersion,
|
|
2052
|
+
activeJobs: counters.activeJobs,
|
|
2053
|
+
connectCount: counters.connectCount,
|
|
2054
|
+
worktreeCount: counters.worktreeCount,
|
|
2055
|
+
...free !== void 0 ? { diskFreeBytes: free } : {},
|
|
2056
|
+
runtimes: inputs.runtimes,
|
|
2057
|
+
...Object.keys(errors).length > 0 ? { errors } : {},
|
|
2058
|
+
...counters.crashCount() > 0 ? { crashes: counters.crashCount() } : {}
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// ../../packages/edge/src/log-shipper.ts
|
|
2063
|
+
import { shouldShip } from "@dahrk/contracts";
|
|
2064
|
+
var SHIP_BUFFER_MAX = 500;
|
|
2065
|
+
var SHIP_BATCH_MAX = 200;
|
|
2066
|
+
var SHIP_FLUSH_MS = 2e3;
|
|
2067
|
+
var LogShipper = class {
|
|
2068
|
+
constructor(opts = {}) {
|
|
2069
|
+
this.opts = opts;
|
|
2070
|
+
this.ceiling = opts.ceiling ?? { health: true, logs: "debug" };
|
|
2071
|
+
this.policy = this.clampToCeiling(opts.initial ?? { health: true, logs: "off" });
|
|
2072
|
+
}
|
|
2073
|
+
opts;
|
|
2074
|
+
buffer = [];
|
|
2075
|
+
policy;
|
|
2076
|
+
ceiling;
|
|
2077
|
+
timer;
|
|
2078
|
+
/** Attached once the socket exists. The shipper is constructed FIRST, because it has to be a pino
|
|
2079
|
+
* stream before the logger is built, which is before the client is started. Until `attach`, records
|
|
2080
|
+
* simply accumulate in the (bounded) ring. */
|
|
2081
|
+
send;
|
|
2082
|
+
/** Records we threw away because the buffer was full. Reported, never hidden. */
|
|
2083
|
+
dropped = 0;
|
|
2084
|
+
/** Give the shipper its transport. Called by the client once the socket exists. */
|
|
2085
|
+
attach(send) {
|
|
2086
|
+
this.send = send;
|
|
2087
|
+
}
|
|
2088
|
+
/** The hub may only ever ask for LESS than the local ceiling allows. */
|
|
2089
|
+
clampToCeiling(p) {
|
|
2090
|
+
const rank = { off: 0, error: 1, warn: 2, info: 3, debug: 4 };
|
|
2091
|
+
const logs = rank[p.logs] > rank[this.ceiling.logs] ? this.ceiling.logs : p.logs;
|
|
2092
|
+
return { health: p.health && this.ceiling.health, logs };
|
|
2093
|
+
}
|
|
2094
|
+
/** Apply a policy from the hub (`welcome`, or a live `policy` frame). */
|
|
2095
|
+
setPolicy(p) {
|
|
2096
|
+
this.policy = this.clampToCeiling(p);
|
|
2097
|
+
if (this.policy.logs === "off") this.buffer = [];
|
|
2098
|
+
}
|
|
2099
|
+
current() {
|
|
2100
|
+
return this.policy;
|
|
2101
|
+
}
|
|
2102
|
+
/** Offer a record. Cheap and synchronous - this sits on the logging hot path. */
|
|
2103
|
+
offer(record) {
|
|
2104
|
+
if (this.policy.logs === "off") return;
|
|
2105
|
+
if (!shouldShip(record.level, this.policy.logs)) return;
|
|
2106
|
+
this.buffer.push(record);
|
|
2107
|
+
if (this.buffer.length > (this.opts.bufferMax ?? SHIP_BUFFER_MAX)) {
|
|
2108
|
+
this.buffer.shift();
|
|
2109
|
+
this.dropped++;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
/** Send one batch if there is anything to send and the socket will take it. */
|
|
2113
|
+
flush() {
|
|
2114
|
+
if (this.buffer.length === 0 || !this.send) return;
|
|
2115
|
+
const batch = this.buffer.slice(0, SHIP_BATCH_MAX);
|
|
2116
|
+
if (!this.send(batch)) return;
|
|
2117
|
+
this.buffer = this.buffer.slice(batch.length);
|
|
2118
|
+
}
|
|
2119
|
+
start() {
|
|
2120
|
+
if (this.timer) return;
|
|
2121
|
+
this.timer = setInterval(() => this.flush(), this.opts.flushMs ?? SHIP_FLUSH_MS);
|
|
2122
|
+
this.timer.unref?.();
|
|
2123
|
+
}
|
|
2124
|
+
stop() {
|
|
2125
|
+
if (this.timer) clearInterval(this.timer);
|
|
2126
|
+
this.timer = void 0;
|
|
2127
|
+
}
|
|
2128
|
+
/** For the health report and for tests. */
|
|
2129
|
+
pending() {
|
|
2130
|
+
return this.buffer.length;
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
function shipperStream(shipper) {
|
|
2134
|
+
return {
|
|
2135
|
+
write(chunk) {
|
|
2136
|
+
try {
|
|
2137
|
+
const r = JSON.parse(chunk);
|
|
2138
|
+
if (!r.msg) return;
|
|
2139
|
+
shipper.offer({
|
|
2140
|
+
level: r.level,
|
|
2141
|
+
time: r.time,
|
|
2142
|
+
msg: r.msg,
|
|
2143
|
+
...r.runId ? { runId: r.runId } : {},
|
|
2144
|
+
...r.stageId ? { stageId: r.stageId } : {},
|
|
2145
|
+
...r.jobId ? { jobId: r.jobId } : {},
|
|
2146
|
+
...r.component ? { component: r.component } : {},
|
|
2147
|
+
...r.err ? { err: r.err } : {}
|
|
2148
|
+
});
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function ceilingFromEnv(env) {
|
|
2155
|
+
const v = env.DAHRK_TELEMETRY?.toLowerCase();
|
|
2156
|
+
if (v === "off" || v === "0" || v === "false") return { health: false, logs: "off" };
|
|
2157
|
+
if (v === "health") return { health: true, logs: "off" };
|
|
2158
|
+
return { health: true, logs: "debug" };
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// ../../packages/edge/src/logger.ts
|
|
2162
|
+
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, renameSync, rmSync as rmSync4, statSync as statSync2, writeSync } from "fs";
|
|
2163
|
+
import { join as join7 } from "path";
|
|
2164
|
+
import pino from "pino";
|
|
2165
|
+
|
|
2166
|
+
// ../../packages/edge/src/redact.ts
|
|
2167
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
2168
|
+
"token",
|
|
2169
|
+
"secret",
|
|
2170
|
+
"password",
|
|
2171
|
+
"passwd",
|
|
2172
|
+
"apikey",
|
|
2173
|
+
"api_key",
|
|
2174
|
+
"authorization",
|
|
2175
|
+
"auth_header",
|
|
2176
|
+
"cookie",
|
|
2177
|
+
"private_key",
|
|
2178
|
+
"privatekey",
|
|
2179
|
+
"client_secret",
|
|
2180
|
+
"clientsecret",
|
|
2181
|
+
"refresh_token",
|
|
2182
|
+
"access_token",
|
|
2183
|
+
"bearer",
|
|
2184
|
+
"credential",
|
|
2185
|
+
"enroltoken",
|
|
2186
|
+
"enrol_token"
|
|
2187
|
+
];
|
|
2188
|
+
var REDACTED = "[REDACTED]";
|
|
2189
|
+
var MAX_DEPTH = 8;
|
|
2190
|
+
var KEY_ALLOWLIST = /* @__PURE__ */ new Set(["credentialmode"]);
|
|
2191
|
+
function isSensitiveKey(key) {
|
|
2192
|
+
const lower = key.toLowerCase();
|
|
2193
|
+
if (KEY_ALLOWLIST.has(lower)) return false;
|
|
2194
|
+
return SENSITIVE_KEY_PATTERNS.some((p) => lower.includes(p));
|
|
2195
|
+
}
|
|
2196
|
+
var INLINE_TOKEN_RE = /\b(github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[abprs]-[A-Za-z0-9-]{10,}|lin_(?:api|oauth)_[A-Za-z0-9]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16})\b/g;
|
|
2197
|
+
var URL_CREDENTIAL_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/gi;
|
|
2198
|
+
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g;
|
|
2199
|
+
function scrubString(s) {
|
|
2200
|
+
return s.replace(URL_CREDENTIAL_RE, `$1$2:${REDACTED}@`).replace(INLINE_TOKEN_RE, REDACTED).replace(JWT_RE, REDACTED).replace(/Bearer\s+[A-Za-z0-9._\-+/=]{16,}/gi, `Bearer ${REDACTED}`).replace(/([?&](?:token|access_token|api_key|key|secret)=)[^&\s"']+/gi, `$1${REDACTED}`);
|
|
2201
|
+
}
|
|
2202
|
+
function looksLikeWholeToken(s) {
|
|
2203
|
+
if (s.length < 20 || /\s/.test(s)) return false;
|
|
2204
|
+
return /^(gh[pousr]|github_pat)_/i.test(s) || /^xox[abprs]-/i.test(s) || /^glpat-/i.test(s) || /^lin_(api|oauth)_/i.test(s) || /^sk-/i.test(s);
|
|
2205
|
+
}
|
|
2206
|
+
function scrubValue(value, depth = 0) {
|
|
2207
|
+
if (depth > MAX_DEPTH) return REDACTED;
|
|
2208
|
+
if (value === null || value === void 0) return value;
|
|
2209
|
+
if (typeof value === "string") {
|
|
2210
|
+
return looksLikeWholeToken(value) ? REDACTED : scrubString(value);
|
|
2211
|
+
}
|
|
2212
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value;
|
|
2213
|
+
if (value instanceof Error) {
|
|
2214
|
+
const out = new Error(scrubString(value.message));
|
|
2215
|
+
out.name = value.name;
|
|
2216
|
+
if (value.stack) out.stack = scrubString(value.stack);
|
|
2217
|
+
return out;
|
|
2218
|
+
}
|
|
2219
|
+
if (Array.isArray(value)) return value.map((v) => scrubValue(v, depth + 1));
|
|
2220
|
+
if (typeof value === "object") {
|
|
2221
|
+
const out = {};
|
|
2222
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2223
|
+
out[k] = isSensitiveKey(k) ? REDACTED : scrubValue(v, depth + 1);
|
|
2224
|
+
}
|
|
2225
|
+
return out;
|
|
2226
|
+
}
|
|
2227
|
+
return value;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// ../../packages/edge/src/logger.ts
|
|
2231
|
+
var LEVELS = ["trace", "debug", "info", "warn", "error", "fatal", "silent"];
|
|
2232
|
+
var isLevel = (v) => v !== void 0 && LEVELS.includes(v);
|
|
2233
|
+
function levelFromEnv(env) {
|
|
2234
|
+
const v = env.DAHRK_LOG_LEVEL?.toLowerCase();
|
|
2235
|
+
return isLevel(v) ? v : "info";
|
|
2236
|
+
}
|
|
2237
|
+
function fileLevelFromEnv(env) {
|
|
2238
|
+
const off = env.DAHRK_LOG_FILE?.toLowerCase();
|
|
2239
|
+
if (off === "0" || off === "off" || off === "false") return "off";
|
|
2240
|
+
const v = env.DAHRK_LOG_FILE_LEVEL?.toLowerCase();
|
|
2241
|
+
return isLevel(v) ? v : "debug";
|
|
2242
|
+
}
|
|
2243
|
+
var RotatingFile = class {
|
|
2244
|
+
constructor(path, maxBytes, maxFiles) {
|
|
2245
|
+
this.path = path;
|
|
2246
|
+
this.maxBytes = maxBytes;
|
|
2247
|
+
this.maxFiles = maxFiles;
|
|
2248
|
+
this.open();
|
|
2249
|
+
}
|
|
2250
|
+
path;
|
|
2251
|
+
maxBytes;
|
|
2252
|
+
maxFiles;
|
|
2253
|
+
fd;
|
|
2254
|
+
size = 0;
|
|
2255
|
+
failed = false;
|
|
2256
|
+
open() {
|
|
2257
|
+
try {
|
|
2258
|
+
this.fd = openSync(this.path, "a");
|
|
2259
|
+
this.size = existsSync5(this.path) ? statSync2(this.path).size : 0;
|
|
2260
|
+
} catch (e) {
|
|
2261
|
+
this.disable(e);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
/** Report once, then go quiet. A logger that spams stderr about being unable to log is worse than one
|
|
2265
|
+
* that silently degrades. */
|
|
2266
|
+
disable(e) {
|
|
2267
|
+
if (!this.failed) {
|
|
2268
|
+
this.failed = true;
|
|
2269
|
+
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
2270
|
+
`);
|
|
2271
|
+
}
|
|
2272
|
+
if (this.fd !== void 0) {
|
|
2273
|
+
try {
|
|
2274
|
+
closeSync(this.fd);
|
|
2275
|
+
} catch {
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
this.fd = void 0;
|
|
2279
|
+
}
|
|
2280
|
+
/** node.jsonl -> node.jsonl.1, .1 -> .2, ... dropping the oldest. */
|
|
2281
|
+
rotate() {
|
|
2282
|
+
if (this.fd === void 0) return;
|
|
2283
|
+
try {
|
|
2284
|
+
closeSync(this.fd);
|
|
2285
|
+
this.fd = void 0;
|
|
2286
|
+
const oldest = `${this.path}.${this.maxFiles}`;
|
|
2287
|
+
if (existsSync5(oldest)) rmSync4(oldest, { force: true });
|
|
2288
|
+
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
2289
|
+
const from = `${this.path}.${i}`;
|
|
2290
|
+
if (existsSync5(from)) renameSync(from, `${this.path}.${i + 1}`);
|
|
2291
|
+
}
|
|
2292
|
+
if (existsSync5(this.path)) renameSync(this.path, `${this.path}.1`);
|
|
2293
|
+
this.open();
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
this.disable(e);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
write(line) {
|
|
2299
|
+
if (this.failed) return;
|
|
2300
|
+
if (this.fd === void 0) return;
|
|
2301
|
+
try {
|
|
2302
|
+
const buf = Buffer.from(line, "utf8");
|
|
2303
|
+
if (this.size > 0 && this.size + buf.length > this.maxBytes) {
|
|
2304
|
+
this.rotate();
|
|
2305
|
+
if (this.fd === void 0) return;
|
|
2306
|
+
}
|
|
2307
|
+
writeSync(this.fd, buf);
|
|
2308
|
+
this.size += buf.length;
|
|
2309
|
+
} catch (e) {
|
|
2310
|
+
this.disable(e);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
var humanSink = {
|
|
2315
|
+
write(chunk) {
|
|
2316
|
+
let msg;
|
|
2317
|
+
try {
|
|
2318
|
+
msg = JSON.parse(chunk).msg ?? "";
|
|
2319
|
+
} catch {
|
|
2320
|
+
msg = chunk.trimEnd();
|
|
2321
|
+
}
|
|
2322
|
+
if (!msg) return;
|
|
2323
|
+
try {
|
|
2324
|
+
process.stdout.write(`${msg}
|
|
2325
|
+
`);
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
var pipeGuardInstalled = false;
|
|
2331
|
+
function guardBrokenPipe() {
|
|
2332
|
+
if (pipeGuardInstalled) return;
|
|
2333
|
+
pipeGuardInstalled = true;
|
|
2334
|
+
const ignoreEpipe = (err) => {
|
|
2335
|
+
if (err.code !== "EPIPE") throw err;
|
|
2336
|
+
};
|
|
2337
|
+
process.stdout.on("error", ignoreEpipe);
|
|
2338
|
+
process.stderr.on("error", ignoreEpipe);
|
|
2339
|
+
}
|
|
2340
|
+
function lowest(a, b) {
|
|
2341
|
+
const rank = (l) => LEVELS.indexOf(l);
|
|
2342
|
+
return rank(a) <= rank(b) ? a : b;
|
|
2343
|
+
}
|
|
2344
|
+
function createNodeLogger(opts = {}) {
|
|
2345
|
+
guardBrokenPipe();
|
|
2346
|
+
const level = opts.level ?? "info";
|
|
2347
|
+
const fileLevel = opts.fileLevel ?? "debug";
|
|
2348
|
+
const streams = [];
|
|
2349
|
+
if (level !== "silent") streams.push({ level, stream: humanSink });
|
|
2350
|
+
if (opts.dir && fileLevel !== "silent") {
|
|
2351
|
+
try {
|
|
2352
|
+
mkdirSync5(opts.dir, { recursive: true, mode: 448 });
|
|
2353
|
+
const file = new RotatingFile(join7(opts.dir, "node.jsonl"), opts.maxBytes ?? 10 * 1024 * 1024, opts.maxFiles ?? 5);
|
|
2354
|
+
streams.push({ level: fileLevel, stream: file });
|
|
2355
|
+
} catch (e) {
|
|
2356
|
+
process.stderr.write(`dahrk: file logging disabled (${e.message})
|
|
2357
|
+
`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (opts.ship) streams.push({ level: "trace", stream: opts.ship });
|
|
2361
|
+
const base = streams.length === 0 ? "silent" : opts.ship ? "trace" : opts.dir ? lowest(level, fileLevel) : level;
|
|
2362
|
+
return pino(
|
|
2363
|
+
{
|
|
2364
|
+
level: base,
|
|
2365
|
+
base: opts.base ?? {},
|
|
2366
|
+
// ISO time reads better than epoch millis in a support bundle a human is scanning.
|
|
2367
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2368
|
+
hooks: {
|
|
2369
|
+
logMethod(args, method) {
|
|
2370
|
+
method.apply(this, scrubValue(args));
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
pino.multistream(streams)
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
function createNodeLoggerFromEnv(env, dir, base, ship) {
|
|
2378
|
+
const fileLevel = fileLevelFromEnv(env);
|
|
2379
|
+
return createNodeLogger({
|
|
2380
|
+
level: levelFromEnv(env),
|
|
2381
|
+
...fileLevel === "off" ? {} : { dir, fileLevel },
|
|
2382
|
+
...base ? { base } : {},
|
|
2383
|
+
...ship ? { ship } : {}
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
|
|
1776
2387
|
// ../../packages/edge/src/policy.ts
|
|
1777
2388
|
var ALLOW = { verdict: "allow", policy: "none" };
|
|
1778
2389
|
function evaluatePolicies(event, rules) {
|
|
@@ -1795,15 +2406,15 @@ function denyToolRule(tool3) {
|
|
|
1795
2406
|
}
|
|
1796
2407
|
|
|
1797
2408
|
// ../../packages/edge/src/stage-runner.ts
|
|
1798
|
-
import { execFileSync as
|
|
2409
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1799
2410
|
import { createHash as createHash3 } from "crypto";
|
|
1800
|
-
import { mkdirSync as
|
|
2411
|
+
import { mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
1801
2412
|
import { tmpdir as tmpdir2 } from "os";
|
|
1802
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2413
|
+
import { isAbsolute as isAbsolute2, join as join8, relative as relative2, resolve, sep as sep2 } from "path";
|
|
1803
2414
|
import { attachedDocBasename as attachedDocBasename2 } from "@dahrk/contracts";
|
|
1804
2415
|
|
|
1805
2416
|
// ../../packages/edge/src/builtins.ts
|
|
1806
|
-
import { execFileSync as
|
|
2417
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1807
2418
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
1808
2419
|
"Write",
|
|
1809
2420
|
"Edit",
|
|
@@ -1845,7 +2456,7 @@ function isDangerousRm(cmd) {
|
|
|
1845
2456
|
}
|
|
1846
2457
|
function currentBranch(worktreePath) {
|
|
1847
2458
|
try {
|
|
1848
|
-
return
|
|
2459
|
+
return execFileSync3("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath }).toString().trim();
|
|
1849
2460
|
} catch {
|
|
1850
2461
|
return "";
|
|
1851
2462
|
}
|
|
@@ -1992,11 +2603,12 @@ async function startMcpGateway(opts) {
|
|
|
1992
2603
|
// ../../packages/edge/src/stage-runner.ts
|
|
1993
2604
|
var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1994
2605
|
var PREVIEW = 500;
|
|
2606
|
+
var RESULT = 16e3;
|
|
1995
2607
|
var putBytes = async (url, body, contentType) => {
|
|
1996
2608
|
await fetch(url, { method: "PUT", headers: { "content-type": contentType }, body: new Uint8Array(body) });
|
|
1997
2609
|
};
|
|
1998
2610
|
function previewOf(event) {
|
|
1999
|
-
const clip = (v) => (typeof v === "string" ? v : JSON.stringify(v) ?? "").slice(0,
|
|
2611
|
+
const clip = (v, max = PREVIEW) => (typeof v === "string" ? v : JSON.stringify(v) ?? "").slice(0, max);
|
|
2000
2612
|
switch (event.type) {
|
|
2001
2613
|
case "response":
|
|
2002
2614
|
return event.text !== void 0 ? { text: event.text } : {};
|
|
@@ -2005,9 +2617,16 @@ function previewOf(event) {
|
|
|
2005
2617
|
case "thought":
|
|
2006
2618
|
return event.text !== void 0 ? { text: clip(event.text) } : {};
|
|
2007
2619
|
case "action":
|
|
2008
|
-
return {
|
|
2620
|
+
return {
|
|
2621
|
+
tool: event.tool,
|
|
2622
|
+
toolUseId: event.toolUseId,
|
|
2623
|
+
...event.input !== void 0 ? { text: clip(event.input) } : {}
|
|
2624
|
+
};
|
|
2009
2625
|
case "observation":
|
|
2010
|
-
return
|
|
2626
|
+
return {
|
|
2627
|
+
toolUseId: event.toolUseId,
|
|
2628
|
+
...event.output !== void 0 ? { text: clip(event.output, RESULT) } : {}
|
|
2629
|
+
};
|
|
2011
2630
|
case "error":
|
|
2012
2631
|
return { text: clip(event.message) };
|
|
2013
2632
|
default:
|
|
@@ -2020,7 +2639,7 @@ var attemptOf = (jobId) => {
|
|
|
2020
2639
|
};
|
|
2021
2640
|
var digest = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
|
|
2022
2641
|
function writeScratchState(ref, job, attempt, status) {
|
|
2023
|
-
const statePath =
|
|
2642
|
+
const statePath = join8(ref.scratchPath, "state.json");
|
|
2024
2643
|
let state;
|
|
2025
2644
|
try {
|
|
2026
2645
|
state = JSON.parse(readFileSync4(statePath, "utf8"));
|
|
@@ -2028,24 +2647,24 @@ function writeScratchState(ref, job, attempt, status) {
|
|
|
2028
2647
|
state = { runId: job.runId, tenantId: job.tenantId, stages: {} };
|
|
2029
2648
|
}
|
|
2030
2649
|
state.stages[job.stageId] = { currentAttempt: attempt, status };
|
|
2031
|
-
|
|
2650
|
+
mkdirSync6(ref.scratchPath, { recursive: true });
|
|
2032
2651
|
writeFileSync5(statePath, JSON.stringify(state, null, 2));
|
|
2033
2652
|
}
|
|
2034
2653
|
function writeIssueContext(ref, issueContext) {
|
|
2035
2654
|
if (issueContext === void 0) return;
|
|
2036
2655
|
try {
|
|
2037
|
-
|
|
2038
|
-
writeFileSync5(
|
|
2656
|
+
mkdirSync6(ref.scratchPath, { recursive: true });
|
|
2657
|
+
writeFileSync5(join8(ref.scratchPath, "issue.md"), issueContext);
|
|
2039
2658
|
} catch {
|
|
2040
2659
|
}
|
|
2041
2660
|
}
|
|
2042
2661
|
function writeAttachedDocuments(ref, docs) {
|
|
2043
2662
|
if (!docs || docs.length === 0) return;
|
|
2044
2663
|
try {
|
|
2045
|
-
const dir =
|
|
2046
|
-
|
|
2664
|
+
const dir = join8(ref.scratchPath, "docs");
|
|
2665
|
+
mkdirSync6(dir, { recursive: true });
|
|
2047
2666
|
for (const doc of docs) {
|
|
2048
|
-
writeFileSync5(
|
|
2667
|
+
writeFileSync5(join8(dir, `${attachedDocBasename2(doc)}.md`), doc.content);
|
|
2049
2668
|
}
|
|
2050
2669
|
} catch {
|
|
2051
2670
|
}
|
|
@@ -2063,8 +2682,8 @@ ${lines.join("\n")}
|
|
|
2063
2682
|
function writeGuidance(ref, guidance) {
|
|
2064
2683
|
if (!guidance || guidance.length === 0) return;
|
|
2065
2684
|
try {
|
|
2066
|
-
|
|
2067
|
-
writeFileSync5(
|
|
2685
|
+
mkdirSync6(ref.scratchPath, { recursive: true });
|
|
2686
|
+
writeFileSync5(join8(ref.scratchPath, "guidance.md"), renderGuidanceMarkdown(guidance));
|
|
2068
2687
|
} catch {
|
|
2069
2688
|
}
|
|
2070
2689
|
}
|
|
@@ -2098,13 +2717,13 @@ function readEmittedArtifact(ref, relPath) {
|
|
|
2098
2717
|
}
|
|
2099
2718
|
function scanScratchOutput(ref, preferRel) {
|
|
2100
2719
|
try {
|
|
2101
|
-
const names =
|
|
2720
|
+
const names = readdirSync3(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR)).filter(
|
|
2102
2721
|
(n) => n.toLowerCase().endsWith(".md")
|
|
2103
2722
|
);
|
|
2104
2723
|
if (names.length === 0) return void 0;
|
|
2105
2724
|
const preferBase = preferRel?.split("/").pop();
|
|
2106
2725
|
const pick = preferBase && names.includes(preferBase) ? preferBase : names[0];
|
|
2107
|
-
const raw = readFileSync4(
|
|
2726
|
+
const raw = readFileSync4(join8(ref.worktreePath, SCRATCH_OUTPUT_DIR, pick), "utf8");
|
|
2108
2727
|
if (raw.trim().length === 0) return void 0;
|
|
2109
2728
|
return { path: `${SCRATCH_OUTPUT_DIR}/${pick}`, content: capContent(raw) };
|
|
2110
2729
|
} catch {
|
|
@@ -2114,7 +2733,7 @@ function scanScratchOutput(ref, preferRel) {
|
|
|
2114
2733
|
function scanChangedMarkdown(ref) {
|
|
2115
2734
|
const git = (args) => {
|
|
2116
2735
|
try {
|
|
2117
|
-
return
|
|
2736
|
+
return execFileSync4("git", ["-C", ref.worktreePath, ...args], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2118
2737
|
} catch {
|
|
2119
2738
|
return [];
|
|
2120
2739
|
}
|
|
@@ -2124,7 +2743,7 @@ function scanChangedMarkdown(ref) {
|
|
|
2124
2743
|
);
|
|
2125
2744
|
for (const rel of rels) {
|
|
2126
2745
|
try {
|
|
2127
|
-
const raw = readFileSync4(
|
|
2746
|
+
const raw = readFileSync4(join8(ref.worktreePath, rel), "utf8");
|
|
2128
2747
|
if (raw.trim().length > 0) return { path: rel, content: capContent(raw) };
|
|
2129
2748
|
} catch {
|
|
2130
2749
|
}
|
|
@@ -2147,22 +2766,34 @@ function resolveStageArtifact(ref, emitArtifact, handedBack) {
|
|
|
2147
2766
|
}
|
|
2148
2767
|
function createStageRunner(deps) {
|
|
2149
2768
|
const worktrees = /* @__PURE__ */ new Map();
|
|
2769
|
+
const log = deps.logger ?? createNodeLogger({ level: "silent" });
|
|
2150
2770
|
const active = /* @__PURE__ */ new Map();
|
|
2151
2771
|
const turnQueues = /* @__PURE__ */ new Map();
|
|
2152
2772
|
const runToolCalls = /* @__PURE__ */ new Map();
|
|
2153
2773
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
2154
2774
|
const inFlight = /* @__PURE__ */ new Map();
|
|
2775
|
+
const isBusy = (runId) => (inFlight.get(runId) ?? 0) > 0;
|
|
2776
|
+
const reaperDryRun = process.env.DAHRK_REAPER_DRY_RUN === "1";
|
|
2777
|
+
const reaper = createWorktreeReaper({
|
|
2778
|
+
worktreesDir: deps.gitService.worktreesDir,
|
|
2779
|
+
mirrorsDir: resolveMirrorsDir(),
|
|
2780
|
+
isBusy,
|
|
2781
|
+
logger: { info: (m) => console.log(`REAPER ${m}`), warn: (m) => console.warn(`REAPER ${m}`) }
|
|
2782
|
+
});
|
|
2155
2783
|
const scratchOnly = /* @__PURE__ */ new Set();
|
|
2156
2784
|
const teardownRun = async (runId) => {
|
|
2157
2785
|
const ref = worktrees.get(runId);
|
|
2158
2786
|
if (ref) {
|
|
2159
2787
|
if (scratchOnly.has(runId)) {
|
|
2160
2788
|
try {
|
|
2161
|
-
|
|
2162
|
-
} catch {
|
|
2789
|
+
rmSync5(ref.worktreePath, { recursive: true, force: true });
|
|
2790
|
+
} catch (e) {
|
|
2791
|
+
log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove scratch dir");
|
|
2163
2792
|
}
|
|
2164
2793
|
} else {
|
|
2165
|
-
await deps.gitService.teardownWorktree(ref).catch(() =>
|
|
2794
|
+
await deps.gitService.teardownWorktree(ref).catch((e) => {
|
|
2795
|
+
log.warn({ err: e, runId, path: ref.worktreePath }, "teardown: could not remove worktree");
|
|
2796
|
+
});
|
|
2166
2797
|
}
|
|
2167
2798
|
}
|
|
2168
2799
|
worktrees.delete(runId);
|
|
@@ -2172,6 +2803,11 @@ function createStageRunner(deps) {
|
|
|
2172
2803
|
};
|
|
2173
2804
|
const applyRetention = async (keepRunId) => {
|
|
2174
2805
|
const policy = deps.retention;
|
|
2806
|
+
await reaper.reap({
|
|
2807
|
+
...policy?.maxRuns !== void 0 ? { maxRuns: policy.maxRuns } : {},
|
|
2808
|
+
...policy?.maxAgeMs !== void 0 ? { maxIdleMs: policy.maxAgeMs } : {},
|
|
2809
|
+
...reaperDryRun ? { dryRun: true } : {}
|
|
2810
|
+
}).catch(() => void 0);
|
|
2175
2811
|
if (!policy) return;
|
|
2176
2812
|
const prunable = (id) => id !== keepRunId && (inFlight.get(id) ?? 0) === 0;
|
|
2177
2813
|
const now = Date.now();
|
|
@@ -2191,6 +2827,10 @@ function createStageRunner(deps) {
|
|
|
2191
2827
|
}
|
|
2192
2828
|
};
|
|
2193
2829
|
return {
|
|
2830
|
+
isBusy,
|
|
2831
|
+
async reapWorktrees() {
|
|
2832
|
+
return reaper.reap(reaperDryRun ? { dryRun: true } : {});
|
|
2833
|
+
},
|
|
2194
2834
|
async runJob(job) {
|
|
2195
2835
|
const { stageId, jobId, runId, agentConfig } = job;
|
|
2196
2836
|
const attempt = attemptOf(jobId);
|
|
@@ -2206,299 +2846,307 @@ function createStageRunner(deps) {
|
|
|
2206
2846
|
return { jobId, status: "fail", summary: `edge does not serve repo "${job.workspaceRef.repoId}"` };
|
|
2207
2847
|
}
|
|
2208
2848
|
inFlight.set(runId, (inFlight.get(runId) ?? 0) + 1);
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
if (
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2849
|
+
try {
|
|
2850
|
+
lastUsed.set(runId, Date.now());
|
|
2851
|
+
let ref = worktrees.get(runId);
|
|
2852
|
+
if (!ref) {
|
|
2853
|
+
if (job.workspaceRef) {
|
|
2854
|
+
ref = await deps.gitService.createWorktree({
|
|
2855
|
+
repoId: job.workspaceRef.repoId,
|
|
2856
|
+
gitUrl: job.workspaceRef.gitUrl,
|
|
2857
|
+
baseBranch: job.workspaceRef.baseBranch,
|
|
2858
|
+
runId,
|
|
2859
|
+
repo: job.workspaceRef.repo,
|
|
2860
|
+
// Stable per-issue branch (continuation); absent = legacy dahrk/<runId>.
|
|
2861
|
+
...job.workspaceRef.branch ? { branch: job.workspaceRef.branch } : {},
|
|
2862
|
+
// Re-enter from work a prior backup push preserved, rather than starting over from the base
|
|
2863
|
+
// (DHK-264). Carried on the contract but never plumbed through to the git service until now.
|
|
2864
|
+
...job.workspaceRef.seedRef ? { seedRef: job.workspaceRef.seedRef } : {},
|
|
2865
|
+
// Brokered git credential for this job; absent on ambient nodes (host creds used).
|
|
2866
|
+
...job.workspaceRef.credentialToken ? { credentialToken: job.workspaceRef.credentialToken } : {}
|
|
2867
|
+
});
|
|
2868
|
+
} else {
|
|
2869
|
+
const base = deps.scratchRoot ?? join8(tmpdir2(), "dahrk", "scratch");
|
|
2870
|
+
const worktreePath = join8(base, runId);
|
|
2871
|
+
const scratchPath = join8(worktreePath, ".skakel", "scratch");
|
|
2872
|
+
mkdirSync6(scratchPath, { recursive: true });
|
|
2873
|
+
ref = { repoId: "", gitUrl: "", repo: "", baseBranch: "", worktreePath, scratchPath };
|
|
2874
|
+
scratchOnly.add(runId);
|
|
2875
|
+
}
|
|
2876
|
+
worktrees.set(runId, ref);
|
|
2231
2877
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2878
|
+
writeScratchState(ref, job, attempt, "in-flight");
|
|
2879
|
+
writeIssueContext(ref, job.issueContext);
|
|
2880
|
+
writeGuidance(ref, job.guidance);
|
|
2881
|
+
writeAttachedDocuments(ref, job.attachedDocuments);
|
|
2882
|
+
if (job.agentConfig.emitArtifact) {
|
|
2883
|
+
const artifactDir = resolveWorktreeRelativePath(ref, job.agentConfig.emitArtifact);
|
|
2884
|
+
const slash = job.agentConfig.emitArtifact.lastIndexOf("/");
|
|
2885
|
+
if (artifactDir && slash > 0) {
|
|
2886
|
+
try {
|
|
2887
|
+
mkdirSync6(join8(ref.worktreePath, job.agentConfig.emitArtifact.slice(0, slash)), { recursive: true });
|
|
2888
|
+
} catch {
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
let gateway;
|
|
2893
|
+
const meta = {
|
|
2894
|
+
tenantId: job.tenantId,
|
|
2895
|
+
runId,
|
|
2896
|
+
stageId,
|
|
2897
|
+
jobId,
|
|
2898
|
+
attempt,
|
|
2899
|
+
runtime: agentConfig.runtime,
|
|
2900
|
+
model: agentConfig.model,
|
|
2901
|
+
sessionId: job.sessionId,
|
|
2902
|
+
configDigest: digest(agentConfig),
|
|
2903
|
+
startedAt: nowIso2()
|
|
2904
|
+
};
|
|
2905
|
+
const writer = createTraceWriter(ref.scratchPath, meta);
|
|
2906
|
+
const streamEvent = (e) => deps.trace?.event({ runId, stageId, attempt, tenantId: job.tenantId, event: e });
|
|
2907
|
+
streamEvent(
|
|
2908
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "attempt-start" })
|
|
2909
|
+
);
|
|
2910
|
+
const shipFinalTrace = async (finalMeta) => {
|
|
2911
|
+
const sink = deps.trace;
|
|
2912
|
+
if (!sink) return;
|
|
2913
|
+
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
2242
2914
|
try {
|
|
2243
|
-
|
|
2915
|
+
for (const name of readdirSync3(join8(writer.dir, "blobs"))) {
|
|
2916
|
+
const bytes = readFileSync4(join8(writer.dir, "blobs", name));
|
|
2917
|
+
const { url } = await sink.requestBlobUrl({
|
|
2918
|
+
...base,
|
|
2919
|
+
sha256: name,
|
|
2920
|
+
size: bytes.length,
|
|
2921
|
+
contentType: "application/octet-stream",
|
|
2922
|
+
slot: "blob"
|
|
2923
|
+
});
|
|
2924
|
+
if (url) await putBytes(url, bytes, "application/octet-stream");
|
|
2925
|
+
}
|
|
2244
2926
|
} catch {
|
|
2245
2927
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
runId,
|
|
2252
|
-
stageId,
|
|
2253
|
-
jobId,
|
|
2254
|
-
attempt,
|
|
2255
|
-
runtime: agentConfig.runtime,
|
|
2256
|
-
model: agentConfig.model,
|
|
2257
|
-
sessionId: job.sessionId,
|
|
2258
|
-
configDigest: digest(agentConfig),
|
|
2259
|
-
startedAt: nowIso2()
|
|
2260
|
-
};
|
|
2261
|
-
const writer = createTraceWriter(ref.scratchPath, meta);
|
|
2262
|
-
const streamEvent = (e) => deps.trace?.event({ runId, stageId, attempt, tenantId: job.tenantId, event: e });
|
|
2263
|
-
streamEvent(
|
|
2264
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "attempt-start" })
|
|
2265
|
-
);
|
|
2266
|
-
const shipFinalTrace = async (finalMeta) => {
|
|
2267
|
-
const sink = deps.trace;
|
|
2268
|
-
if (!sink) return;
|
|
2269
|
-
const base = { tenantId: job.tenantId, runId, stageId, attempt };
|
|
2270
|
-
try {
|
|
2271
|
-
for (const name of readdirSync2(join6(writer.dir, "blobs"))) {
|
|
2272
|
-
const bytes = readFileSync4(join6(writer.dir, "blobs", name));
|
|
2273
|
-
const { url } = await sink.requestBlobUrl({
|
|
2928
|
+
let archiveKey;
|
|
2929
|
+
try {
|
|
2930
|
+
const bytes = readFileSync4(join8(writer.dir, "trace.jsonl"));
|
|
2931
|
+
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
2932
|
+
const { key, url } = await sink.requestBlobUrl({
|
|
2274
2933
|
...base,
|
|
2275
|
-
sha256:
|
|
2934
|
+
sha256: sha,
|
|
2276
2935
|
size: bytes.length,
|
|
2277
|
-
contentType: "application/
|
|
2278
|
-
slot: "
|
|
2936
|
+
contentType: "application/x-ndjson",
|
|
2937
|
+
slot: "archive"
|
|
2279
2938
|
});
|
|
2280
|
-
if (url) await putBytes(url, bytes, "application/
|
|
2939
|
+
if (url) await putBytes(url, bytes, "application/x-ndjson");
|
|
2940
|
+
archiveKey = key;
|
|
2941
|
+
} catch {
|
|
2281
2942
|
}
|
|
2282
|
-
|
|
2283
|
-
}
|
|
2284
|
-
let archiveKey;
|
|
2285
|
-
try {
|
|
2286
|
-
const bytes = readFileSync4(join6(writer.dir, "trace.jsonl"));
|
|
2287
|
-
const sha = createHash3("sha256").update(bytes).digest("hex");
|
|
2288
|
-
const { key, url } = await sink.requestBlobUrl({
|
|
2289
|
-
...base,
|
|
2290
|
-
sha256: sha,
|
|
2291
|
-
size: bytes.length,
|
|
2292
|
-
contentType: "application/x-ndjson",
|
|
2293
|
-
slot: "archive"
|
|
2294
|
-
});
|
|
2295
|
-
if (url) await putBytes(url, bytes, "application/x-ndjson");
|
|
2296
|
-
archiveKey = key;
|
|
2297
|
-
} catch {
|
|
2298
|
-
}
|
|
2299
|
-
sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
|
|
2300
|
-
};
|
|
2301
|
-
const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc) => {
|
|
2302
|
-
active.delete(jobId);
|
|
2303
|
-
turnQueues.delete(jobId);
|
|
2304
|
-
await gateway?.stop().catch(() => void 0);
|
|
2305
|
-
gateway = void 0;
|
|
2306
|
-
streamEvent(
|
|
2307
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "stage-exit", status: status2 })
|
|
2308
|
-
);
|
|
2309
|
-
const endedAt = nowIso2();
|
|
2310
|
-
writer.finalise({ status: status2, endedAt, ...sessionId ? { sessionId } : {} });
|
|
2311
|
-
writeScratchState(ref, job, attempt, status2);
|
|
2312
|
-
const finalMeta = {
|
|
2313
|
-
...meta,
|
|
2314
|
-
status: status2,
|
|
2315
|
-
endedAt,
|
|
2316
|
-
...sessionId ? { sessionId } : {},
|
|
2317
|
-
...costUsd !== void 0 ? { costUsd } : {}
|
|
2943
|
+
sink.finalised({ ...base, meta: finalMeta, eventCount: writer.count(), ...archiveKey ? { archiveKey } : {} });
|
|
2318
2944
|
};
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig.emitArtifact, handedBackDoc) : void 0;
|
|
2325
|
-
if (status2 === "ok" && wantsArtifact) {
|
|
2326
|
-
const detail = resolved ? `source=${resolved.source} path=${resolved.artifact.path} bytes=${resolved.artifact.content.length}` : "no document resolved (declared path, tool handoff, and scratch/changed-file scans all empty)";
|
|
2945
|
+
const finish = async (status2, summary2, sessionId, costUsd, handedBackDoc) => {
|
|
2946
|
+
active.delete(jobId);
|
|
2947
|
+
turnQueues.delete(jobId);
|
|
2948
|
+
await gateway?.stop().catch((e) => log.warn({ err: e, jobId }, "mcp gateway: stop failed"));
|
|
2949
|
+
gateway = void 0;
|
|
2327
2950
|
streamEvent(
|
|
2328
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "
|
|
2951
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "stage-exit", status: status2 })
|
|
2329
2952
|
);
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
const overlay = await overlayComponents({
|
|
2343
|
-
worktreePath: ref.worktreePath,
|
|
2344
|
-
runtime: agentConfig.runtime,
|
|
2345
|
-
components: job.provision,
|
|
2346
|
-
cache: deps.packCache
|
|
2347
|
-
});
|
|
2348
|
-
const detail = `provision: ${overlay.written.length} written, ${overlay.skippedRepoLocal.length} repo-local, ${overlay.warnings.length} warning(s)`;
|
|
2349
|
-
streamEvent(
|
|
2350
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "provision", detail })
|
|
2953
|
+
const endedAt = nowIso2();
|
|
2954
|
+
writer.finalise({ status: status2, endedAt, ...sessionId ? { sessionId } : {} });
|
|
2955
|
+
writeScratchState(ref, job, attempt, status2);
|
|
2956
|
+
const finalMeta = {
|
|
2957
|
+
...meta,
|
|
2958
|
+
status: status2,
|
|
2959
|
+
endedAt,
|
|
2960
|
+
...sessionId ? { sessionId } : {},
|
|
2961
|
+
...costUsd !== void 0 ? { costUsd } : {}
|
|
2962
|
+
};
|
|
2963
|
+
await shipFinalTrace(finalMeta).catch(
|
|
2964
|
+
(e) => log.error({ err: e, runId, stageId: job.stageId, jobId, attempt }, "trace: failed to ship the final trace to the hub")
|
|
2351
2965
|
);
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2966
|
+
lastUsed.set(runId, Date.now());
|
|
2967
|
+
await applyRetention(runId).catch((e) => log.warn({ err: e, runId }, "retention: pass failed"));
|
|
2968
|
+
const wantsArtifact = agentConfig.emitArtifact !== void 0 || handedBackDoc !== void 0;
|
|
2969
|
+
const resolved = status2 === "ok" && wantsArtifact ? resolveStageArtifact(ref, agentConfig.emitArtifact, handedBackDoc) : void 0;
|
|
2970
|
+
if (status2 === "ok" && wantsArtifact) {
|
|
2971
|
+
const detail = resolved ? `source=${resolved.source} path=${resolved.artifact.path} bytes=${resolved.artifact.content.length}` : "no document resolved (declared path, tool handoff, and scratch/changed-file scans all empty)";
|
|
2972
|
+
streamEvent(
|
|
2973
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "artifact", detail })
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
return {
|
|
2977
|
+
jobId,
|
|
2978
|
+
status: status2,
|
|
2979
|
+
summary: summary2,
|
|
2980
|
+
...sessionId ? { sessionId } : {},
|
|
2981
|
+
...costUsd !== void 0 ? { costUsd } : {},
|
|
2982
|
+
...resolved ? { artifact: resolved.artifact } : {}
|
|
2983
|
+
};
|
|
2984
|
+
};
|
|
2985
|
+
if (deps.packCache && job.provision && job.provision.length > 0) {
|
|
2986
|
+
try {
|
|
2987
|
+
const overlay = await overlayComponents({
|
|
2988
|
+
worktreePath: ref.worktreePath,
|
|
2989
|
+
runtime: agentConfig.runtime,
|
|
2990
|
+
components: job.provision,
|
|
2991
|
+
cache: deps.packCache
|
|
2992
|
+
});
|
|
2993
|
+
const detail = `provision: ${overlay.written.length} written, ${overlay.skippedRepoLocal.length} repo-local, ${overlay.warnings.length} warning(s)`;
|
|
2994
|
+
streamEvent(
|
|
2995
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "provision", detail })
|
|
2996
|
+
);
|
|
2997
|
+
const noteText = overlay.warnings.length > 0 ? `${detail}; ${overlay.warnings.join("; ")}` : detail;
|
|
2998
|
+
deps.sendProgress({ jobId, kind: "observation", ts: nowIso2(), text: noteText });
|
|
2999
|
+
} catch (e) {
|
|
3000
|
+
const msg = `component provisioning failed: ${e.message}`;
|
|
3001
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime: agentConfig.runtime, kind: "provision-failed", message: msg });
|
|
3002
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: msg });
|
|
3003
|
+
return finish("fail", `${stageId}: ${msg}`, job.sessionId);
|
|
3004
|
+
}
|
|
2385
3005
|
}
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
const reason = policyReason(verdict);
|
|
2391
|
-
if (toolUseId) {
|
|
2392
|
-
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
3006
|
+
let counter = runToolCalls.get(runId);
|
|
3007
|
+
if (!counter) {
|
|
3008
|
+
counter = { count: 0 };
|
|
3009
|
+
runToolCalls.set(runId, counter);
|
|
2393
3010
|
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
3011
|
+
const jobRules = buildRules(job.policies ?? [], {
|
|
3012
|
+
worktreePath: ref.worktreePath,
|
|
3013
|
+
repoName: job.workspaceRef?.repo ?? "",
|
|
3014
|
+
runToolCalls: counter
|
|
3015
|
+
});
|
|
3016
|
+
const rules = [...jobRules, ...deps.rules];
|
|
3017
|
+
const entry = evaluatePolicies({ kind: "stage-entry", stageId }, rules);
|
|
3018
|
+
if (entry.verdict === "deny") {
|
|
3019
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime: agentConfig.runtime, event: "policy-deny", detail: entry.reason });
|
|
3020
|
+
return finish("fail", `${stageId}: denied at stage entry (${entry.policy})`, job.sessionId);
|
|
2403
3021
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
3022
|
+
let denied = false;
|
|
3023
|
+
const authorisedActions = [];
|
|
3024
|
+
const runtime = agentConfig.runtime;
|
|
3025
|
+
const actionKey = (tool3, input) => {
|
|
3026
|
+
try {
|
|
3027
|
+
return `${tool3}\0${JSON.stringify(input)}`;
|
|
3028
|
+
} catch {
|
|
3029
|
+
return `${tool3}\0`;
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
const policyReason = (verdict) => verdict.reason ?? `tool action denied by ${verdict.policy}`;
|
|
3033
|
+
const recordDeny = (verdict, toolUseId) => {
|
|
3034
|
+
denied = true;
|
|
3035
|
+
const reason = policyReason(verdict);
|
|
3036
|
+
if (toolUseId) {
|
|
3037
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "observation", runtime, toolUseId, isError: true, output: { error: reason } }));
|
|
3038
|
+
}
|
|
3039
|
+
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
|
|
3040
|
+
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
3041
|
+
};
|
|
3042
|
+
const authorizeToolUse = (tool3, input) => {
|
|
3043
|
+
const verdict = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
3044
|
+
if (verdict.verdict === "deny") {
|
|
3045
|
+
recordDeny(verdict);
|
|
2412
3046
|
} else {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3047
|
+
authorisedActions.push(actionKey(tool3, input));
|
|
3048
|
+
}
|
|
3049
|
+
return verdict;
|
|
3050
|
+
};
|
|
3051
|
+
const onTrace = (event) => {
|
|
3052
|
+
if (event.type === "action") {
|
|
3053
|
+
const key = actionKey(event.tool, event.input);
|
|
3054
|
+
const authorised = authorisedActions.indexOf(key);
|
|
3055
|
+
if (authorised >= 0) {
|
|
3056
|
+
authorisedActions.splice(authorised, 1);
|
|
3057
|
+
} else {
|
|
3058
|
+
const verdict = evaluatePolicies(
|
|
3059
|
+
{ kind: "action", stageId, tool: event.tool, input: event.input },
|
|
3060
|
+
rules
|
|
3061
|
+
);
|
|
3062
|
+
if (verdict.verdict === "deny") {
|
|
3063
|
+
streamEvent(writer.append(event));
|
|
3064
|
+
recordDeny(verdict, event.toolUseId);
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
2421
3067
|
}
|
|
2422
3068
|
}
|
|
3069
|
+
streamEvent(writer.append(event));
|
|
3070
|
+
if (event.type !== "state") deps.sendProgress({ jobId, kind: event.type, ts: event.ts, ...previewOf(event) });
|
|
3071
|
+
};
|
|
3072
|
+
const mcpServers = agentConfig.mcpServers;
|
|
3073
|
+
if (mcpServers && mcpServers.length > 0 && runtime === "claude-code") {
|
|
3074
|
+
gateway = await startMcpGateway({ servers: mcpServers, creds: job.brokeredCreds ?? {} });
|
|
2423
3075
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
turnQueues.set(jobId, mailbox);
|
|
2474
|
-
result = await runner.runInteractive(ctx, mailbox, onTrace);
|
|
2475
|
-
} else {
|
|
2476
|
-
result = await runner.runBatch(ctx, onTrace);
|
|
3076
|
+
const runner = deps.makeRunner(runtime);
|
|
3077
|
+
active.set(jobId, runner);
|
|
3078
|
+
const ctx = {
|
|
3079
|
+
config: agentConfig,
|
|
3080
|
+
workspace: ref,
|
|
3081
|
+
sessionId: job.sessionId,
|
|
3082
|
+
...job.issueContext !== void 0 ? { issueContext: job.issueContext } : {},
|
|
3083
|
+
...job.guidance !== void 0 ? { guidance: job.guidance } : {},
|
|
3084
|
+
...job.gateFeedback !== void 0 ? { gateFeedback: job.gateFeedback } : {},
|
|
3085
|
+
...job.attachedDocuments !== void 0 ? { attachedDocuments: job.attachedDocuments } : {},
|
|
3086
|
+
...gateway ? { mcpProxyBaseUrl: gateway.baseUrl } : {},
|
|
3087
|
+
// brokered inference env for a managed node (no operator login). The runtime adapter
|
|
3088
|
+
// (Pi) / container executor apply it as the inference process env, so the raw key is
|
|
3089
|
+
// never surfaced to the agent's own tool calls. Absent on ambient nodes; inert for the Claude/
|
|
3090
|
+
// Codex adapters, which use ambient inference.
|
|
3091
|
+
...job.runtimeEnv ? { runtimeEnv: job.runtimeEnv } : {},
|
|
3092
|
+
// The adapter persists each runtime-native record under the attempt's raw/ sidecar
|
|
3093
|
+
// and stamps the rawRef onto the emitted event.
|
|
3094
|
+
writeRaw: writer.writeRaw,
|
|
3095
|
+
authorizeToolUse,
|
|
3096
|
+
// Wire the interactive AskUserQuestion elicitation seam (DHK-344): the adapter calls this when
|
|
3097
|
+
// the agent asks a structured question, and the edge relays it to the hub as an `elicit` frame.
|
|
3098
|
+
...deps.sendElicit ? {
|
|
3099
|
+
emitElicit: (question) => deps.sendElicit({
|
|
3100
|
+
jobId,
|
|
3101
|
+
prompt: question.prompt,
|
|
3102
|
+
options: question.options,
|
|
3103
|
+
...question.multiSelect ? { multiSelect: true } : {}
|
|
3104
|
+
})
|
|
3105
|
+
} : {}
|
|
3106
|
+
};
|
|
3107
|
+
const interactive = agentConfig.interaction === "interactive";
|
|
3108
|
+
let result;
|
|
3109
|
+
let timedOut = false;
|
|
3110
|
+
const killMs = Math.max(0, Math.floor((job.timeout ?? 0) * 1e3));
|
|
3111
|
+
const killTimer = killMs > 0 ? setTimeout(() => {
|
|
3112
|
+
timedOut = true;
|
|
3113
|
+
void runner.cancel();
|
|
3114
|
+
}, killMs) : void 0;
|
|
3115
|
+
try {
|
|
3116
|
+
if (interactive) {
|
|
3117
|
+
const mailbox = new ManagedMailbox();
|
|
3118
|
+
turnQueues.set(jobId, mailbox);
|
|
3119
|
+
result = await runner.runInteractive(ctx, mailbox, onTrace);
|
|
3120
|
+
} else {
|
|
3121
|
+
result = await runner.runBatch(ctx, onTrace);
|
|
3122
|
+
}
|
|
3123
|
+
} finally {
|
|
3124
|
+
if (killTimer) clearTimeout(killTimer);
|
|
2477
3125
|
}
|
|
2478
|
-
|
|
2479
|
-
if (
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
|
|
2489
|
-
break;
|
|
3126
|
+
let status = timedOut ? "timeout" : result.status;
|
|
3127
|
+
if (status === "ok" && job.hooks && job.hooks.length > 0) {
|
|
3128
|
+
for (const cmd of job.hooks) {
|
|
3129
|
+
try {
|
|
3130
|
+
execFileSync4("sh", ["-c", cmd], { cwd: ref.worktreePath, stdio: ["pipe", "pipe", "pipe"] });
|
|
3131
|
+
} catch (e) {
|
|
3132
|
+
status = "fail";
|
|
3133
|
+
writer.append({ seq: 0, ts: nowIso2(), type: "error", runtime, kind: "hook-failed", message: `hook "${cmd}" failed: ${e.message}` });
|
|
3134
|
+
break;
|
|
3135
|
+
}
|
|
2490
3136
|
}
|
|
2491
3137
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
3138
|
+
let summary = result.summary ?? `${stageId}: ${status}`;
|
|
3139
|
+
if (!interactive && status === "ok") {
|
|
3140
|
+
try {
|
|
3141
|
+
summary = await runner.summarise(ctx);
|
|
3142
|
+
} catch {
|
|
3143
|
+
}
|
|
2498
3144
|
}
|
|
3145
|
+
if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
|
|
3146
|
+
return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact);
|
|
3147
|
+
} finally {
|
|
3148
|
+
inFlight.set(runId, Math.max(0, (inFlight.get(runId) ?? 1) - 1));
|
|
2499
3149
|
}
|
|
2500
|
-
if (denied) summary += "\n\n(note: one or more tool actions were blocked by a deny-only policy guard.)";
|
|
2501
|
-
return finish(status, summary, result.sessionId ?? job.sessionId, result.costUsd, result.artifact);
|
|
2502
3150
|
},
|
|
2503
3151
|
async runPush(job) {
|
|
2504
3152
|
const { runId, jobId } = job;
|
|
@@ -2641,34 +3289,71 @@ function createStageRunner(deps) {
|
|
|
2641
3289
|
|
|
2642
3290
|
// ../../packages/edge/src/ws-client.ts
|
|
2643
3291
|
var ENROLMENT_REJECTED_EXIT_CODE = 78;
|
|
2644
|
-
|
|
2645
|
-
|
|
3292
|
+
function classifyError(e) {
|
|
3293
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3294
|
+
if (/\bgit\b|worktree|clone|fetch|branch|checkout|remote/i.test(msg)) return "git";
|
|
3295
|
+
if (/policy|denied|not permitted|forbidden/i.test(msg)) return "policy";
|
|
3296
|
+
if (/hub|websocket|socket|awakeable/i.test(msg)) return "hub";
|
|
3297
|
+
if (/runtime|claude|codex|\bpi\b|model|sdk|api key/i.test(msg)) return "runtime";
|
|
3298
|
+
return "internal";
|
|
3299
|
+
}
|
|
2646
3300
|
async function startEdgeNode(opts) {
|
|
3301
|
+
const log = opts.logger ?? createNodeLogger({ level: levelFromEnv(process.env) });
|
|
2647
3302
|
const rules = opts.denyTool ? [denyToolRule(opts.denyTool)] : [];
|
|
3303
|
+
const counters = new HealthCounters();
|
|
3304
|
+
const shipper = opts.shipper;
|
|
3305
|
+
const telemetryCeiling = ceilingFromEnv(process.env);
|
|
3306
|
+
const gitLog = log.child({ component: "git" });
|
|
3307
|
+
const gitLogger = {
|
|
3308
|
+
info: (msg) => gitLog.debug(msg),
|
|
3309
|
+
warn: (msg) => gitLog.warn(msg)
|
|
3310
|
+
};
|
|
3311
|
+
let stageRunnerRef;
|
|
2648
3312
|
const gitService = createGitService({
|
|
2649
3313
|
worktreesDir: opts.worktreesDir,
|
|
2650
|
-
mirrorsDir: opts.mirrorsDir
|
|
3314
|
+
mirrorsDir: opts.mirrorsDir,
|
|
3315
|
+
isBusy: (runId) => stageRunnerRef?.isBusy(runId) ?? false,
|
|
3316
|
+
logger: gitLogger
|
|
2651
3317
|
});
|
|
2652
3318
|
let ws;
|
|
2653
3319
|
let heartbeat;
|
|
2654
3320
|
let reconnectTimer;
|
|
2655
3321
|
let lastPongAt = 0;
|
|
2656
3322
|
let shuttingDown = false;
|
|
3323
|
+
let connectCount = 0;
|
|
2657
3324
|
let onFatal;
|
|
2658
3325
|
const send = (msg) => {
|
|
2659
3326
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
|
|
2660
3327
|
};
|
|
3328
|
+
shipper?.attach((records) => {
|
|
3329
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
3330
|
+
ws.send(encode({ type: "node-log", records }));
|
|
3331
|
+
return true;
|
|
3332
|
+
});
|
|
3333
|
+
shipper?.start();
|
|
2661
3334
|
const MISSED_PONGS_BEFORE_DEAD = 3;
|
|
2662
3335
|
const startHeartbeat = (sock, intervalMs) => {
|
|
2663
3336
|
if (heartbeat) clearInterval(heartbeat);
|
|
2664
3337
|
lastPongAt = Date.now();
|
|
2665
3338
|
heartbeat = setInterval(() => {
|
|
2666
|
-
|
|
2667
|
-
|
|
3339
|
+
const sincePongMs = Date.now() - lastPongAt;
|
|
3340
|
+
if (sincePongMs > MISSED_PONGS_BEFORE_DEAD * intervalMs) {
|
|
3341
|
+
log.warn({ sincePongMs, intervalMs }, `EDGE_STALE:${sincePongMs}ms without a pong, terminating socket`);
|
|
2668
3342
|
sock.terminate();
|
|
2669
3343
|
return;
|
|
2670
3344
|
}
|
|
2671
|
-
|
|
3345
|
+
const health = shipper?.current().health ?? telemetryCeiling.health;
|
|
3346
|
+
send(
|
|
3347
|
+
health ? {
|
|
3348
|
+
type: "heartbeat",
|
|
3349
|
+
health: collectHealth({
|
|
3350
|
+
counters,
|
|
3351
|
+
clientVersion: opts.clientVersion ?? "0.0.0",
|
|
3352
|
+
runtimes: opts.runtimes,
|
|
3353
|
+
worktreesDir: gitService.worktreesDir
|
|
3354
|
+
})
|
|
3355
|
+
} : { type: "heartbeat" }
|
|
3356
|
+
);
|
|
2672
3357
|
if (sock.readyState === WebSocket.OPEN) sock.ping();
|
|
2673
3358
|
}, intervalMs);
|
|
2674
3359
|
};
|
|
@@ -2699,6 +3384,7 @@ async function startEdgeNode(opts) {
|
|
|
2699
3384
|
const stageDeps = {
|
|
2700
3385
|
gitService,
|
|
2701
3386
|
makeRunner,
|
|
3387
|
+
logger: log,
|
|
2702
3388
|
...opts.servesRepoIds ? { servesRepoIds: opts.servesRepoIds } : {},
|
|
2703
3389
|
...opts.tenantId ? { tenantId: opts.tenantId } : {},
|
|
2704
3390
|
rules,
|
|
@@ -2710,6 +3396,17 @@ async function startEdgeNode(opts) {
|
|
|
2710
3396
|
...opts.retention ? { retention: opts.retention } : {}
|
|
2711
3397
|
};
|
|
2712
3398
|
const stageRunner = createStageRunner(stageDeps);
|
|
3399
|
+
stageRunnerRef = stageRunner;
|
|
3400
|
+
void stageRunner.reapWorktrees().then((r) => {
|
|
3401
|
+
counters.worktreeCount = Math.max(r.scanned - r.reaped.length, 0);
|
|
3402
|
+
if (r.reaped.length) {
|
|
3403
|
+
log.info(
|
|
3404
|
+
{ reaped: r.reaped, scanned: r.scanned, skipped: r.skipped },
|
|
3405
|
+
`EDGE_REAPED:${r.reaped.length} worktrees (scanned ${r.scanned}, skipped ${r.skipped})`
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
for (const e of r.errors) log.warn({ reapError: e }, `EDGE_REAP_ERROR:${e}`);
|
|
3409
|
+
}).catch((e) => log.warn({ err: e }, `EDGE_REAP_ERROR:${e.message}`));
|
|
2713
3410
|
const nodeId = opts.nodeId ?? randomUUID();
|
|
2714
3411
|
const running = /* @__PURE__ */ new Set();
|
|
2715
3412
|
const onMessage = async (raw) => {
|
|
@@ -2720,7 +3417,17 @@ async function startEdgeNode(opts) {
|
|
|
2720
3417
|
if (opts.heartbeatMs === void 0 && msg.heartbeatMs > 0 && ws) {
|
|
2721
3418
|
startHeartbeat(ws, msg.heartbeatMs);
|
|
2722
3419
|
}
|
|
2723
|
-
|
|
3420
|
+
if (msg.telemetry && shipper) shipper.setPolicy(msg.telemetry);
|
|
3421
|
+
log.info(
|
|
3422
|
+
{
|
|
3423
|
+
name: msg.name,
|
|
3424
|
+
tenantId: msg.tenantId,
|
|
3425
|
+
credentialMode: msg.credentialMode,
|
|
3426
|
+
heartbeatMs: msg.heartbeatMs,
|
|
3427
|
+
...shipper ? { telemetry: shipper.current() } : {}
|
|
3428
|
+
},
|
|
3429
|
+
`EDGE_WELCOMED:${msg.name} tenant=${msg.tenantId} credentialMode=${msg.credentialMode}`
|
|
3430
|
+
);
|
|
2724
3431
|
try {
|
|
2725
3432
|
opts.onEnrolled?.({
|
|
2726
3433
|
name: msg.name,
|
|
@@ -2728,7 +3435,7 @@ async function startEdgeNode(opts) {
|
|
|
2728
3435
|
credentialMode: msg.credentialMode
|
|
2729
3436
|
});
|
|
2730
3437
|
} catch (e) {
|
|
2731
|
-
log(`EDGE_ENROL_PERSIST_FAILED ${e.message}`);
|
|
3438
|
+
log.warn({ err: e }, `EDGE_ENROL_PERSIST_FAILED ${e.message}`);
|
|
2732
3439
|
}
|
|
2733
3440
|
return;
|
|
2734
3441
|
}
|
|
@@ -2740,8 +3447,13 @@ async function startEdgeNode(opts) {
|
|
|
2740
3447
|
}
|
|
2741
3448
|
return;
|
|
2742
3449
|
}
|
|
3450
|
+
if (msg.type === "policy") {
|
|
3451
|
+
shipper?.setPolicy(msg.telemetry);
|
|
3452
|
+
log.info({ telemetry: shipper?.current() ?? msg.telemetry }, `EDGE_POLICY:logs=${shipper?.current().logs ?? msg.telemetry.logs}`);
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
2743
3455
|
if (msg.type === "cancel") {
|
|
2744
|
-
log(`JOB_CANCEL:${msg.jobId}`);
|
|
3456
|
+
log.info({ jobId: msg.jobId }, `JOB_CANCEL:${msg.jobId}`);
|
|
2745
3457
|
stageRunner.cancel(msg.jobId);
|
|
2746
3458
|
return;
|
|
2747
3459
|
}
|
|
@@ -2749,29 +3461,34 @@ async function startEdgeNode(opts) {
|
|
|
2749
3461
|
if (msg.end === "cancel") stageRunner.cancel(msg.jobId);
|
|
2750
3462
|
else if (msg.end === "complete") stageRunner.endTurns(msg.jobId);
|
|
2751
3463
|
else if (msg.turn) stageRunner.enqueueTurn(msg.jobId, msg.turn);
|
|
2752
|
-
log(`JOB_TURN:${msg.jobId}${msg.end ? `:${msg.end}` : ""}`);
|
|
3464
|
+
log.info({ jobId: msg.jobId, ...msg.end ? { end: msg.end } : {} }, `JOB_TURN:${msg.jobId}${msg.end ? `:${msg.end}` : ""}`);
|
|
2753
3465
|
return;
|
|
2754
3466
|
}
|
|
2755
3467
|
if (msg.type === "push") {
|
|
2756
3468
|
const { job: job2 } = msg;
|
|
3469
|
+
const pushLog = log.child({ runId: job2.runId, jobId: job2.jobId, branch: job2.branch });
|
|
2757
3470
|
if (running.has(job2.jobId)) {
|
|
2758
|
-
|
|
3471
|
+
pushLog.warn({}, `PUSH_DUPLICATE:${job2.runId} ${job2.jobId}`);
|
|
2759
3472
|
return;
|
|
2760
3473
|
}
|
|
2761
3474
|
const cachedPush = lastResults.get(job2.jobId);
|
|
2762
3475
|
if (cachedPush) {
|
|
2763
3476
|
send(cachedPush);
|
|
2764
|
-
|
|
3477
|
+
pushLog.info({}, `PUSH_REPLAY:${job2.runId} ${job2.jobId}`);
|
|
2765
3478
|
return;
|
|
2766
3479
|
}
|
|
2767
3480
|
running.add(job2.jobId);
|
|
2768
|
-
|
|
3481
|
+
const pushStartedAt = Date.now();
|
|
3482
|
+
pushLog.info({}, `PUSH_STARTED:${job2.runId} ${job2.branch}`);
|
|
2769
3483
|
try {
|
|
2770
3484
|
const result = await stageRunner.runPush(job2);
|
|
2771
3485
|
const frame = { type: "push-result", awakeableId: job2.awakeableId, result };
|
|
2772
3486
|
rememberResult(job2.jobId, frame);
|
|
2773
3487
|
send(frame);
|
|
2774
|
-
|
|
3488
|
+
pushLog.info(
|
|
3489
|
+
{ status: result.status, summary: result.summary, durationMs: Date.now() - pushStartedAt },
|
|
3490
|
+
`PUSH_DONE:${job2.runId} ${result.status}${result.status !== "ok" ? ` - ${result.summary}` : ""}`
|
|
3491
|
+
);
|
|
2775
3492
|
} catch (e) {
|
|
2776
3493
|
const frame = {
|
|
2777
3494
|
type: "push-result",
|
|
@@ -2780,7 +3497,7 @@ async function startEdgeNode(opts) {
|
|
|
2780
3497
|
};
|
|
2781
3498
|
rememberResult(job2.jobId, frame);
|
|
2782
3499
|
send(frame);
|
|
2783
|
-
|
|
3500
|
+
pushLog.error({ err: e, durationMs: Date.now() - pushStartedAt }, `PUSH_ERROR:${job2.runId} ${e.message}`);
|
|
2784
3501
|
} finally {
|
|
2785
3502
|
running.delete(job2.jobId);
|
|
2786
3503
|
}
|
|
@@ -2788,24 +3505,37 @@ async function startEdgeNode(opts) {
|
|
|
2788
3505
|
}
|
|
2789
3506
|
if (msg.type !== "job") return;
|
|
2790
3507
|
const { job } = msg;
|
|
3508
|
+
const jobLog = log.child({
|
|
3509
|
+
runId: job.runId,
|
|
3510
|
+
stageId: job.stageId,
|
|
3511
|
+
jobId: job.jobId,
|
|
3512
|
+
...job.tenantId ? { tenantId: job.tenantId } : {},
|
|
3513
|
+
...job.agentConfig?.runtime ? { runtime: job.agentConfig.runtime } : {},
|
|
3514
|
+
...job.agentConfig?.model ? { model: job.agentConfig.model } : {}
|
|
3515
|
+
});
|
|
2791
3516
|
if (running.has(job.jobId)) {
|
|
2792
|
-
|
|
3517
|
+
jobLog.warn({}, `JOB_DUPLICATE:${job.stageId} ${job.jobId}`);
|
|
2793
3518
|
return;
|
|
2794
3519
|
}
|
|
2795
3520
|
const cachedJob = lastResults.get(job.jobId);
|
|
2796
3521
|
if (cachedJob) {
|
|
2797
3522
|
send(cachedJob);
|
|
2798
|
-
|
|
3523
|
+
jobLog.info({}, `JOB_REPLAY:${job.stageId} ${job.jobId}`);
|
|
2799
3524
|
return;
|
|
2800
3525
|
}
|
|
2801
3526
|
running.add(job.jobId);
|
|
2802
|
-
|
|
3527
|
+
counters.activeJobs = running.size;
|
|
3528
|
+
const startedAt = Date.now();
|
|
3529
|
+
jobLog.info({}, `JOB_STARTED:${job.stageId} ${job.jobId}`);
|
|
2803
3530
|
try {
|
|
2804
3531
|
const result = await stageRunner.runJob(job);
|
|
2805
3532
|
const frame = { type: "result", awakeableId: job.awakeableId, result };
|
|
2806
3533
|
rememberResult(job.jobId, frame);
|
|
2807
3534
|
send(frame);
|
|
2808
|
-
|
|
3535
|
+
jobLog.info(
|
|
3536
|
+
{ status: result.status, costUsd: result.costUsd, summary: result.summary, durationMs: Date.now() - startedAt },
|
|
3537
|
+
`JOB_DONE:${job.stageId} ${result.status}`
|
|
3538
|
+
);
|
|
2809
3539
|
} catch (e) {
|
|
2810
3540
|
const frame = {
|
|
2811
3541
|
type: "result",
|
|
@@ -2814,16 +3544,20 @@ async function startEdgeNode(opts) {
|
|
|
2814
3544
|
};
|
|
2815
3545
|
rememberResult(job.jobId, frame);
|
|
2816
3546
|
send(frame);
|
|
2817
|
-
|
|
3547
|
+
counters.recordError(classifyError(e));
|
|
3548
|
+
jobLog.error({ err: e, durationMs: Date.now() - startedAt }, `JOB_ERROR:${job.stageId} ${e.message}`);
|
|
2818
3549
|
} finally {
|
|
2819
3550
|
running.delete(job.jobId);
|
|
3551
|
+
counters.activeJobs = running.size;
|
|
2820
3552
|
}
|
|
2821
3553
|
};
|
|
2822
3554
|
const connect = () => {
|
|
2823
3555
|
const sock = new WebSocket(opts.hubUrl);
|
|
2824
3556
|
ws = sock;
|
|
2825
3557
|
sock.on("open", () => {
|
|
2826
|
-
|
|
3558
|
+
connectCount++;
|
|
3559
|
+
counters.connectCount = connectCount;
|
|
3560
|
+
log.info({ hubUrl: opts.hubUrl, nodeId, connectCount }, "EDGE_CONNECTED");
|
|
2827
3561
|
send({
|
|
2828
3562
|
type: "hello",
|
|
2829
3563
|
enrolToken: opts.enrolToken ?? "",
|
|
@@ -2847,19 +3581,22 @@ async function startEdgeNode(opts) {
|
|
|
2847
3581
|
sock.on("pong", () => {
|
|
2848
3582
|
lastPongAt = Date.now();
|
|
2849
3583
|
});
|
|
2850
|
-
sock.on("error", (e) => log(`EDGE_ERROR ${e.message}`));
|
|
3584
|
+
sock.on("error", (e) => log.error({ err: e, hubUrl: opts.hubUrl }, `EDGE_ERROR ${e.message}`));
|
|
2851
3585
|
sock.on("close", (code, reason) => {
|
|
2852
3586
|
if (heartbeat) clearInterval(heartbeat);
|
|
2853
3587
|
heartbeat = void 0;
|
|
2854
3588
|
if (isEnrolmentRejection(code)) {
|
|
2855
3589
|
shuttingDown = true;
|
|
2856
3590
|
const detail = reason.toString() || "enrolment rejected";
|
|
2857
|
-
log(`EDGE_REJECTED:${code} ${detail}`);
|
|
3591
|
+
log.error({ closeCode: code, detail, fatal: true }, `EDGE_REJECTED:${code} ${detail}`);
|
|
2858
3592
|
process.exitCode = ENROLMENT_REJECTED_EXIT_CODE;
|
|
2859
3593
|
onFatal?.(new Error(`hub rejected edge enrolment (${code}): ${detail}`));
|
|
2860
3594
|
return;
|
|
2861
3595
|
}
|
|
2862
|
-
if (!shuttingDown)
|
|
3596
|
+
if (!shuttingDown) {
|
|
3597
|
+
log.warn({ closeCode: code, reason: reason.toString(), connectCount }, `EDGE_DISCONNECTED:${code} reconnecting in 500ms`);
|
|
3598
|
+
reconnectTimer = setTimeout(connect, 500);
|
|
3599
|
+
}
|
|
2863
3600
|
});
|
|
2864
3601
|
};
|
|
2865
3602
|
opts.signal?.addEventListener(
|
|
@@ -3019,7 +3756,19 @@ function probeHub(opts) {
|
|
|
3019
3756
|
|
|
3020
3757
|
// src/cli.ts
|
|
3021
3758
|
import { parseArgs } from "util";
|
|
3022
|
-
var
|
|
3759
|
+
var DEFAULT_LOG_LINES = 200;
|
|
3760
|
+
var COMMANDS = /* @__PURE__ */ new Set([
|
|
3761
|
+
"start",
|
|
3762
|
+
"stop",
|
|
3763
|
+
"restart",
|
|
3764
|
+
"logs",
|
|
3765
|
+
"diagnose",
|
|
3766
|
+
"run",
|
|
3767
|
+
"service",
|
|
3768
|
+
"doctor",
|
|
3769
|
+
"status",
|
|
3770
|
+
"update"
|
|
3771
|
+
]);
|
|
3023
3772
|
var isCommand = (s) => COMMANDS.has(s);
|
|
3024
3773
|
function parseCli(argv) {
|
|
3025
3774
|
const [first, ...rest] = argv;
|
|
@@ -3040,6 +3789,8 @@ function parseCli(argv) {
|
|
|
3040
3789
|
if (command === "run") return parseRun(flagArgs);
|
|
3041
3790
|
if (command === "service") return parseService(flagArgs);
|
|
3042
3791
|
if (command === "update") return parseUpdate(flagArgs);
|
|
3792
|
+
if (command === "logs") return parseLogs(flagArgs);
|
|
3793
|
+
if (command === "diagnose") return parseDiagnose(flagArgs);
|
|
3043
3794
|
let values;
|
|
3044
3795
|
try {
|
|
3045
3796
|
({ values } = parseArgs({
|
|
@@ -3049,6 +3800,7 @@ function parseCli(argv) {
|
|
|
3049
3800
|
name: { type: "string" },
|
|
3050
3801
|
"hub-url": { type: "string" },
|
|
3051
3802
|
ephemeral: { type: "boolean", default: false },
|
|
3803
|
+
foreground: { type: "boolean", default: false },
|
|
3052
3804
|
help: { type: "boolean", default: false }
|
|
3053
3805
|
},
|
|
3054
3806
|
allowPositionals: false
|
|
@@ -3057,14 +3809,74 @@ function parseCli(argv) {
|
|
|
3057
3809
|
return { kind: "error", message: e.message };
|
|
3058
3810
|
}
|
|
3059
3811
|
if (values.help) return { kind: "help", command };
|
|
3812
|
+
const ephemeral = values.ephemeral ?? false;
|
|
3060
3813
|
const flags = {
|
|
3061
3814
|
...values.token ? { token: values.token } : {},
|
|
3062
3815
|
...values.name ? { name: values.name } : {},
|
|
3063
3816
|
...values["hub-url"] ? { hubUrl: values["hub-url"] } : {},
|
|
3064
|
-
ephemeral
|
|
3817
|
+
ephemeral,
|
|
3818
|
+
// An ephemeral node mints a throwaway id and persists nothing, so there is nothing coherent to hand to
|
|
3819
|
+
// a supervisor that restarts it on boot. It is a foreground node by definition.
|
|
3820
|
+
foreground: (values.foreground ?? false) || ephemeral
|
|
3065
3821
|
};
|
|
3822
|
+
if (command === "stop") return { kind: "stop" };
|
|
3066
3823
|
return { kind: command, flags };
|
|
3067
3824
|
}
|
|
3825
|
+
var LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"];
|
|
3826
|
+
function parseLogs(flagArgs) {
|
|
3827
|
+
let values;
|
|
3828
|
+
try {
|
|
3829
|
+
({ values } = parseArgs({
|
|
3830
|
+
args: flagArgs,
|
|
3831
|
+
options: {
|
|
3832
|
+
follow: { type: "boolean", short: "f", default: false },
|
|
3833
|
+
lines: { type: "string", short: "n" },
|
|
3834
|
+
level: { type: "string" },
|
|
3835
|
+
run: { type: "string" },
|
|
3836
|
+
json: { type: "boolean", default: false },
|
|
3837
|
+
help: { type: "boolean", default: false }
|
|
3838
|
+
},
|
|
3839
|
+
allowPositionals: false
|
|
3840
|
+
}));
|
|
3841
|
+
} catch (e) {
|
|
3842
|
+
return { kind: "error", message: e.message };
|
|
3843
|
+
}
|
|
3844
|
+
if (values.help) return { kind: "help", command: "logs" };
|
|
3845
|
+
const lines = values.lines === void 0 ? DEFAULT_LOG_LINES : Number(values.lines);
|
|
3846
|
+
if (!Number.isInteger(lines) || lines < 0) {
|
|
3847
|
+
return { kind: "error", message: `logs: --lines must be a non-negative whole number (got "${values.lines}")` };
|
|
3848
|
+
}
|
|
3849
|
+
if (values.level !== void 0 && !LOG_LEVELS.includes(values.level)) {
|
|
3850
|
+
return { kind: "error", message: `logs: --level must be one of ${LOG_LEVELS.join(", ")} (got "${values.level}")` };
|
|
3851
|
+
}
|
|
3852
|
+
return {
|
|
3853
|
+
kind: "logs",
|
|
3854
|
+
flags: {
|
|
3855
|
+
lines,
|
|
3856
|
+
follow: values.follow ?? false,
|
|
3857
|
+
...values.level ? { level: values.level } : {},
|
|
3858
|
+
...values.run ? { run: values.run } : {},
|
|
3859
|
+
json: values.json ?? false
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
function parseDiagnose(flagArgs) {
|
|
3864
|
+
let values;
|
|
3865
|
+
try {
|
|
3866
|
+
({ values } = parseArgs({
|
|
3867
|
+
args: flagArgs,
|
|
3868
|
+
options: {
|
|
3869
|
+
out: { type: "string" },
|
|
3870
|
+
help: { type: "boolean", default: false }
|
|
3871
|
+
},
|
|
3872
|
+
allowPositionals: false
|
|
3873
|
+
}));
|
|
3874
|
+
} catch (e) {
|
|
3875
|
+
return { kind: "error", message: e.message };
|
|
3876
|
+
}
|
|
3877
|
+
if (values.help) return { kind: "help", command: "diagnose" };
|
|
3878
|
+
return { kind: "diagnose", flags: { ...values.out ? { out: values.out } : {} } };
|
|
3879
|
+
}
|
|
3068
3880
|
function parseRun(flagArgs) {
|
|
3069
3881
|
let values;
|
|
3070
3882
|
let positionals;
|
|
@@ -3157,16 +3969,96 @@ function usage(bin, command) {
|
|
|
3157
3969
|
return [
|
|
3158
3970
|
`Usage: ${bin} start [--token <token>] [options]`,
|
|
3159
3971
|
"",
|
|
3160
|
-
"
|
|
3972
|
+
"Make this node run, and keep running: install it as an always-on service (launchd / systemd), start",
|
|
3973
|
+
"it, and return. It restarts on failure and comes back after a reboot. Running it again is a no-op if",
|
|
3974
|
+
"the node is already up, so it is safe to repeat.",
|
|
3161
3975
|
"",
|
|
3162
3976
|
"A token is needed only to enrol. Once the hub accepts it, it is cached in ~/.dahrk/node.json and",
|
|
3163
3977
|
"every later `start` re-attaches without one. Pass --token again to re-enrol (rotated token, new pool).",
|
|
3164
3978
|
"",
|
|
3979
|
+
"Once it is running:",
|
|
3980
|
+
` ${bin} logs -f Watch what it is doing.`,
|
|
3981
|
+
` ${bin} status Is it enrolled, and is it up?`,
|
|
3982
|
+
` ${bin} stop Stop it (it stays stopped until the next \`start\`).`,
|
|
3983
|
+
"",
|
|
3165
3984
|
"Options:",
|
|
3166
3985
|
" --token <token> Enrolment token (first run only; or set DAHRK_ENROL_TOKEN).",
|
|
3167
3986
|
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
3168
3987
|
" --name <name> Display-name override (else the hub assigns one).",
|
|
3169
|
-
" --
|
|
3988
|
+
" --foreground Run the node in THIS terminal and block, instead of as a service. For",
|
|
3989
|
+
" containers, pm2, CI, or just watching one work. Ctrl-C stops it. Same thing as",
|
|
3990
|
+
" setting DAHRK_FOREGROUND=1, which is easier to set in a Dockerfile or pm2 config.",
|
|
3991
|
+
" --ephemeral Do not persist (or read) node id and token; mint a throwaway id (CI / one-shot).",
|
|
3992
|
+
" Implies --foreground: a node with no persistent identity has nothing to daemonise.",
|
|
3993
|
+
"",
|
|
3994
|
+
"Only one node may run at a time on a host - they would share this machine's node id and race each",
|
|
3995
|
+
"other for Jobs - so a second `start` refuses rather than dialling the hub twice."
|
|
3996
|
+
].join("\n");
|
|
3997
|
+
}
|
|
3998
|
+
if (command === "stop") {
|
|
3999
|
+
return [
|
|
4000
|
+
`Usage: ${bin} stop`,
|
|
4001
|
+
"",
|
|
4002
|
+
"Stop the node. It stays stopped across reboots until you run `dahrk start` again - a stop that",
|
|
4003
|
+
"quietly undid itself at the next boot would not be a stop.",
|
|
4004
|
+
"",
|
|
4005
|
+
"The service stays installed, so starting it again is instant. To remove it entirely, use",
|
|
4006
|
+
"`dahrk service uninstall`. To stop a node running in a terminal (--foreground), press Ctrl-C."
|
|
4007
|
+
].join("\n");
|
|
4008
|
+
}
|
|
4009
|
+
if (command === "restart") {
|
|
4010
|
+
return [
|
|
4011
|
+
`Usage: ${bin} restart [options]`,
|
|
4012
|
+
"",
|
|
4013
|
+
"Stop the node and start it again. Picks up a new client version, a rotated token, or a runtime you",
|
|
4014
|
+
"have just installed (the node detects runtimes at boot, so a fresh `claude` on PATH needs a restart).",
|
|
4015
|
+
"",
|
|
4016
|
+
"Options:",
|
|
4017
|
+
" --token <token> Re-enrol with a new token (or set DAHRK_ENROL_TOKEN).",
|
|
4018
|
+
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
4019
|
+
" --name <name> Display-name override (else the hub assigns one)."
|
|
4020
|
+
].join("\n");
|
|
4021
|
+
}
|
|
4022
|
+
if (command === "logs") {
|
|
4023
|
+
return [
|
|
4024
|
+
`Usage: ${bin} logs [-f] [-n <lines>] [--level <l>] [--run <id>] [--json]`,
|
|
4025
|
+
"",
|
|
4026
|
+
"Show what the node has been doing: its connect / welcome handshake, the Jobs it has run, and anything",
|
|
4027
|
+
"it has crashed on.",
|
|
4028
|
+
"",
|
|
4029
|
+
"There are two logs, and this reads whichever you ask for. On its own it tails the transcript the",
|
|
4030
|
+
"service captured (~/.dahrk/logs/node.out.log and node.err.log) - the same lines the node printed.",
|
|
4031
|
+
"Pass --level, --run or --json and it reads node.jsonl instead: the node's own structured log, which",
|
|
4032
|
+
"is the one carrying levels, timestamps, correlation ids and full error stacks. It is written at",
|
|
4033
|
+
"debug even when the terminal is not, so the detail for an incident is already there afterwards.",
|
|
4034
|
+
"",
|
|
4035
|
+
"Options:",
|
|
4036
|
+
" -f, --follow Keep watching as new lines arrive (Ctrl-C to stop).",
|
|
4037
|
+
" -n, --lines <n> Lines of history to show first (default 200).",
|
|
4038
|
+
" --level <level> Only this level and above: trace, debug, info, warn, error, fatal.",
|
|
4039
|
+
" --run <runId> Only this run. The runId is the hub's - so `logs --run <id>` and the hub's",
|
|
4040
|
+
" view of the same run describe the same thing from both ends.",
|
|
4041
|
+
" --json Print the raw JSON records (for jq) rather than a rendered line.",
|
|
4042
|
+
"",
|
|
4043
|
+
"A node run with --foreground prints to its terminal, but it still writes node.jsonl - so --run and",
|
|
4044
|
+
"--level work for it too."
|
|
4045
|
+
].join("\n");
|
|
4046
|
+
}
|
|
4047
|
+
if (command === "diagnose") {
|
|
4048
|
+
return [
|
|
4049
|
+
`Usage: ${bin} diagnose [--out <path>]`,
|
|
4050
|
+
"",
|
|
4051
|
+
"Write a support bundle: everything needed to debug this node, in one file you can read.",
|
|
4052
|
+
"",
|
|
4053
|
+
"It collects this node's id, name, tenant, version and host; the doctor's verdict; the tail of the",
|
|
4054
|
+
"structured log; and every crash record. Secrets are stripped and the enrolment token is not",
|
|
4055
|
+
"included. Your source code, prompts and issue content are not included.",
|
|
4056
|
+
"",
|
|
4057
|
+
"Nothing is uploaded. There is no flag to upload it. The file is written locally so that you can open",
|
|
4058
|
+
"it, read every line, and decide for yourself whether to send it on.",
|
|
4059
|
+
"",
|
|
4060
|
+
"Options:",
|
|
4061
|
+
" --out <path> Write the bundle here (default: ./dahrk-diagnose-<timestamp>.json)."
|
|
3170
4062
|
].join("\n");
|
|
3171
4063
|
}
|
|
3172
4064
|
if (command === "run") {
|
|
@@ -3242,19 +4134,139 @@ function usage(bin, command) {
|
|
|
3242
4134
|
`Usage: ${bin} <command> [options]`,
|
|
3243
4135
|
"",
|
|
3244
4136
|
"Commands:",
|
|
3245
|
-
" start Run the
|
|
4137
|
+
" start Run the node, and keep it running (installs the service). --token to enrol, once.",
|
|
4138
|
+
" stop Stop the node. It stays stopped until the next `start`.",
|
|
4139
|
+
" restart Stop it and start it again.",
|
|
4140
|
+
" logs Show what the node is doing (-f to follow, --run <id> to narrow to one run).",
|
|
4141
|
+
" diagnose Write a support bundle you can read, and send on if you choose. Uploads nothing.",
|
|
4142
|
+
" status Is this node enrolled, and is it up? (local, dials nothing)",
|
|
3246
4143
|
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
3247
|
-
" service Install/uninstall the node as an always-on service (launchd/systemd).",
|
|
3248
|
-
" status Is this node enrolled, and is the service running? (local, dials nothing)",
|
|
3249
4144
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
4145
|
+
" service Install/uninstall the always-on service by hand (`start` does this for you).",
|
|
3250
4146
|
" update Update the client to the latest release (or print how for your channel).",
|
|
3251
4147
|
" version Print the client version.",
|
|
3252
4148
|
" help Show this help, or `help <command>` for a command's options.",
|
|
3253
4149
|
"",
|
|
3254
|
-
`Run \`${bin} help <command>\` for command-specific options
|
|
4150
|
+
`Run \`${bin} help <command>\` for command-specific options.`,
|
|
4151
|
+
`To run a node in this terminal instead of as a service: \`${bin} start --foreground\`.`
|
|
3255
4152
|
].join("\n");
|
|
3256
4153
|
}
|
|
3257
4154
|
|
|
4155
|
+
// src/diagnose.ts
|
|
4156
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
4157
|
+
import { join as join9 } from "path";
|
|
4158
|
+
var BUNDLE_LOG_LINES = 2e3;
|
|
4159
|
+
function tailJsonl(raw, n) {
|
|
4160
|
+
const records = [];
|
|
4161
|
+
for (const line of raw.split("\n")) {
|
|
4162
|
+
if (!line.trim()) continue;
|
|
4163
|
+
try {
|
|
4164
|
+
records.push(JSON.parse(line));
|
|
4165
|
+
} catch {
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
return n > 0 ? records.slice(-n) : records;
|
|
4169
|
+
}
|
|
4170
|
+
async function buildBundle(deps) {
|
|
4171
|
+
const warnings = [];
|
|
4172
|
+
let state = {};
|
|
4173
|
+
if (deps.exists(deps.stateFile)) {
|
|
4174
|
+
try {
|
|
4175
|
+
const parsed = JSON.parse(deps.readFile(deps.stateFile));
|
|
4176
|
+
const { enrolToken: _dropped, ...rest } = parsed;
|
|
4177
|
+
state = rest;
|
|
4178
|
+
} catch (e) {
|
|
4179
|
+
warnings.push(`could not read node state (${e.message})`);
|
|
4180
|
+
}
|
|
4181
|
+
} else {
|
|
4182
|
+
warnings.push("no node.json - this node has never enrolled");
|
|
4183
|
+
}
|
|
4184
|
+
let log = [];
|
|
4185
|
+
if (deps.exists(deps.jsonlFile)) {
|
|
4186
|
+
try {
|
|
4187
|
+
log = tailJsonl(deps.readFile(deps.jsonlFile), BUNDLE_LOG_LINES);
|
|
4188
|
+
} catch (e) {
|
|
4189
|
+
warnings.push(`could not read node.jsonl (${e.message})`);
|
|
4190
|
+
}
|
|
4191
|
+
} else {
|
|
4192
|
+
warnings.push("no node.jsonl - the node has not run since structured logging was added");
|
|
4193
|
+
}
|
|
4194
|
+
const crashes = [];
|
|
4195
|
+
if (deps.exists(deps.crashDir)) {
|
|
4196
|
+
for (const name of deps.listDir(deps.crashDir).filter((f) => f.endsWith(".json")).sort()) {
|
|
4197
|
+
try {
|
|
4198
|
+
crashes.push(JSON.parse(deps.readFile(join9(deps.crashDir, name))));
|
|
4199
|
+
} catch (e) {
|
|
4200
|
+
warnings.push(`could not read crash record ${name} (${e.message})`);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
let doctor;
|
|
4205
|
+
if (deps.doctor) {
|
|
4206
|
+
try {
|
|
4207
|
+
doctor = await deps.doctor();
|
|
4208
|
+
} catch (e) {
|
|
4209
|
+
warnings.push(`doctor failed (${e.message})`);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
const bundle = {
|
|
4213
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4214
|
+
clientVersion: deps.clientVersion,
|
|
4215
|
+
node: {
|
|
4216
|
+
state,
|
|
4217
|
+
platform: process.platform,
|
|
4218
|
+
arch: process.arch,
|
|
4219
|
+
nodeVersion: process.version
|
|
4220
|
+
},
|
|
4221
|
+
...doctor ? { doctor } : {},
|
|
4222
|
+
log,
|
|
4223
|
+
crashes,
|
|
4224
|
+
warnings
|
|
4225
|
+
};
|
|
4226
|
+
return scrubValue(bundle);
|
|
4227
|
+
}
|
|
4228
|
+
async function runDiagnose(deps) {
|
|
4229
|
+
const bundle = await buildBundle(deps);
|
|
4230
|
+
try {
|
|
4231
|
+
deps.writeFile(deps.outFile, `${JSON.stringify(bundle, null, 2)}
|
|
4232
|
+
`);
|
|
4233
|
+
} catch (e) {
|
|
4234
|
+
deps.out(`Could not write the bundle: ${e.message}`);
|
|
4235
|
+
return 1;
|
|
4236
|
+
}
|
|
4237
|
+
deps.out(`Wrote a support bundle to ${deps.outFile}`);
|
|
4238
|
+
deps.out("");
|
|
4239
|
+
deps.out(`It contains: this node's id, name and tenant; its version and host; ${bundle.log.length} log`);
|
|
4240
|
+
deps.out(`lines; and ${bundle.crashes.length} crash record(s). Secrets are stripped and the enrolment`);
|
|
4241
|
+
deps.out("token is not in it. Your source code, prompts and issue content are not in it.");
|
|
4242
|
+
deps.out("");
|
|
4243
|
+
deps.out("Nothing has been sent anywhere. Open the file, read it, and send it on only if you are happy to.");
|
|
4244
|
+
if (bundle.warnings.length > 0) {
|
|
4245
|
+
deps.out("");
|
|
4246
|
+
deps.out("Note:");
|
|
4247
|
+
for (const w of bundle.warnings) deps.out(` - ${w}`);
|
|
4248
|
+
}
|
|
4249
|
+
return 0;
|
|
4250
|
+
}
|
|
4251
|
+
var defaultDiagnoseDeps = (paths, clientVersion, doctor) => ({
|
|
4252
|
+
...paths,
|
|
4253
|
+
clientVersion,
|
|
4254
|
+
...doctor ? { doctor } : {},
|
|
4255
|
+
readFile: (p) => readFileSync5(p, "utf8"),
|
|
4256
|
+
listDir: (p) => readdirSync4(p),
|
|
4257
|
+
exists: (p) => existsSync6(p),
|
|
4258
|
+
writeFile: (p, content) => {
|
|
4259
|
+
const dir = join9(p, "..");
|
|
4260
|
+
if (!existsSync6(dir)) mkdirSync7(dir, { recursive: true });
|
|
4261
|
+
writeFileSync6(p, content, { mode: 384 });
|
|
4262
|
+
},
|
|
4263
|
+
out: (line) => void process.stdout.write(`${line}
|
|
4264
|
+
`)
|
|
4265
|
+
});
|
|
4266
|
+
function defaultBundlePath(cwd, now) {
|
|
4267
|
+
return join9(cwd, `dahrk-diagnose-${now.toISOString().replace(/[:.]/g, "-")}.json`);
|
|
4268
|
+
}
|
|
4269
|
+
|
|
3258
4270
|
// src/doctor.ts
|
|
3259
4271
|
var MIN_NODE_MAJOR = 22;
|
|
3260
4272
|
var TAG = { pass: "[PASS]", warn: "[WARN]", fail: "[FAIL]" };
|
|
@@ -3386,12 +4398,222 @@ async function runDoctor(inputs, deps = {}) {
|
|
|
3386
4398
|
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
3387
4399
|
}
|
|
3388
4400
|
|
|
4401
|
+
// src/lock.ts
|
|
4402
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4403
|
+
import { dirname as dirname4 } from "path";
|
|
4404
|
+
function parseLock(content) {
|
|
4405
|
+
if (!content) return void 0;
|
|
4406
|
+
const pid = Number(content.trim());
|
|
4407
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
4408
|
+
}
|
|
4409
|
+
function acquireLock(deps) {
|
|
4410
|
+
const held = parseLock(deps.readFile(deps.file));
|
|
4411
|
+
if (held !== void 0 && held !== deps.pid && deps.isAlive(held)) {
|
|
4412
|
+
return { ok: false, heldBy: held };
|
|
4413
|
+
}
|
|
4414
|
+
deps.writeFile(deps.file, `${deps.pid}
|
|
4415
|
+
`);
|
|
4416
|
+
return { ok: true, release: () => releaseLock(deps) };
|
|
4417
|
+
}
|
|
4418
|
+
function releaseLock(deps) {
|
|
4419
|
+
if (parseLock(deps.readFile(deps.file)) !== deps.pid) return;
|
|
4420
|
+
deps.removeFile(deps.file);
|
|
4421
|
+
}
|
|
4422
|
+
function isAlive(pid) {
|
|
4423
|
+
try {
|
|
4424
|
+
process.kill(pid, 0);
|
|
4425
|
+
return true;
|
|
4426
|
+
} catch (e) {
|
|
4427
|
+
return e.code === "EPERM";
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
var defaultLockDeps = (file) => ({
|
|
4431
|
+
file,
|
|
4432
|
+
pid: process.pid,
|
|
4433
|
+
readFile: (path) => {
|
|
4434
|
+
try {
|
|
4435
|
+
return readFileSync6(path, "utf8");
|
|
4436
|
+
} catch {
|
|
4437
|
+
return void 0;
|
|
4438
|
+
}
|
|
4439
|
+
},
|
|
4440
|
+
writeFile: (path, content) => {
|
|
4441
|
+
if (!existsSync7(dirname4(path))) mkdirSync8(dirname4(path), { recursive: true, mode: 448 });
|
|
4442
|
+
writeFileSync7(path, content);
|
|
4443
|
+
},
|
|
4444
|
+
removeFile: (path) => rmSync6(path, { force: true }),
|
|
4445
|
+
isAlive
|
|
4446
|
+
});
|
|
4447
|
+
|
|
4448
|
+
// src/logs.ts
|
|
4449
|
+
import { spawn } from "child_process";
|
|
4450
|
+
import { copyFileSync, existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4, truncateSync } from "fs";
|
|
4451
|
+
var MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
4452
|
+
function logsCommand(inputs) {
|
|
4453
|
+
return [
|
|
4454
|
+
"tail",
|
|
4455
|
+
"-n",
|
|
4456
|
+
String(inputs.lines),
|
|
4457
|
+
...inputs.follow ? ["-f"] : [],
|
|
4458
|
+
...inputs.files
|
|
4459
|
+
];
|
|
4460
|
+
}
|
|
4461
|
+
var LEVEL_RANK = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 };
|
|
4462
|
+
var levelName = (n) => Object.entries(LEVEL_RANK).find(([, rank]) => rank === n)?.[0] ?? String(n);
|
|
4463
|
+
function parseRecords(raw) {
|
|
4464
|
+
const out = [];
|
|
4465
|
+
for (const line of raw.split("\n")) {
|
|
4466
|
+
if (!line.trim()) continue;
|
|
4467
|
+
try {
|
|
4468
|
+
out.push(JSON.parse(line));
|
|
4469
|
+
} catch {
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
return out;
|
|
4473
|
+
}
|
|
4474
|
+
function filterRecords(records, q) {
|
|
4475
|
+
const min = q.level ? LEVEL_RANK[q.level] ?? 0 : 0;
|
|
4476
|
+
const matched = records.filter((r) => r.level >= min && (q.run === void 0 || r.runId === q.run));
|
|
4477
|
+
return q.lines > 0 ? matched.slice(-q.lines) : matched;
|
|
4478
|
+
}
|
|
4479
|
+
function renderRecord(r) {
|
|
4480
|
+
const where = [r.runId, r.stageId].filter(Boolean).join("/");
|
|
4481
|
+
const scope = where ? ` [${where}]` : r.component ? ` [${r.component}]` : "";
|
|
4482
|
+
const head = `${r.time} ${levelName(r.level).toUpperCase().padEnd(5)}${scope} ${r.msg}`;
|
|
4483
|
+
const lines = [head];
|
|
4484
|
+
if (r.err?.stack) lines.push(...r.err.stack.split("\n").map((l) => ` ${l}`));
|
|
4485
|
+
return lines;
|
|
4486
|
+
}
|
|
4487
|
+
async function runLogs(inputs, deps) {
|
|
4488
|
+
if (inputs.level !== void 0 || inputs.run !== void 0 || inputs.json) {
|
|
4489
|
+
return runStructuredLogs(inputs, deps);
|
|
4490
|
+
}
|
|
4491
|
+
const files = [deps.files.out, deps.files.err].filter((f) => deps.fileExists(f));
|
|
4492
|
+
if (files.length === 0) {
|
|
4493
|
+
deps.out("No logs yet - this node has not run under the service.");
|
|
4494
|
+
deps.out("Start it with `dahrk start`, or check what it thinks it is doing with `dahrk status`.");
|
|
4495
|
+
deps.out("");
|
|
4496
|
+
deps.out("(A node run in a terminal with `--foreground` logs to that terminal, not to a file.)");
|
|
4497
|
+
return 0;
|
|
4498
|
+
}
|
|
4499
|
+
return deps.run(logsCommand({ files, lines: inputs.lines, follow: inputs.follow }));
|
|
4500
|
+
}
|
|
4501
|
+
async function runStructuredLogs(inputs, deps) {
|
|
4502
|
+
if (!deps.fileExists(deps.jsonlFile)) {
|
|
4503
|
+
deps.out("No structured log yet - this node has not run since structured logging was added.");
|
|
4504
|
+
deps.out("Start it with `dahrk start`; it writes ~/.dahrk/logs/node.jsonl from the first boot.");
|
|
4505
|
+
return 0;
|
|
4506
|
+
}
|
|
4507
|
+
if (inputs.follow) {
|
|
4508
|
+
deps.out("(--follow on the structured log streams it unfiltered; pipe it through jq to narrow it:)");
|
|
4509
|
+
deps.out(` tail -f ${deps.jsonlFile} | jq -c 'select(.runId == "<runId>")'`);
|
|
4510
|
+
deps.out("");
|
|
4511
|
+
return deps.run(["tail", "-n", String(inputs.lines), "-f", deps.jsonlFile]);
|
|
4512
|
+
}
|
|
4513
|
+
const records = filterRecords(parseRecords(deps.readFile(deps.jsonlFile)), {
|
|
4514
|
+
level: inputs.level,
|
|
4515
|
+
run: inputs.run,
|
|
4516
|
+
lines: inputs.lines
|
|
4517
|
+
});
|
|
4518
|
+
if (records.length === 0) {
|
|
4519
|
+
const narrowed = [inputs.run ? `run ${inputs.run}` : void 0, inputs.level ? `level ${inputs.level}+` : void 0].filter(Boolean).join(" at ");
|
|
4520
|
+
deps.out(narrowed ? `No records for ${narrowed}.` : "No records.");
|
|
4521
|
+
return 0;
|
|
4522
|
+
}
|
|
4523
|
+
for (const r of records) {
|
|
4524
|
+
if (inputs.json) deps.out(JSON.stringify(r));
|
|
4525
|
+
else for (const line of renderRecord(r)) deps.out(line);
|
|
4526
|
+
}
|
|
4527
|
+
return 0;
|
|
4528
|
+
}
|
|
4529
|
+
function rotateIfLarge(file, maxBytes = MAX_LOG_BYTES) {
|
|
4530
|
+
try {
|
|
4531
|
+
if (!existsSync8(file) || statSync4(file).size <= maxBytes) return;
|
|
4532
|
+
copyFileSync(file, `${file}.1`);
|
|
4533
|
+
truncateSync(file, 0);
|
|
4534
|
+
} catch {
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
var defaultLogsDeps = (files, jsonlFile) => ({
|
|
4538
|
+
files,
|
|
4539
|
+
jsonlFile,
|
|
4540
|
+
fileExists: (path) => existsSync8(path),
|
|
4541
|
+
readFile: (path) => readFileSync7(path, "utf8"),
|
|
4542
|
+
run: (argv) => new Promise((resolve2) => {
|
|
4543
|
+
const [cmd, ...args] = argv;
|
|
4544
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
4545
|
+
child.on("error", () => resolve2(1));
|
|
4546
|
+
child.on("close", (code) => resolve2(code ?? 0));
|
|
4547
|
+
}),
|
|
4548
|
+
out: (line) => void process.stdout.write(`${line}
|
|
4549
|
+
`)
|
|
4550
|
+
});
|
|
4551
|
+
|
|
4552
|
+
// src/process-safety.ts
|
|
4553
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
4554
|
+
import { join as join10 } from "path";
|
|
4555
|
+
function writeCrashRecord(dir, record) {
|
|
4556
|
+
try {
|
|
4557
|
+
mkdirSync9(dir, { recursive: true, mode: 448 });
|
|
4558
|
+
const file = join10(dir, `${record.at.replace(/[:.]/g, "-")}.json`);
|
|
4559
|
+
writeFileSync8(file, `${JSON.stringify(record, null, 2)}
|
|
4560
|
+
`, { mode: 384 });
|
|
4561
|
+
return file;
|
|
4562
|
+
} catch {
|
|
4563
|
+
return void 0;
|
|
4564
|
+
}
|
|
4565
|
+
}
|
|
4566
|
+
function toRecord(kind, reason, opts) {
|
|
4567
|
+
const err = reason instanceof Error ? reason : void 0;
|
|
4568
|
+
return {
|
|
4569
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4570
|
+
kind,
|
|
4571
|
+
name: err?.name ?? typeof reason,
|
|
4572
|
+
message: err?.message ?? String(reason),
|
|
4573
|
+
...err?.stack ? { stack: err.stack } : {},
|
|
4574
|
+
clientVersion: opts.clientVersion,
|
|
4575
|
+
nodeVersion: process.version,
|
|
4576
|
+
platform: process.platform,
|
|
4577
|
+
arch: process.arch,
|
|
4578
|
+
uptimeSec: Math.round(process.uptime()),
|
|
4579
|
+
...opts.activeJobIds ? { activeJobIds: opts.activeJobIds() } : {}
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
function makeCrashHandlers(opts) {
|
|
4583
|
+
const handle = (kind, reason) => {
|
|
4584
|
+
const record = toRecord(kind, reason, opts);
|
|
4585
|
+
const file = opts.crashDir ? writeCrashRecord(opts.crashDir, record) : void 0;
|
|
4586
|
+
opts.logger.error(
|
|
4587
|
+
{ err: reason, kind, ...file ? { crashFile: file } : {}, activeJobIds: record.activeJobIds },
|
|
4588
|
+
`NODE_CRASH:${kind} ${record.name}: ${record.message}`
|
|
4589
|
+
);
|
|
4590
|
+
};
|
|
4591
|
+
return {
|
|
4592
|
+
onUnhandledRejection: (reason) => handle("unhandledRejection", reason),
|
|
4593
|
+
onUncaughtException: (err) => {
|
|
4594
|
+
handle("uncaughtException", err);
|
|
4595
|
+
if (opts.exitOnCrash) process.exit(1);
|
|
4596
|
+
}
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
function installProcessSafetyNet(opts) {
|
|
4600
|
+
const { onUnhandledRejection, onUncaughtException } = makeCrashHandlers(opts);
|
|
4601
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
4602
|
+
process.on("uncaughtException", onUncaughtException);
|
|
4603
|
+
return {
|
|
4604
|
+
uninstall: () => {
|
|
4605
|
+
process.off("unhandledRejection", onUnhandledRejection);
|
|
4606
|
+
process.off("uncaughtException", onUncaughtException);
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
|
|
3389
4611
|
// src/preflight.ts
|
|
3390
|
-
import { execFileSync as
|
|
4612
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3391
4613
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3392
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
4614
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync9, readdirSync as readdirSync5, statfsSync as statfsSync2 } from "fs";
|
|
3393
4615
|
import { homedir as homedir2 } from "os";
|
|
3394
|
-
import { join as
|
|
4616
|
+
import { join as join11 } from "path";
|
|
3395
4617
|
var REPORT_BASE_URL = "https://app.dahrk.ai/r";
|
|
3396
4618
|
var LOW_DISK_BYTES = 512 * 1024 * 1024;
|
|
3397
4619
|
var PREFLIGHT_STAGES = [
|
|
@@ -3520,7 +4742,7 @@ var defaultDeps2 = () => ({
|
|
|
3520
4742
|
});
|
|
3521
4743
|
function commandPresent(cmd) {
|
|
3522
4744
|
try {
|
|
3523
|
-
|
|
4745
|
+
execFileSync5("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" });
|
|
3524
4746
|
return true;
|
|
3525
4747
|
} catch {
|
|
3526
4748
|
return false;
|
|
@@ -3528,12 +4750,12 @@ function commandPresent(cmd) {
|
|
|
3528
4750
|
}
|
|
3529
4751
|
function sshKeyPresent() {
|
|
3530
4752
|
try {
|
|
3531
|
-
const dir =
|
|
3532
|
-
if (
|
|
4753
|
+
const dir = join11(homedir2(), ".ssh");
|
|
4754
|
+
if (existsSync9(dir) && readdirSync5(dir).some((f) => f.endsWith(".pub"))) return true;
|
|
3533
4755
|
} catch {
|
|
3534
4756
|
}
|
|
3535
4757
|
try {
|
|
3536
|
-
|
|
4758
|
+
execFileSync5("ssh-add", ["-l"], { stdio: "ignore" });
|
|
3537
4759
|
return true;
|
|
3538
4760
|
} catch {
|
|
3539
4761
|
return false;
|
|
@@ -3548,12 +4770,12 @@ function writable(dir) {
|
|
|
3548
4770
|
}
|
|
3549
4771
|
}
|
|
3550
4772
|
function worktreeRoot(env) {
|
|
3551
|
-
return env.DAHRK_WORKTREES_DIR ??
|
|
4773
|
+
return env.DAHRK_WORKTREES_DIR ?? join11(env.DAHRK_STATE_DIR ?? join11(homedir2(), ".dahrk"), "worktrees");
|
|
3552
4774
|
}
|
|
3553
4775
|
function nearestExisting(dir) {
|
|
3554
4776
|
let cur = dir;
|
|
3555
|
-
while (!
|
|
3556
|
-
const parent =
|
|
4777
|
+
while (!existsSync9(cur)) {
|
|
4778
|
+
const parent = join11(cur, "..");
|
|
3557
4779
|
if (parent === cur) break;
|
|
3558
4780
|
cur = parent;
|
|
3559
4781
|
}
|
|
@@ -3561,14 +4783,14 @@ function nearestExisting(dir) {
|
|
|
3561
4783
|
}
|
|
3562
4784
|
function freeDiskBytes(dir) {
|
|
3563
4785
|
try {
|
|
3564
|
-
const s =
|
|
4786
|
+
const s = statfsSync2(nearestExisting(dir));
|
|
3565
4787
|
return s.bavail * s.bsize;
|
|
3566
4788
|
} catch {
|
|
3567
4789
|
return void 0;
|
|
3568
4790
|
}
|
|
3569
4791
|
}
|
|
3570
4792
|
function probeRepo(repoPath) {
|
|
3571
|
-
const git = (args) =>
|
|
4793
|
+
const git = (args) => execFileSync5("git", ["-C", repoPath, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3572
4794
|
try {
|
|
3573
4795
|
if (git(["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
3574
4796
|
return { path: repoPath, isGitRepo: false, headResolves: false, detail: "not inside a work tree" };
|
|
@@ -3603,10 +4825,97 @@ function gatherHostFacts(repoPath) {
|
|
|
3603
4825
|
}
|
|
3604
4826
|
|
|
3605
4827
|
// src/service.ts
|
|
3606
|
-
import { execFileSync as
|
|
3607
|
-
import { chmodSync, existsSync as
|
|
3608
|
-
import { homedir as
|
|
3609
|
-
import { join as
|
|
4828
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
4829
|
+
import { chmodSync as chmodSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
4830
|
+
import { homedir as homedir4, platform as osPlatform3, userInfo } from "os";
|
|
4831
|
+
import { join as join13 } from "path";
|
|
4832
|
+
|
|
4833
|
+
// src/state.ts
|
|
4834
|
+
import { chmodSync, existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
4835
|
+
import { homedir as homedir3 } from "os";
|
|
4836
|
+
import { join as join12 } from "path";
|
|
4837
|
+
var STRING_FIELDS = ["nodeId", "enrolToken", "name", "tenantId", "updateCheckedAt", "updateLatest"];
|
|
4838
|
+
var isDesired = (v) => v === "running" || v === "stopped";
|
|
4839
|
+
var FILE_MODE = 384;
|
|
4840
|
+
var DIR_MODE = 448;
|
|
4841
|
+
function stateDir(env) {
|
|
4842
|
+
return env.DAHRK_STATE_DIR ?? join12(homedir3(), ".dahrk");
|
|
4843
|
+
}
|
|
4844
|
+
function legacyStateDir(env) {
|
|
4845
|
+
return env.DAHRK_STATE_DIR ? void 0 : join12(homedir3(), ".skakel");
|
|
4846
|
+
}
|
|
4847
|
+
function stateFile(env) {
|
|
4848
|
+
return join12(stateDir(env), "node.json");
|
|
4849
|
+
}
|
|
4850
|
+
function logDir(env) {
|
|
4851
|
+
return join12(stateDir(env), "logs");
|
|
4852
|
+
}
|
|
4853
|
+
function logFiles(env) {
|
|
4854
|
+
const dir = logDir(env);
|
|
4855
|
+
return { out: join12(dir, "node.out.log"), err: join12(dir, "node.err.log") };
|
|
4856
|
+
}
|
|
4857
|
+
function jsonlLogFile(env) {
|
|
4858
|
+
return join12(logDir(env), "node.jsonl");
|
|
4859
|
+
}
|
|
4860
|
+
function crashDir(env) {
|
|
4861
|
+
return join12(logDir(env), "crashes");
|
|
4862
|
+
}
|
|
4863
|
+
function lockFile(env) {
|
|
4864
|
+
return join12(stateDir(env), "node.pid");
|
|
4865
|
+
}
|
|
4866
|
+
function setDesired(env, desired) {
|
|
4867
|
+
writeState(env, { desired });
|
|
4868
|
+
}
|
|
4869
|
+
function readState(file) {
|
|
4870
|
+
if (!existsSync10(file)) return {};
|
|
4871
|
+
try {
|
|
4872
|
+
const parsed = JSON.parse(readFileSync8(file, "utf8"));
|
|
4873
|
+
const state = {};
|
|
4874
|
+
for (const key of STRING_FIELDS) {
|
|
4875
|
+
const value = parsed[key];
|
|
4876
|
+
if (typeof value === "string" && value) state[key] = value;
|
|
4877
|
+
}
|
|
4878
|
+
if (isDesired(parsed["desired"])) state.desired = parsed["desired"];
|
|
4879
|
+
return state;
|
|
4880
|
+
} catch {
|
|
4881
|
+
return {};
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
function writeState(env, patch) {
|
|
4885
|
+
const dir = stateDir(env);
|
|
4886
|
+
const file = stateFile(env);
|
|
4887
|
+
try {
|
|
4888
|
+
mkdirSync10(dir, { recursive: true, mode: DIR_MODE });
|
|
4889
|
+
const next = { ...readState(file), ...patch };
|
|
4890
|
+
writeFileSync9(file, `${JSON.stringify(next, null, 2)}
|
|
4891
|
+
`, { mode: FILE_MODE });
|
|
4892
|
+
chmodSync(file, FILE_MODE);
|
|
4893
|
+
} catch (e) {
|
|
4894
|
+
console.warn(`could not persist node state to ${file}: ${e.message}`);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
function readPersistedToken(env) {
|
|
4898
|
+
return readState(stateFile(env)).enrolToken;
|
|
4899
|
+
}
|
|
4900
|
+
function persistEnrolment(env, enrolment) {
|
|
4901
|
+
const { token, name, tenantId } = enrolment;
|
|
4902
|
+
if (!token) return;
|
|
4903
|
+
const current = readState(stateFile(env));
|
|
4904
|
+
const unchanged = current.enrolToken === token && (name === void 0 || current.name === name) && (tenantId === void 0 || current.tenantId === tenantId);
|
|
4905
|
+
if (unchanged) return;
|
|
4906
|
+
writeState(env, {
|
|
4907
|
+
enrolToken: token,
|
|
4908
|
+
...name ? { name } : {},
|
|
4909
|
+
...tenantId ? { tenantId } : {}
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
function resolveEnrolToken(env, opts = {}) {
|
|
4913
|
+
if (env.DAHRK_ENROL_TOKEN) return env.DAHRK_ENROL_TOKEN;
|
|
4914
|
+
if (opts.ephemeral) return void 0;
|
|
4915
|
+
return readPersistedToken(env);
|
|
4916
|
+
}
|
|
4917
|
+
|
|
4918
|
+
// src/service.ts
|
|
3610
4919
|
var UNIT_FILE_MODE = 384;
|
|
3611
4920
|
var LAUNCHD_LABEL = "ai.dahrk.node";
|
|
3612
4921
|
var SYSTEMD_UNIT = "dahrk-node.service";
|
|
@@ -3615,9 +4924,13 @@ function detectManager(plat) {
|
|
|
3615
4924
|
if (plat === "linux") return "systemd";
|
|
3616
4925
|
return "unsupported";
|
|
3617
4926
|
}
|
|
4927
|
+
function serviceArgv(inputs) {
|
|
4928
|
+
return [inputs.nodeBin, inputs.scriptPath, "start", "--foreground"];
|
|
4929
|
+
}
|
|
3618
4930
|
function serviceEnv(inputs) {
|
|
3619
4931
|
return {
|
|
3620
4932
|
DAHRK_ENROL_TOKEN: inputs.token,
|
|
4933
|
+
DAHRK_SUPERVISED: "1",
|
|
3621
4934
|
...inputs.hubUrl ? { DAHRK_HUB_URL: inputs.hubUrl } : {},
|
|
3622
4935
|
...inputs.name ? { DAHRK_NODE_NAME: inputs.name } : {},
|
|
3623
4936
|
...inputs.pathEnv ? { PATH: inputs.pathEnv } : {}
|
|
@@ -3627,7 +4940,7 @@ function xmlEscape(s) {
|
|
|
3627
4940
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3628
4941
|
}
|
|
3629
4942
|
function renderLaunchdPlist(inputs) {
|
|
3630
|
-
const argv =
|
|
4943
|
+
const argv = serviceArgv(inputs);
|
|
3631
4944
|
const env = serviceEnv(inputs);
|
|
3632
4945
|
const progArgs = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
3633
4946
|
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
@@ -3655,9 +4968,9 @@ ${envEntries}
|
|
|
3655
4968
|
<key>ThrottleInterval</key>
|
|
3656
4969
|
<integer>10</integer>
|
|
3657
4970
|
<key>StandardOutPath</key>
|
|
3658
|
-
<string>${xmlEscape(
|
|
4971
|
+
<string>${xmlEscape(join13(inputs.logDir, "node.out.log"))}</string>
|
|
3659
4972
|
<key>StandardErrorPath</key>
|
|
3660
|
-
<string>${xmlEscape(
|
|
4973
|
+
<string>${xmlEscape(join13(inputs.logDir, "node.err.log"))}</string>
|
|
3661
4974
|
</dict>
|
|
3662
4975
|
</plist>
|
|
3663
4976
|
`;
|
|
@@ -3666,7 +4979,7 @@ function systemdEnvValue(v) {
|
|
|
3666
4979
|
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
3667
4980
|
}
|
|
3668
4981
|
function renderSystemdUnit(inputs) {
|
|
3669
|
-
const exec =
|
|
4982
|
+
const exec = serviceArgv(inputs).map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
3670
4983
|
const env = serviceEnv(inputs);
|
|
3671
4984
|
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`).join("\n");
|
|
3672
4985
|
return `[Unit]
|
|
@@ -3680,6 +4993,8 @@ Type=simple
|
|
|
3680
4993
|
ExecStart=${exec}
|
|
3681
4994
|
${envLines}
|
|
3682
4995
|
WorkingDirectory=${inputs.homeDir}
|
|
4996
|
+
StandardOutput=append:${join13(inputs.logDir, "node.out.log")}
|
|
4997
|
+
StandardError=append:${join13(inputs.logDir, "node.err.log")}
|
|
3683
4998
|
Restart=on-failure
|
|
3684
4999
|
RestartSec=3
|
|
3685
5000
|
RestartPreventExitStatus=78
|
|
@@ -3690,22 +5005,24 @@ WantedBy=default.target
|
|
|
3690
5005
|
}
|
|
3691
5006
|
function buildPlan(inputs) {
|
|
3692
5007
|
if (inputs.manager === "launchd") {
|
|
3693
|
-
const filePath2 =
|
|
5008
|
+
const filePath2 = join13(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
3694
5009
|
return {
|
|
3695
5010
|
manager: "launchd",
|
|
3696
5011
|
label: LAUNCHD_LABEL,
|
|
3697
5012
|
filePath: filePath2,
|
|
3698
5013
|
content: renderLaunchdPlist(inputs),
|
|
3699
|
-
// Unload first so a re-install picks up the rewritten plist; a not-loaded unload is a no-op.
|
|
5014
|
+
// Unload first so a re-install picks up the rewritten plist; a not-loaded unload is a no-op. The
|
|
5015
|
+
// `-w` on load also clears the "disabled" flag a previous `dahrk stop` set, so start-after-stop works.
|
|
3700
5016
|
installCommands: [
|
|
3701
5017
|
{ argv: ["launchctl", "unload", filePath2], ignoreFailure: true },
|
|
3702
5018
|
{ argv: ["launchctl", "load", "-w", filePath2] }
|
|
3703
5019
|
],
|
|
5020
|
+
stopCommands: [{ argv: ["launchctl", "unload", "-w", filePath2], ignoreFailure: true }],
|
|
3704
5021
|
uninstallCommands: [{ argv: ["launchctl", "unload", "-w", filePath2], ignoreFailure: true }],
|
|
3705
|
-
logHint:
|
|
5022
|
+
logHint: "dahrk logs -f"
|
|
3706
5023
|
};
|
|
3707
5024
|
}
|
|
3708
|
-
const filePath =
|
|
5025
|
+
const filePath = join13(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
3709
5026
|
const user = userInfo().username;
|
|
3710
5027
|
return {
|
|
3711
5028
|
manager: "systemd",
|
|
@@ -3719,15 +5036,23 @@ function buildPlan(inputs) {
|
|
|
3719
5036
|
{ argv: ["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT] },
|
|
3720
5037
|
{ argv: ["loginctl", "enable-linger", user], ignoreFailure: true }
|
|
3721
5038
|
],
|
|
5039
|
+
// `disable`, not `stop`: a stopped-but-enabled unit comes straight back at the next boot, which is not
|
|
5040
|
+
// what anyone means by `dahrk stop`. `dahrk start` re-enables it (`enable --now` above).
|
|
5041
|
+
stopCommands: [
|
|
5042
|
+
{ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], ignoreFailure: true }
|
|
5043
|
+
],
|
|
3722
5044
|
uninstallCommands: [
|
|
3723
5045
|
{ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], ignoreFailure: true },
|
|
3724
5046
|
{ argv: ["systemctl", "--user", "daemon-reload"], ignoreFailure: true }
|
|
3725
5047
|
],
|
|
3726
|
-
logHint:
|
|
5048
|
+
logHint: "dahrk logs -f"
|
|
3727
5049
|
};
|
|
3728
5050
|
}
|
|
5051
|
+
function unitIsCurrent(plan, onDisk) {
|
|
5052
|
+
return onDisk === plan.content;
|
|
5053
|
+
}
|
|
3729
5054
|
function unitPath(manager, homeDir) {
|
|
3730
|
-
return manager === "launchd" ?
|
|
5055
|
+
return manager === "launchd" ? join13(homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`) : join13(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
3731
5056
|
}
|
|
3732
5057
|
function statusCommand(manager) {
|
|
3733
5058
|
return manager === "launchd" ? ["launchctl", "list", LAUNCHD_LABEL] : ["systemctl", "--user", "show", SYSTEMD_UNIT, "-p", "ActiveState", "-p", "MainPID"];
|
|
@@ -3782,7 +5107,7 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
3782
5107
|
logDir: d.logDir
|
|
3783
5108
|
});
|
|
3784
5109
|
try {
|
|
3785
|
-
|
|
5110
|
+
d.mkdirp(d.logDir);
|
|
3786
5111
|
d.mkdirp(dirOf(plan.filePath));
|
|
3787
5112
|
d.writeFile(plan.filePath, plan.content);
|
|
3788
5113
|
} catch (e) {
|
|
@@ -3802,6 +5127,105 @@ async function runServiceInstall(inputs, deps = {}) {
|
|
|
3802
5127
|
d.out(" uninstall: dahrk service uninstall");
|
|
3803
5128
|
return 0;
|
|
3804
5129
|
}
|
|
5130
|
+
function availabilityCommand(manager) {
|
|
5131
|
+
return manager === "systemd" ? ["systemctl", "--user", "show", "-p", "Version"] : void 0;
|
|
5132
|
+
}
|
|
5133
|
+
async function runNodeStart(inputs, deps = {}) {
|
|
5134
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
5135
|
+
const manager = detectManager(d.platform);
|
|
5136
|
+
if (manager === "unsupported") {
|
|
5137
|
+
return { kind: "foreground", reason: "no supported supervisor on this platform (launchd / systemd)" };
|
|
5138
|
+
}
|
|
5139
|
+
const probeCmd = availabilityCommand(manager);
|
|
5140
|
+
if (probeCmd && d.capture(probeCmd).code !== 0) {
|
|
5141
|
+
return { kind: "foreground", reason: "no systemd user session on this host (a container, typically)" };
|
|
5142
|
+
}
|
|
5143
|
+
const filePath = unitPath(manager, d.homeDir);
|
|
5144
|
+
const unitExists = d.fileExists(filePath);
|
|
5145
|
+
if (!inputs.token && !unitExists) {
|
|
5146
|
+
d.out("No enrolment token: pass --token <token> or set DAHRK_ENROL_TOKEN.");
|
|
5147
|
+
d.out("Get one at https://app.dahrk.ai.");
|
|
5148
|
+
return { kind: "error", code: 2 };
|
|
5149
|
+
}
|
|
5150
|
+
const plan = buildPlan({
|
|
5151
|
+
manager,
|
|
5152
|
+
nodeBin: d.nodeBin,
|
|
5153
|
+
scriptPath: d.scriptPath,
|
|
5154
|
+
token: inputs.token ?? "-",
|
|
5155
|
+
...inputs.name ? { name: inputs.name } : {},
|
|
5156
|
+
...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
|
|
5157
|
+
...d.pathEnv ? { pathEnv: d.pathEnv } : {},
|
|
5158
|
+
homeDir: d.homeDir,
|
|
5159
|
+
logDir: d.logDir
|
|
5160
|
+
});
|
|
5161
|
+
const canRender = inputs.token !== void 0;
|
|
5162
|
+
const current = canRender && unitExists && unitIsCurrent(plan, d.readFile(filePath));
|
|
5163
|
+
const probe2 = unitExists ? d.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
5164
|
+
const status = parseServiceStatus(manager, unitExists, probe2);
|
|
5165
|
+
if (current && status.running) {
|
|
5166
|
+
d.out(`Node is already running${status.pid ? ` (pid ${status.pid})` : ""}.`);
|
|
5167
|
+
d.out(` logs: ${plan.logHint}`);
|
|
5168
|
+
return { kind: "running", code: 0 };
|
|
5169
|
+
}
|
|
5170
|
+
if (canRender && !current) {
|
|
5171
|
+
try {
|
|
5172
|
+
d.mkdirp(d.logDir);
|
|
5173
|
+
d.mkdirp(dirOf(plan.filePath));
|
|
5174
|
+
d.writeFile(plan.filePath, plan.content);
|
|
5175
|
+
} catch (e) {
|
|
5176
|
+
d.out(`Could not write the service file at ${plan.filePath}: ${e.message}`);
|
|
5177
|
+
return { kind: "error", code: 1 };
|
|
5178
|
+
}
|
|
5179
|
+
d.out(unitExists ? `Updated ${plan.manager} service: ${plan.filePath}` : `Installed ${plan.manager} service: ${plan.filePath}`);
|
|
5180
|
+
}
|
|
5181
|
+
const code = runCommands(plan.installCommands, d);
|
|
5182
|
+
if (code !== 0) {
|
|
5183
|
+
d.out(`The service file is in place but starting it failed (exit ${code}).`);
|
|
5184
|
+
return { kind: "error", code };
|
|
5185
|
+
}
|
|
5186
|
+
d.out("Node is running. It will start on boot and restart on failure.");
|
|
5187
|
+
d.out(` logs: ${plan.logHint}`);
|
|
5188
|
+
d.out(" stop: dahrk stop");
|
|
5189
|
+
return { kind: "running", code: 0 };
|
|
5190
|
+
}
|
|
5191
|
+
var STOP_FOREIGN_NODE = 3;
|
|
5192
|
+
function foreignNodePid(lock, servicePid, isAlive2) {
|
|
5193
|
+
const held = parseLock(lock);
|
|
5194
|
+
if (held === void 0 || held === servicePid) return void 0;
|
|
5195
|
+
return isAlive2(held) ? held : void 0;
|
|
5196
|
+
}
|
|
5197
|
+
async function runNodeStop(deps = {}) {
|
|
5198
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
5199
|
+
const manager = detectManager(d.platform);
|
|
5200
|
+
if (manager === "unsupported") return printUnsupported(d.out);
|
|
5201
|
+
const plan = buildPlan({
|
|
5202
|
+
manager,
|
|
5203
|
+
nodeBin: d.nodeBin,
|
|
5204
|
+
scriptPath: d.scriptPath,
|
|
5205
|
+
token: "-",
|
|
5206
|
+
homeDir: d.homeDir,
|
|
5207
|
+
logDir: d.logDir
|
|
5208
|
+
});
|
|
5209
|
+
if (!d.fileExists(plan.filePath)) {
|
|
5210
|
+
d.out("No service installed, so there is nothing to stop.");
|
|
5211
|
+
d.out("If you are running a node in a terminal (`dahrk start --foreground`), stop it with Ctrl-C.");
|
|
5212
|
+
return reportForeignNode(d, void 0) ?? 0;
|
|
5213
|
+
}
|
|
5214
|
+
const servicePid = parseServiceStatus(manager, true, d.capture(statusCommand(manager))).pid;
|
|
5215
|
+
runCommands(plan.stopCommands, d);
|
|
5216
|
+
d.out("Node stopped. It will stay stopped across reboots until you run `dahrk start`.");
|
|
5217
|
+
return reportForeignNode(d, servicePid) ?? 0;
|
|
5218
|
+
}
|
|
5219
|
+
function reportForeignNode(d, servicePid) {
|
|
5220
|
+
const pid = foreignNodePid(d.readFile(d.lockFile), servicePid, d.isAlive);
|
|
5221
|
+
if (pid === void 0) return void 0;
|
|
5222
|
+
d.out("");
|
|
5223
|
+
d.out(`WARNING: a node is STILL RUNNING on this host (pid ${pid}). \`dahrk stop\` cannot stop it.`);
|
|
5224
|
+
d.out("Something else is supervising it: pm2, a container, or `dahrk start --foreground` in a terminal.");
|
|
5225
|
+
d.out("It is not a stray copy - it holds this node's identity, so it is still connected to the hub and");
|
|
5226
|
+
d.out("still taking Jobs. Stop it where it was started (e.g. `pm2 delete dahrk-node`), or kill the pid.");
|
|
5227
|
+
return STOP_FOREIGN_NODE;
|
|
5228
|
+
}
|
|
3805
5229
|
async function runServiceUninstall(deps = {}) {
|
|
3806
5230
|
const d = { ...defaultDeps3(), ...deps };
|
|
3807
5231
|
d.out("dahrk service uninstall");
|
|
@@ -3837,7 +5261,7 @@ function dirOf(path) {
|
|
|
3837
5261
|
function resolveScriptPath() {
|
|
3838
5262
|
const argv1 = process.argv[1] ?? "";
|
|
3839
5263
|
try {
|
|
3840
|
-
return
|
|
5264
|
+
return realpathSync2(argv1);
|
|
3841
5265
|
} catch {
|
|
3842
5266
|
return argv1;
|
|
3843
5267
|
}
|
|
@@ -3851,49 +5275,71 @@ function stableNodeBin(execPath, realpath) {
|
|
|
3851
5275
|
}
|
|
3852
5276
|
var realpathOrUndefined = (p) => {
|
|
3853
5277
|
try {
|
|
3854
|
-
return
|
|
5278
|
+
return realpathSync2(p);
|
|
3855
5279
|
} catch {
|
|
3856
5280
|
return void 0;
|
|
3857
5281
|
}
|
|
3858
5282
|
};
|
|
3859
5283
|
var defaultDeps3 = () => ({
|
|
3860
5284
|
platform: osPlatform3(),
|
|
3861
|
-
homeDir:
|
|
5285
|
+
homeDir: homedir4(),
|
|
3862
5286
|
// Not `process.execPath` raw: that is the versioned Homebrew Cellar path, which the next
|
|
3863
5287
|
// `brew upgrade node` deletes out from under the unit. See `stableNodeBin`.
|
|
3864
5288
|
nodeBin: stableNodeBin(process.execPath, realpathOrUndefined),
|
|
3865
5289
|
scriptPath: resolveScriptPath(),
|
|
3866
|
-
logDir:
|
|
5290
|
+
logDir: logDir(process.env),
|
|
5291
|
+
lockFile: lockFile(process.env),
|
|
5292
|
+
isAlive,
|
|
3867
5293
|
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
3868
5294
|
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
3869
5295
|
pathEnv: process.env.PATH,
|
|
3870
|
-
mkdirp: (dir) => void
|
|
5296
|
+
mkdirp: (dir) => void mkdirSync11(dir, { recursive: true }),
|
|
3871
5297
|
// The unit's environment block carries the enrolment token, so the file is a secret: write it
|
|
3872
5298
|
// owner-only. `writeFileSync`'s `mode` applies only when it CREATES the file, so chmod explicitly
|
|
3873
5299
|
// too - re-installing over a unit an older client left at 0644 must tighten it, not keep it.
|
|
3874
5300
|
writeFile: (path, content) => {
|
|
3875
|
-
|
|
3876
|
-
|
|
5301
|
+
writeFileSync10(path, content, { mode: UNIT_FILE_MODE });
|
|
5302
|
+
chmodSync2(path, UNIT_FILE_MODE);
|
|
3877
5303
|
},
|
|
3878
|
-
|
|
3879
|
-
|
|
5304
|
+
readFile: (path) => {
|
|
5305
|
+
try {
|
|
5306
|
+
return readFileSync9(path, "utf8");
|
|
5307
|
+
} catch {
|
|
5308
|
+
return void 0;
|
|
5309
|
+
}
|
|
5310
|
+
},
|
|
5311
|
+
removeFile: (path) => rmSync7(path, { force: true }),
|
|
5312
|
+
fileExists: (path) => existsSync11(path),
|
|
3880
5313
|
run: (argv) => {
|
|
3881
5314
|
const [cmd, ...args] = argv;
|
|
3882
5315
|
try {
|
|
3883
|
-
|
|
5316
|
+
execFileSync6(cmd, args, { stdio: "inherit" });
|
|
3884
5317
|
return 0;
|
|
3885
5318
|
} catch (e) {
|
|
3886
5319
|
const status = e.status;
|
|
3887
5320
|
return typeof status === "number" ? status : 1;
|
|
3888
5321
|
}
|
|
3889
5322
|
},
|
|
5323
|
+
capture: (argv) => {
|
|
5324
|
+
const [cmd, ...args] = argv;
|
|
5325
|
+
try {
|
|
5326
|
+
const stdout = execFileSync6(cmd, args, {
|
|
5327
|
+
encoding: "utf8",
|
|
5328
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5329
|
+
});
|
|
5330
|
+
return { code: 0, stdout };
|
|
5331
|
+
} catch (e) {
|
|
5332
|
+
const status = e.status;
|
|
5333
|
+
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
5334
|
+
}
|
|
5335
|
+
},
|
|
3890
5336
|
out: (line) => void process.stdout.write(`${line}
|
|
3891
5337
|
`)
|
|
3892
5338
|
});
|
|
3893
5339
|
|
|
3894
5340
|
// src/update.ts
|
|
3895
|
-
import { execFileSync as
|
|
3896
|
-
import { realpathSync as
|
|
5341
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
5342
|
+
import { realpathSync as realpathSync3 } from "fs";
|
|
3897
5343
|
var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
|
|
3898
5344
|
var CHANNEL_COMMANDS = {
|
|
3899
5345
|
npm: "npm install -g dahrk-node@latest",
|
|
@@ -3914,7 +5360,7 @@ function detectChannel(binPath) {
|
|
|
3914
5360
|
if (!binPath) return "unknown";
|
|
3915
5361
|
let resolved = binPath;
|
|
3916
5362
|
try {
|
|
3917
|
-
resolved =
|
|
5363
|
+
resolved = realpathSync3(binPath);
|
|
3918
5364
|
} catch {
|
|
3919
5365
|
}
|
|
3920
5366
|
if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
|
|
@@ -3989,8 +5435,11 @@ async function runUpdate(inputs, deps = {}) {
|
|
|
3989
5435
|
}
|
|
3990
5436
|
return code;
|
|
3991
5437
|
}
|
|
3992
|
-
async function fetchLatestVersion() {
|
|
3993
|
-
const res = await fetch(LATEST_URL, {
|
|
5438
|
+
async function fetchLatestVersion(signal) {
|
|
5439
|
+
const res = await fetch(LATEST_URL, {
|
|
5440
|
+
headers: { accept: "application/json" },
|
|
5441
|
+
...signal ? { signal } : {}
|
|
5442
|
+
});
|
|
3994
5443
|
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
3995
5444
|
const body = await res.json();
|
|
3996
5445
|
if (typeof body.version !== "string" || !body.version) throw new Error("registry returned no version");
|
|
@@ -3999,7 +5448,7 @@ async function fetchLatestVersion() {
|
|
|
3999
5448
|
function spawnUpgrade(argv) {
|
|
4000
5449
|
const [cmd, ...args] = argv;
|
|
4001
5450
|
try {
|
|
4002
|
-
|
|
5451
|
+
execFileSync7(cmd, args, { stdio: "inherit" });
|
|
4003
5452
|
return 0;
|
|
4004
5453
|
} catch (e) {
|
|
4005
5454
|
const status = e.status;
|
|
@@ -4014,68 +5463,56 @@ var defaultDeps4 = () => ({
|
|
|
4014
5463
|
`)
|
|
4015
5464
|
});
|
|
4016
5465
|
|
|
4017
|
-
// src/
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
5466
|
+
// src/update-check.ts
|
|
5467
|
+
var DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
5468
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
5469
|
+
function checkSuppressed(env) {
|
|
5470
|
+
return Boolean(env.DAHRK_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI);
|
|
5471
|
+
}
|
|
5472
|
+
function checkIntervalMs(env) {
|
|
5473
|
+
const raw = env.DAHRK_UPDATE_CHECK_INTERVAL_MS;
|
|
5474
|
+
if (raw === void 0) return DEFAULT_INTERVAL_MS;
|
|
5475
|
+
const n = Number(raw);
|
|
5476
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_INTERVAL_MS;
|
|
5477
|
+
}
|
|
5478
|
+
function shouldCheck(now, checkedAt, intervalMs, env) {
|
|
5479
|
+
if (checkSuppressed(env)) return false;
|
|
5480
|
+
if (!checkedAt) return true;
|
|
5481
|
+
const last = Date.parse(checkedAt);
|
|
5482
|
+
if (!Number.isFinite(last)) return true;
|
|
5483
|
+
return now - last >= intervalMs || last > now;
|
|
5484
|
+
}
|
|
5485
|
+
function jitterMs(random, spreadMs = 60 * 60 * 1e3) {
|
|
5486
|
+
return Math.floor(random * spreadMs);
|
|
5487
|
+
}
|
|
5488
|
+
function renderUpdateNotice(u) {
|
|
5489
|
+
const cmd = upgradeCommand(u.channel);
|
|
5490
|
+
const how = cmd ? ` (${u.channel}: ${cmd.display})` : " (run `dahrk update`)";
|
|
5491
|
+
return `update available: ${u.current} -> ${u.latest}${how}`;
|
|
5492
|
+
}
|
|
5493
|
+
function renderUpdateLogLine(u) {
|
|
5494
|
+
return `UPDATE_AVAILABLE:${u.latest} current=${u.current}`;
|
|
5495
|
+
}
|
|
5496
|
+
async function checkForUpdate(currentVersion, deps) {
|
|
5497
|
+
if (checkSuppressed(deps.env)) return void 0;
|
|
5498
|
+
const state = deps.readState();
|
|
5499
|
+
if (!shouldCheck(deps.now(), state.updateCheckedAt, checkIntervalMs(deps.env), deps.env)) {
|
|
5500
|
+
return cachedUpdate(state, currentVersion, deps.binPath);
|
|
4045
5501
|
}
|
|
4046
|
-
|
|
4047
|
-
function writeState(env, patch) {
|
|
4048
|
-
const dir = stateDir(env);
|
|
4049
|
-
const file = stateFile(env);
|
|
5502
|
+
let latest;
|
|
4050
5503
|
try {
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
`, { mode: FILE_MODE });
|
|
4055
|
-
chmodSync2(file, FILE_MODE);
|
|
4056
|
-
} catch (e) {
|
|
4057
|
-
console.warn(`could not persist node state to ${file}: ${e.message}`);
|
|
5504
|
+
latest = await deps.fetchLatest(AbortSignal.timeout(FETCH_TIMEOUT_MS));
|
|
5505
|
+
} catch {
|
|
5506
|
+
return void 0;
|
|
4058
5507
|
}
|
|
5508
|
+
deps.saveResult({ updateCheckedAt: new Date(deps.now()).toISOString(), updateLatest: latest });
|
|
5509
|
+
if (!isNewer(latest, currentVersion)) return void 0;
|
|
5510
|
+
return { current: currentVersion, latest, channel: detectChannel(deps.binPath) };
|
|
4059
5511
|
}
|
|
4060
|
-
function
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
const { token, name, tenantId } = enrolment;
|
|
4065
|
-
if (!token) return;
|
|
4066
|
-
const current = readState(stateFile(env));
|
|
4067
|
-
const unchanged = current.enrolToken === token && (name === void 0 || current.name === name) && (tenantId === void 0 || current.tenantId === tenantId);
|
|
4068
|
-
if (unchanged) return;
|
|
4069
|
-
writeState(env, {
|
|
4070
|
-
enrolToken: token,
|
|
4071
|
-
...name ? { name } : {},
|
|
4072
|
-
...tenantId ? { tenantId } : {}
|
|
4073
|
-
});
|
|
4074
|
-
}
|
|
4075
|
-
function resolveEnrolToken(env, opts = {}) {
|
|
4076
|
-
if (env.DAHRK_ENROL_TOKEN) return env.DAHRK_ENROL_TOKEN;
|
|
4077
|
-
if (opts.ephemeral) return void 0;
|
|
4078
|
-
return readPersistedToken(env);
|
|
5512
|
+
function cachedUpdate(state, currentVersion, binPath) {
|
|
5513
|
+
const latest = state.updateLatest;
|
|
5514
|
+
if (!latest || !isNewer(latest, currentVersion)) return void 0;
|
|
5515
|
+
return { current: currentVersion, latest, channel: detectChannel(binPath) };
|
|
4079
5516
|
}
|
|
4080
5517
|
|
|
4081
5518
|
// src/status.ts
|
|
@@ -4092,7 +5529,12 @@ function renderStatus(f) {
|
|
|
4092
5529
|
lines.push(bullet("Enrolled", "no - run `dahrk start --token <token>` once to enrol"));
|
|
4093
5530
|
}
|
|
4094
5531
|
lines.push(bullet("Node id", nodeId ?? "not yet minted (first `dahrk start` mints one)"));
|
|
4095
|
-
lines.push(
|
|
5532
|
+
lines.push(
|
|
5533
|
+
bullet(
|
|
5534
|
+
"Client",
|
|
5535
|
+
f.update ? `${f.clientVersion} (update available: ${f.update.latest} - run \`dahrk update\`)` : f.clientVersion
|
|
5536
|
+
)
|
|
5537
|
+
);
|
|
4096
5538
|
lines.push(bullet("Hub", f.hubUrl));
|
|
4097
5539
|
lines.push(
|
|
4098
5540
|
bullet(
|
|
@@ -4101,19 +5543,24 @@ function renderStatus(f) {
|
|
|
4101
5543
|
)
|
|
4102
5544
|
);
|
|
4103
5545
|
if (!f.service) {
|
|
4104
|
-
lines.push(bullet("
|
|
5546
|
+
lines.push(bullet("Node", "no supervisor on this host - run `dahrk start --foreground` (or under pm2)"));
|
|
4105
5547
|
} else if (!f.service.installed) {
|
|
4106
|
-
lines.push(bullet("
|
|
5548
|
+
lines.push(bullet("Node", "not installed - run `dahrk start` to run it always-on"));
|
|
4107
5549
|
} else if (f.service.running) {
|
|
4108
5550
|
const pid = f.service.pid ? ` (pid ${f.service.pid})` : "";
|
|
4109
|
-
lines.push(bullet("
|
|
5551
|
+
lines.push(bullet("Node", `running${pid}`));
|
|
5552
|
+
} else if (f.state.desired === "stopped") {
|
|
5553
|
+
lines.push(bullet("Node", "stopped - run `dahrk start` to bring it back"));
|
|
4110
5554
|
} else {
|
|
4111
|
-
lines.push(bullet("
|
|
5555
|
+
lines.push(bullet("Node", "INSTALLED BUT NOT RUNNING - it is failing to start or crash-looping"));
|
|
4112
5556
|
if (f.logHint) lines.push(bullet("", `check the logs: ${f.logHint}`));
|
|
4113
5557
|
}
|
|
4114
5558
|
lines.push("", `State file: ${f.stateFile}`);
|
|
4115
5559
|
return lines;
|
|
4116
5560
|
}
|
|
5561
|
+
function isUnhealthy(f) {
|
|
5562
|
+
return Boolean(f.service?.installed && !f.service.running && f.state.desired !== "stopped");
|
|
5563
|
+
}
|
|
4117
5564
|
async function runStatus(inputs, deps) {
|
|
4118
5565
|
const manager = detectManager(deps.platform);
|
|
4119
5566
|
let service;
|
|
@@ -4123,24 +5570,27 @@ async function runStatus(inputs, deps) {
|
|
|
4123
5570
|
const exists = deps.fileExists(unit);
|
|
4124
5571
|
const probe2 = exists ? deps.capture(statusCommand(manager)) : { code: 1, stdout: "" };
|
|
4125
5572
|
service = parseServiceStatus(manager, exists, probe2);
|
|
4126
|
-
logHint =
|
|
5573
|
+
logHint = "dahrk logs -f";
|
|
4127
5574
|
}
|
|
5575
|
+
const state = readState(stateFile(deps.env));
|
|
5576
|
+
const update = checkSuppressed(deps.env) ? void 0 : cachedUpdate(state, inputs.clientVersion, deps.binPath);
|
|
4128
5577
|
const facts = {
|
|
4129
5578
|
clientVersion: inputs.clientVersion,
|
|
4130
5579
|
hubUrl: inputs.hubUrl,
|
|
4131
5580
|
stateFile: stateFile(deps.env),
|
|
4132
|
-
state
|
|
5581
|
+
state,
|
|
4133
5582
|
envToken: Boolean(deps.env.DAHRK_ENROL_TOKEN),
|
|
4134
5583
|
runtimes: await deps.detectRuntimes(),
|
|
4135
5584
|
...service ? { service } : {},
|
|
4136
|
-
...logHint ? { logHint } : {}
|
|
5585
|
+
...logHint ? { logHint } : {},
|
|
5586
|
+
...update ? { update } : {}
|
|
4137
5587
|
};
|
|
4138
5588
|
for (const line of renderStatus(facts)) deps.out(line);
|
|
4139
|
-
return
|
|
5589
|
+
return isUnhealthy(facts) ? 1 : 0;
|
|
4140
5590
|
}
|
|
4141
5591
|
|
|
4142
5592
|
// src/main.ts
|
|
4143
|
-
var CLIENT_VERSION = "0.1.
|
|
5593
|
+
var CLIENT_VERSION = "0.1.10";
|
|
4144
5594
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
4145
5595
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
4146
5596
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
@@ -4152,7 +5602,7 @@ function resolveNodeId(env, opts = {}) {
|
|
|
4152
5602
|
if (existing) return existing;
|
|
4153
5603
|
const legacy = legacyStateDir(env);
|
|
4154
5604
|
if (legacy) {
|
|
4155
|
-
const legacyId = readState(
|
|
5605
|
+
const legacyId = readState(join14(legacy, "node.json")).nodeId;
|
|
4156
5606
|
if (legacyId) return legacyId;
|
|
4157
5607
|
}
|
|
4158
5608
|
const nodeId = randomUUID3();
|
|
@@ -4219,9 +5669,49 @@ function envWithFlags(env, flags) {
|
|
|
4219
5669
|
if (flags.hubUrl) merged.DAHRK_HUB_URL = flags.hubUrl;
|
|
4220
5670
|
return merged;
|
|
4221
5671
|
}
|
|
5672
|
+
function wantsForeground(env, flags) {
|
|
5673
|
+
return flags.foreground || env.DAHRK_FOREGROUND === "1" || env.DAHRK_SUPERVISED === "1";
|
|
5674
|
+
}
|
|
4222
5675
|
async function start(flags) {
|
|
4223
5676
|
const env = envWithFlags(process.env, flags);
|
|
5677
|
+
if (wantsForeground(env, flags)) return startForeground(env, flags);
|
|
5678
|
+
await offerUpdate(env);
|
|
5679
|
+
const outcome = await runNodeStart({
|
|
5680
|
+
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
5681
|
+
...env.DAHRK_NODE_NAME ? { name: env.DAHRK_NODE_NAME } : {},
|
|
5682
|
+
...env.DAHRK_HUB_URL ? { hubUrl: env.DAHRK_HUB_URL } : {}
|
|
5683
|
+
});
|
|
5684
|
+
if (outcome.kind === "error") return outcome.code;
|
|
5685
|
+
if (outcome.kind === "running") {
|
|
5686
|
+
setDesired(env, "running");
|
|
5687
|
+
return 0;
|
|
5688
|
+
}
|
|
5689
|
+
console.warn(`${outcome.reason}; running the node in this terminal instead.`);
|
|
5690
|
+
console.warn("Use `dahrk start --foreground` (or DAHRK_FOREGROUND=1) to ask for this explicitly.");
|
|
5691
|
+
return startForeground(env, flags);
|
|
5692
|
+
}
|
|
5693
|
+
async function startForeground(env, flags) {
|
|
5694
|
+
if (!flags.ephemeral) {
|
|
5695
|
+
const lock = acquireLock(defaultLockDeps(lockFile(env)));
|
|
5696
|
+
if (!lock.ok) {
|
|
5697
|
+
console.error(`A node is already running on this host (pid ${lock.heldBy}).`);
|
|
5698
|
+
console.error("Follow it with `dahrk logs -f`, or stop it with `dahrk stop` first.");
|
|
5699
|
+
return 1;
|
|
5700
|
+
}
|
|
5701
|
+
process.on("exit", lock.release);
|
|
5702
|
+
}
|
|
5703
|
+
const files = logFiles(env);
|
|
5704
|
+
rotateIfLarge(files.out);
|
|
5705
|
+
rotateIfLarge(files.err);
|
|
4224
5706
|
const nodeId = resolveNodeId(env, { ephemeral: flags.ephemeral });
|
|
5707
|
+
const shipper = new LogShipper({ ceiling: ceilingFromEnv(env) });
|
|
5708
|
+
const logger = createNodeLoggerFromEnv(env, logDir(env), { nodeId, clientVersion: CLIENT_VERSION }, shipperStream(shipper));
|
|
5709
|
+
installProcessSafetyNet({
|
|
5710
|
+
logger,
|
|
5711
|
+
crashDir: crashDir(env),
|
|
5712
|
+
clientVersion: CLIENT_VERSION,
|
|
5713
|
+
...env.DAHRK_CRASH_EXIT === "1" ? { exitOnCrash: true } : {}
|
|
5714
|
+
});
|
|
4225
5715
|
const token = resolveEnrolToken(env, { ephemeral: flags.ephemeral });
|
|
4226
5716
|
if (token) env.DAHRK_ENROL_TOKEN = token;
|
|
4227
5717
|
const runtimes = await resolveRuntimes(env);
|
|
@@ -4230,26 +5720,78 @@ async function start(flags) {
|
|
|
4230
5720
|
"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."
|
|
4231
5721
|
);
|
|
4232
5722
|
}
|
|
5723
|
+
if (!flags.ephemeral) setDesired(env, "running");
|
|
5724
|
+
scheduleUpdateChecks(env);
|
|
4233
5725
|
const resolved = { nodeId, runtimes, clientVersion: CLIENT_VERSION };
|
|
4234
5726
|
const persist = token !== void 0 && !flags.ephemeral;
|
|
4235
5727
|
await startEdgeNode({
|
|
4236
5728
|
...buildEdgeOptions(env, resolved),
|
|
5729
|
+
logger,
|
|
5730
|
+
shipper,
|
|
4237
5731
|
...persist ? {
|
|
4238
5732
|
onEnrolled: (welcome) => persistEnrolment(env, { token, name: welcome.name, tenantId: welcome.tenantId })
|
|
4239
5733
|
} : {}
|
|
4240
5734
|
});
|
|
5735
|
+
return 0;
|
|
5736
|
+
}
|
|
5737
|
+
async function stop(env) {
|
|
5738
|
+
const code = await runNodeStop();
|
|
5739
|
+
if (code === 0 || code === STOP_FOREIGN_NODE) setDesired(env, "stopped");
|
|
5740
|
+
return code;
|
|
5741
|
+
}
|
|
5742
|
+
function updateCheckDeps(env) {
|
|
5743
|
+
return {
|
|
5744
|
+
env,
|
|
5745
|
+
now: () => Date.now(),
|
|
5746
|
+
binPath: process.argv[1],
|
|
5747
|
+
readState: () => readState(stateFile(env)),
|
|
5748
|
+
saveResult: (patch) => writeState(env, patch),
|
|
5749
|
+
fetchLatest: fetchLatestVersion
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
5753
|
+
async function confirm(question) {
|
|
5754
|
+
const { createInterface } = await import("readline/promises");
|
|
5755
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5756
|
+
try {
|
|
5757
|
+
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
5758
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
5759
|
+
} finally {
|
|
5760
|
+
rl.close();
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
async function offerUpdate(env) {
|
|
5764
|
+
const available = await checkForUpdate(CLIENT_VERSION, updateCheckDeps(env));
|
|
5765
|
+
if (!available) return;
|
|
5766
|
+
console.log(renderUpdateNotice(available));
|
|
5767
|
+
if (!isInteractive()) return;
|
|
5768
|
+
if (!await confirm("update now?")) return;
|
|
5769
|
+
const code = await runUpdate({ currentVersion: CLIENT_VERSION, check: false });
|
|
5770
|
+
if (code !== 0) console.warn("Update failed; starting the node on the current version anyway.");
|
|
5771
|
+
}
|
|
5772
|
+
function scheduleUpdateChecks(env) {
|
|
5773
|
+
if (checkSuppressed(env)) return;
|
|
5774
|
+
const deps = updateCheckDeps(env);
|
|
5775
|
+
const tick = async () => {
|
|
5776
|
+
const available = await checkForUpdate(CLIENT_VERSION, deps);
|
|
5777
|
+
if (available) process.stdout.write(`${renderUpdateLogLine(available)}
|
|
5778
|
+
`);
|
|
5779
|
+
};
|
|
5780
|
+
setTimeout(() => void tick(), jitterMs(Math.random())).unref();
|
|
5781
|
+
setInterval(() => void tick(), checkIntervalMs(env)).unref();
|
|
4241
5782
|
}
|
|
4242
5783
|
function statusDeps(env) {
|
|
4243
5784
|
return {
|
|
4244
5785
|
platform: osPlatform4(),
|
|
4245
5786
|
homeDir: homedir5(),
|
|
4246
5787
|
env,
|
|
5788
|
+
binPath: process.argv[1],
|
|
4247
5789
|
detectRuntimes: () => resolveRuntimes(env),
|
|
4248
|
-
fileExists: (path) =>
|
|
5790
|
+
fileExists: (path) => existsSync12(path),
|
|
4249
5791
|
capture: (argv) => {
|
|
4250
5792
|
const [cmd, ...args] = argv;
|
|
4251
5793
|
try {
|
|
4252
|
-
const stdout =
|
|
5794
|
+
const stdout = execFileSync8(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
4253
5795
|
return { code: 0, stdout };
|
|
4254
5796
|
} catch (e) {
|
|
4255
5797
|
const status = e.status;
|
|
@@ -4279,7 +5821,7 @@ async function runWorkflow(flags) {
|
|
|
4279
5821
|
});
|
|
4280
5822
|
}
|
|
4281
5823
|
async function main() {
|
|
4282
|
-
const invoked =
|
|
5824
|
+
const invoked = basename2(process.argv[1] ?? "");
|
|
4283
5825
|
const bin = !invoked || invoked.startsWith("main.") ? "dahrk" : invoked;
|
|
4284
5826
|
const parsed = parseCli(process.argv.slice(2));
|
|
4285
5827
|
switch (parsed.kind) {
|
|
@@ -4337,22 +5879,84 @@ async function main() {
|
|
|
4337
5879
|
process.exitCode = await runUpdate({ currentVersion: CLIENT_VERSION, check: parsed.flags.check });
|
|
4338
5880
|
break;
|
|
4339
5881
|
case "start":
|
|
4340
|
-
await start(parsed.flags);
|
|
5882
|
+
process.exitCode = await start(parsed.flags);
|
|
5883
|
+
break;
|
|
5884
|
+
case "stop":
|
|
5885
|
+
process.exitCode = await stop(applyEnvAliases(process.env));
|
|
5886
|
+
break;
|
|
5887
|
+
case "restart": {
|
|
5888
|
+
const env = envWithFlags(process.env, parsed.flags);
|
|
5889
|
+
const code = await stop(env);
|
|
5890
|
+
if (code !== 0) {
|
|
5891
|
+
process.exitCode = code;
|
|
5892
|
+
break;
|
|
5893
|
+
}
|
|
5894
|
+
process.exitCode = await start(parsed.flags);
|
|
5895
|
+
break;
|
|
5896
|
+
}
|
|
5897
|
+
case "logs": {
|
|
5898
|
+
const env = applyEnvAliases(process.env);
|
|
5899
|
+
process.exitCode = await runLogs(parsed.flags, defaultLogsDeps(logFiles(env), jsonlLogFile(env)));
|
|
5900
|
+
break;
|
|
5901
|
+
}
|
|
5902
|
+
case "diagnose": {
|
|
5903
|
+
const env = applyEnvAliases(process.env);
|
|
5904
|
+
const doctorLines = [];
|
|
5905
|
+
const doctor = async () => {
|
|
5906
|
+
await runDoctor(
|
|
5907
|
+
{
|
|
5908
|
+
...resolveEnrolToken(env) ? { token: resolveEnrolToken(env) } : {},
|
|
5909
|
+
hubUrl: env.DAHRK_HUB_URL ?? DEFAULT_HUB_URL
|
|
5910
|
+
},
|
|
5911
|
+
{ out: (line) => void doctorLines.push(line) }
|
|
5912
|
+
);
|
|
5913
|
+
return doctorLines;
|
|
5914
|
+
};
|
|
5915
|
+
process.exitCode = await runDiagnose(
|
|
5916
|
+
defaultDiagnoseDeps(
|
|
5917
|
+
{
|
|
5918
|
+
stateFile: stateFile(env),
|
|
5919
|
+
jsonlFile: jsonlLogFile(env),
|
|
5920
|
+
crashDir: crashDir(env),
|
|
5921
|
+
outFile: parsed.flags.out ?? defaultBundlePath(process.cwd(), /* @__PURE__ */ new Date())
|
|
5922
|
+
},
|
|
5923
|
+
CLIENT_VERSION,
|
|
5924
|
+
doctor
|
|
5925
|
+
)
|
|
5926
|
+
);
|
|
4341
5927
|
break;
|
|
5928
|
+
}
|
|
4342
5929
|
}
|
|
4343
5930
|
}
|
|
4344
5931
|
var invokedAsEntrypoint = (() => {
|
|
4345
5932
|
const argv1 = process.argv[1];
|
|
4346
5933
|
if (!argv1) return false;
|
|
4347
5934
|
try {
|
|
4348
|
-
return pathToFileURL(
|
|
5935
|
+
return pathToFileURL(realpathSync4(argv1)).href === import.meta.url;
|
|
4349
5936
|
} catch {
|
|
4350
5937
|
return false;
|
|
4351
5938
|
}
|
|
4352
5939
|
})();
|
|
4353
5940
|
if (invokedAsEntrypoint) {
|
|
4354
5941
|
main().catch((err) => {
|
|
5942
|
+
const enrolmentRejected = process.exitCode === ENROLMENT_REJECTED_EXIT_CODE;
|
|
4355
5943
|
console.error(err instanceof Error ? err.message : String(err));
|
|
5944
|
+
if (!enrolmentRejected) {
|
|
5945
|
+
if (err instanceof Error && err.stack) console.error(err.stack);
|
|
5946
|
+
const env = applyEnvAliases(process.env);
|
|
5947
|
+
writeCrashRecord(crashDir(env), {
|
|
5948
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5949
|
+
kind: "uncaughtException",
|
|
5950
|
+
name: err instanceof Error ? err.name : typeof err,
|
|
5951
|
+
message: err instanceof Error ? err.message : String(err),
|
|
5952
|
+
...err instanceof Error && err.stack ? { stack: err.stack } : {},
|
|
5953
|
+
clientVersion: CLIENT_VERSION,
|
|
5954
|
+
nodeVersion: process.version,
|
|
5955
|
+
platform: process.platform,
|
|
5956
|
+
arch: process.arch,
|
|
5957
|
+
uptimeSec: Math.round(process.uptime())
|
|
5958
|
+
});
|
|
5959
|
+
}
|
|
4356
5960
|
process.exit(process.exitCode || 1);
|
|
4357
5961
|
});
|
|
4358
5962
|
}
|