@inerrata-corporation/errata 2.0.6-dev.2008 → 2.0.6-dev.2028
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/errata.mjs +1796 -1758
- package/package.json +1 -1
package/errata.mjs
CHANGED
|
@@ -77008,7 +77008,7 @@ function createWatchBreaker(deps) {
|
|
|
77008
77008
|
}
|
|
77009
77009
|
|
|
77010
77010
|
// src/engine.ts
|
|
77011
|
-
var DAEMON_VERSION = true ? "2.0.6-dev.
|
|
77011
|
+
var DAEMON_VERSION = true ? "2.0.6-dev.2028" : "2.0.0-alpha.0";
|
|
77012
77012
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
77013
77013
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
77014
77014
|
var GIT_OP_MUTE_MS = 4e3;
|
|
@@ -79337,7 +79337,7 @@ init_identity2();
|
|
|
79337
79337
|
// src/multi.ts
|
|
79338
79338
|
init_dist();
|
|
79339
79339
|
init_src5();
|
|
79340
|
-
import { existsSync as
|
|
79340
|
+
import { existsSync as existsSync31, readFileSync as readFileSync30, unlinkSync as unlinkSync3, writeFileSync as writeFileSync23 } from "node:fs";
|
|
79341
79341
|
|
|
79342
79342
|
// src/principle-sync.ts
|
|
79343
79343
|
init_src5();
|
|
@@ -82293,10 +82293,14 @@ function applyStopRequest(profile, now = () => /* @__PURE__ */ new Date()) {
|
|
|
82293
82293
|
}
|
|
82294
82294
|
};
|
|
82295
82295
|
}
|
|
82296
|
-
async function startHistoryStage(root,
|
|
82296
|
+
async function startHistoryStage(root, input, client, walk10 = {}) {
|
|
82297
|
+
let profile = input;
|
|
82297
82298
|
if (!profile.projectId) return { started: false, reason: "no-project", profile };
|
|
82298
|
-
const consents = historyConsents(profile);
|
|
82299
82299
|
const response = await reportHistoryStage(client, profile);
|
|
82300
|
+
if (response?.consents && walk10.applyConsents) {
|
|
82301
|
+
profile = walk10.applyConsents(profile, response.consents);
|
|
82302
|
+
}
|
|
82303
|
+
const consents = historyConsents(profile);
|
|
82300
82304
|
if (response?.stopRequested && profile.historyStage && profile.historyStage.state !== "refused") {
|
|
82301
82305
|
const stopped = applyStopRequest(profile);
|
|
82302
82306
|
await reportHistoryStage(client, stopped);
|
|
@@ -82307,1197 +82311,1610 @@ async function startHistoryStage(root, profile, client, walk10 = {}) {
|
|
|
82307
82311
|
return { started: true, profile: outcome.profile };
|
|
82308
82312
|
}
|
|
82309
82313
|
|
|
82310
|
-
// src/
|
|
82311
|
-
|
|
82314
|
+
// src/link-flow.ts
|
|
82315
|
+
init_cloud_auth();
|
|
82316
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
82317
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
82318
|
+
import { existsSync as existsSync29, writeFileSync as writeFileSync22 } from "node:fs";
|
|
82319
|
+
import { join as join34 } from "node:path";
|
|
82312
82320
|
|
|
82313
|
-
// src/
|
|
82314
|
-
|
|
82315
|
-
|
|
82316
|
-
import {
|
|
82317
|
-
|
|
82318
|
-
|
|
82319
|
-
|
|
82320
|
-
|
|
82321
|
-
|
|
82322
|
-
|
|
82323
|
-
|
|
82324
|
-
}
|
|
82325
|
-
return null;
|
|
82326
|
-
}
|
|
82327
|
-
var RETRY_COOLDOWN_MS = 15 * 6e4;
|
|
82328
|
-
function createRootAdopter(deps) {
|
|
82329
|
-
const attempted = /* @__PURE__ */ new Map();
|
|
82330
|
-
let chain2 = Promise.resolve();
|
|
82331
|
-
const tryAdopt = async (path2) => {
|
|
82332
|
-
if (!deps.linkingAllowed()) return;
|
|
82333
|
-
const root = findGitRoot(path2);
|
|
82334
|
-
if (!root || deps.isKnownRoot(root)) return;
|
|
82335
|
-
const last = attempted.get(root);
|
|
82336
|
-
const now = Date.now();
|
|
82337
|
-
if (last !== void 0 && now - last < RETRY_COOLDOWN_MS) return;
|
|
82338
|
-
attempted.set(root, now);
|
|
82339
|
-
if (!resolveGitDir(root)) return;
|
|
82340
|
-
const locator = detectRepoLocator(root);
|
|
82341
|
-
if (!locator) return;
|
|
82342
|
-
const claimed = await deps.lookup(locator);
|
|
82343
|
-
if (!claimed) return;
|
|
82344
|
-
await deps.adopt(root);
|
|
82345
|
-
deps.log(`[errata] adopted ${root} \u2014 ${locator} is claimed by your org (project ${claimed.id})`);
|
|
82346
|
-
};
|
|
82321
|
+
// src/oauth-login.ts
|
|
82322
|
+
init_src7();
|
|
82323
|
+
init_cloud_auth();
|
|
82324
|
+
import { createServer } from "node:http";
|
|
82325
|
+
var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
82326
|
+
var strip = (u) => u.replace(/\/+$/, "");
|
|
82327
|
+
function deviceEndpoints(tokenEndpoint) {
|
|
82328
|
+
const token = new URL(tokenEndpoint);
|
|
82329
|
+
const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
|
|
82330
|
+
if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
|
|
82331
|
+
const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
|
|
82332
|
+
const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
|
|
82347
82333
|
return {
|
|
82348
|
-
|
|
82349
|
-
|
|
82350
|
-
|
|
82351
|
-
},
|
|
82352
|
-
idle: () => chain2
|
|
82334
|
+
code: at(`${prefix}/api/auth/device/code`),
|
|
82335
|
+
token: at(`${prefix}/api/auth/device/token`),
|
|
82336
|
+
resource: at(`${prefix}/api/gate/session-token`)
|
|
82353
82337
|
};
|
|
82354
82338
|
}
|
|
82355
|
-
|
|
82356
|
-
|
|
82357
|
-
|
|
82358
|
-
const
|
|
82359
|
-
|
|
82360
|
-
|
|
82361
|
-
|
|
82362
|
-
|
|
82363
|
-
|
|
82364
|
-
|
|
82365
|
-
var SENTINEL_STORM_TICKS = numEnv4("ERRATA_SENTINEL_STORM_TICKS", 4);
|
|
82366
|
-
var SENTINEL_RENOTIFY_MS = numEnv4("ERRATA_SENTINEL_RENOTIFY_MS", 18e5);
|
|
82367
|
-
var SENTINEL_CALM_TICKS = numEnv4("ERRATA_SENTINEL_CALM_TICKS", 2);
|
|
82368
|
-
var SENTINEL_TOAST_COOLDOWN_MS = numEnv4("ERRATA_SENTINEL_TOAST_COOLDOWN_MS", 6e5);
|
|
82369
|
-
function createHealthSentinel(deps = {}) {
|
|
82370
|
-
const now = deps.now ?? Date.now;
|
|
82371
|
-
const cpuUsage = deps.cpuUsage ?? (() => process.cpuUsage());
|
|
82372
|
-
const readCensus = deps.readCensus ?? (() => {
|
|
82373
|
-
const c = watchCensus();
|
|
82374
|
-
return {
|
|
82375
|
-
dispatched: c.dispatched,
|
|
82376
|
-
logged: c.logged,
|
|
82377
|
-
topOffender: c.topOffender ? { path: c.topOffender.path, count: c.topOffender.count } : null
|
|
82378
|
-
};
|
|
82339
|
+
async function loginOAuthDevice(opts) {
|
|
82340
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
82341
|
+
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
82342
|
+
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
82343
|
+
const clientId = oauthClientId();
|
|
82344
|
+
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
82345
|
+
const codeResponse = await fetchFn(device.code, {
|
|
82346
|
+
method: "POST",
|
|
82347
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82348
|
+
body: JSON.stringify({ client_id: clientId, scope })
|
|
82379
82349
|
});
|
|
82380
|
-
|
|
82381
|
-
|
|
82382
|
-
|
|
82383
|
-
|
|
82384
|
-
|
|
82385
|
-
|
|
82386
|
-
|
|
82387
|
-
|
|
82388
|
-
|
|
82389
|
-
|
|
82390
|
-
|
|
82391
|
-
|
|
82392
|
-
const
|
|
82393
|
-
|
|
82394
|
-
|
|
82395
|
-
|
|
82396
|
-
|
|
82397
|
-
|
|
82398
|
-
|
|
82399
|
-
|
|
82400
|
-
|
|
82401
|
-
|
|
82402
|
-
|
|
82403
|
-
|
|
82404
|
-
|
|
82405
|
-
|
|
82406
|
-
|
|
82407
|
-
|
|
82408
|
-
|
|
82409
|
-
|
|
82410
|
-
|
|
82411
|
-
|
|
82412
|
-
|
|
82413
|
-
|
|
82414
|
-
deps.onChange?.(next);
|
|
82415
|
-
} else if (next && distress) {
|
|
82416
|
-
distress.body = next.body;
|
|
82417
|
-
distress.headline = next.headline;
|
|
82418
|
-
if (t - lastNotifiedAt >= SENTINEL_RENOTIFY_MS) maybeToast(distress, t);
|
|
82419
|
-
} else if (!next && distress) {
|
|
82420
|
-
deps.ledger?.("clear", distress);
|
|
82421
|
-
distress = null;
|
|
82422
|
-
deps.onChange?.(null);
|
|
82423
|
-
}
|
|
82424
|
-
};
|
|
82425
|
-
const evaluate = () => {
|
|
82426
|
-
const t = now();
|
|
82427
|
-
const wallMs = Math.max(1, t - prevTs);
|
|
82428
|
-
const cpu = cpuUsage();
|
|
82429
|
-
const cores = (cpu.user + cpu.system - prevCpu.user - prevCpu.system) / 1e3 / wallMs;
|
|
82430
|
-
prevCpu = cpu;
|
|
82431
|
-
prevTs = t;
|
|
82432
|
-
coreSamples.push(cores);
|
|
82433
|
-
if (coreSamples.length > SENTINEL_WINDOW_TICKS) coreSamples.shift();
|
|
82434
|
-
const c = readCensus();
|
|
82435
|
-
const dispatchRate = prevDispatched === null ? 0 : (c.dispatched - prevDispatched) / wallMs * 1e3;
|
|
82436
|
-
const survived = c.logged - prevLogged;
|
|
82437
|
-
prevDispatched = c.dispatched;
|
|
82438
|
-
prevLogged = c.logged;
|
|
82439
|
-
stormTicks = dispatchRate > SENTINEL_STORM_RATE && survived === 0 ? stormTicks + 1 : 0;
|
|
82440
|
-
let wanted = null;
|
|
82441
|
-
if (stormTicks >= SENTINEL_STORM_TICKS) {
|
|
82442
|
-
wanted = {
|
|
82443
|
-
kind: "watch-storm",
|
|
82444
|
-
headline: `errata's file watcher is stuck busy (~${Math.round(dispatchRate)} events/sec with nothing changing) \u2014 run \`errata status\` for detail`,
|
|
82445
|
-
body: `filesystem watcher is processing ~${Math.round(dispatchRate)} events/sec with NOTHING surviving \u2014 a wedged or ignored-tree watch is burning CPU for no signal \xB7 ${attribution()}`,
|
|
82446
|
-
sinceTs: distress?.kind === "watch-storm" ? distress.sinceTs : t
|
|
82447
|
-
};
|
|
82448
|
-
} else {
|
|
82449
|
-
const windowFull = coreSamples.length >= SENTINEL_WINDOW_TICKS;
|
|
82450
|
-
const avgCores = coreSamples.reduce((a, b) => a + b, 0) / Math.max(1, coreSamples.length);
|
|
82451
|
-
if (windowFull && avgCores >= SENTINEL_BURN_CORES && cores >= SENTINEL_BURN_CORES && activePasses().length === 0) {
|
|
82452
|
-
wanted = {
|
|
82453
|
-
kind: "cpu-burn",
|
|
82454
|
-
headline: `errata is using more CPU than expected (${avgCores.toFixed(1)} cores with no work running) \u2014 run \`errata status\` for detail`,
|
|
82455
|
-
body: `sustained CPU with no pass running \u2014 avg ${avgCores.toFixed(1)} cores \xB7 ${attribution()}`,
|
|
82456
|
-
sinceTs: distress?.kind === "cpu-burn" ? distress.sinceTs : t
|
|
82457
|
-
};
|
|
82350
|
+
if (!codeResponse.ok) {
|
|
82351
|
+
throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
|
|
82352
|
+
}
|
|
82353
|
+
const code = await codeResponse.json();
|
|
82354
|
+
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
82355
|
+
throw new Error("device authorization returned an incomplete response");
|
|
82356
|
+
}
|
|
82357
|
+
opts.onVerification?.({
|
|
82358
|
+
url: code.verification_uri_complete ?? code.verification_uri,
|
|
82359
|
+
userCode: code.user_code,
|
|
82360
|
+
expiresInSec: code.expires_in
|
|
82361
|
+
});
|
|
82362
|
+
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
82363
|
+
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
82364
|
+
let deviceSession = null;
|
|
82365
|
+
let approvedInstallation = null;
|
|
82366
|
+
while (Date.now() < deadline) {
|
|
82367
|
+
await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
|
|
82368
|
+
const poll = await fetchFn(device.token, {
|
|
82369
|
+
method: "POST",
|
|
82370
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82371
|
+
body: JSON.stringify({
|
|
82372
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
82373
|
+
device_code: code.device_code,
|
|
82374
|
+
client_id: clientId
|
|
82375
|
+
})
|
|
82376
|
+
});
|
|
82377
|
+
const payload = await poll.json().catch(() => null);
|
|
82378
|
+
if (!poll.ok) {
|
|
82379
|
+
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
82380
|
+
if (error48 === "authorization_pending") continue;
|
|
82381
|
+
if (error48 === "slow_down") {
|
|
82382
|
+
intervalMs += 5e3;
|
|
82383
|
+
continue;
|
|
82458
82384
|
}
|
|
82385
|
+
throw new Error(`device authorization failed: ${error48}`);
|
|
82459
82386
|
}
|
|
82460
|
-
|
|
82461
|
-
|
|
82462
|
-
|
|
82463
|
-
|
|
82464
|
-
|
|
82465
|
-
|
|
82466
|
-
|
|
82467
|
-
|
|
82468
|
-
|
|
82469
|
-
|
|
82470
|
-
|
|
82471
|
-
|
|
82472
|
-
|
|
82473
|
-
|
|
82474
|
-
}
|
|
82475
|
-
|
|
82476
|
-
|
|
82477
|
-
|
|
82478
|
-
|
|
82479
|
-
|
|
82480
|
-
|
|
82481
|
-
|
|
82482
|
-
|
|
82387
|
+
deviceSession = typeof payload?.["access_token"] === "string" ? payload["access_token"] : null;
|
|
82388
|
+
approvedInstallation = typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null;
|
|
82389
|
+
break;
|
|
82390
|
+
}
|
|
82391
|
+
if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
|
|
82392
|
+
if (approvedInstallation !== opts.installationId) {
|
|
82393
|
+
throw new Error("approved installation does not match the terminal request");
|
|
82394
|
+
}
|
|
82395
|
+
const resourceResponse = await fetchFn(device.resource, {
|
|
82396
|
+
method: "POST",
|
|
82397
|
+
headers: {
|
|
82398
|
+
authorization: `Bearer ${deviceSession}`,
|
|
82399
|
+
"content-type": "application/json",
|
|
82400
|
+
accept: "application/json"
|
|
82401
|
+
},
|
|
82402
|
+
body: JSON.stringify({
|
|
82403
|
+
installationId: approvedInstallation,
|
|
82404
|
+
clientId,
|
|
82405
|
+
scopes: scope.split(/\s+/).filter((value) => !["openid", "profile", "email", "offline_access"].includes(value))
|
|
82406
|
+
})
|
|
82407
|
+
});
|
|
82408
|
+
const resource = await resourceResponse.json().catch(() => null);
|
|
82409
|
+
if (!resourceResponse.ok || typeof resource?.["access_token"] !== "string") {
|
|
82410
|
+
throw new Error(`device resource exchange failed: HTTP ${resourceResponse.status}`);
|
|
82411
|
+
}
|
|
82412
|
+
return {
|
|
82413
|
+
installationId: approvedInstallation,
|
|
82414
|
+
clientId,
|
|
82415
|
+
tokenEndpoint: device.resource,
|
|
82416
|
+
tokens: {
|
|
82417
|
+
accessToken: resource["access_token"],
|
|
82418
|
+
refreshToken: deviceSession,
|
|
82419
|
+
...typeof resource["expires_in"] === "number" ? { expiresInSec: resource["expires_in"] } : {}
|
|
82483
82420
|
}
|
|
82484
82421
|
};
|
|
82485
82422
|
}
|
|
82486
|
-
|
|
82487
|
-
|
|
82488
|
-
|
|
82489
|
-
|
|
82490
|
-
|
|
82491
|
-
|
|
82492
|
-
|
|
82493
|
-
const out2 = [];
|
|
82494
|
-
for (const entry of input.entries) {
|
|
82495
|
-
if (input.liveRoots.has(input.normPath(entry.path))) continue;
|
|
82496
|
-
let n = 0;
|
|
82497
|
-
try {
|
|
82498
|
-
n = find(entry.path, input.sinceMs);
|
|
82499
|
-
} catch {
|
|
82500
|
-
continue;
|
|
82423
|
+
async function discoverEndpoints(cloudUrl, fetchFn = fetch) {
|
|
82424
|
+
let consoleUrl = cloudUrl;
|
|
82425
|
+
try {
|
|
82426
|
+
const prm = await fetchFn(`${strip(cloudUrl)}/.well-known/oauth-protected-resource`);
|
|
82427
|
+
if (prm.ok) {
|
|
82428
|
+
const j = await prm.json();
|
|
82429
|
+
if (j.authorization_servers?.[0]) consoleUrl = j.authorization_servers[0];
|
|
82501
82430
|
}
|
|
82502
|
-
|
|
82431
|
+
} catch {
|
|
82503
82432
|
}
|
|
82504
|
-
|
|
82505
|
-
}
|
|
82506
|
-
|
|
82507
|
-
|
|
82508
|
-
|
|
82509
|
-
|
|
82510
|
-
|
|
82511
|
-
|
|
82512
|
-
|
|
82513
|
-
|
|
82514
|
-
|
|
82515
|
-
}
|
|
82516
|
-
function normPath(p) {
|
|
82517
|
-
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
82518
|
-
}
|
|
82519
|
-
function contextKey(p) {
|
|
82520
|
-
return `${[...p.stack].sort().join(",")}|${p.domains[0] ?? p.languages[0] ?? "?"}`;
|
|
82521
|
-
}
|
|
82522
|
-
function pickOwnerRoot(roots, absPath) {
|
|
82523
|
-
if (!absPath) return null;
|
|
82524
|
-
const n = normPath(absPath);
|
|
82525
|
-
let best = null;
|
|
82526
|
-
let bestLen = -1;
|
|
82527
|
-
for (const root of roots) {
|
|
82528
|
-
const r = normPath(root);
|
|
82529
|
-
if ((n === r || n.startsWith(r + "/")) && r.length > bestLen) {
|
|
82530
|
-
best = root;
|
|
82531
|
-
bestLen = r.length;
|
|
82433
|
+
try {
|
|
82434
|
+
const asm = await fetchFn(`${strip(consoleUrl)}/.well-known/oauth-authorization-server`);
|
|
82435
|
+
if (asm.ok) {
|
|
82436
|
+
const j = await asm.json();
|
|
82437
|
+
if (j.authorization_endpoint && j.token_endpoint) {
|
|
82438
|
+
return {
|
|
82439
|
+
authorizationEndpoint: j.authorization_endpoint,
|
|
82440
|
+
tokenEndpoint: j.token_endpoint,
|
|
82441
|
+
...j.registration_endpoint ? { registrationEndpoint: j.registration_endpoint } : {}
|
|
82442
|
+
};
|
|
82443
|
+
}
|
|
82532
82444
|
}
|
|
82445
|
+
} catch {
|
|
82533
82446
|
}
|
|
82534
|
-
|
|
82447
|
+
const base = strip(consoleUrl);
|
|
82448
|
+
return {
|
|
82449
|
+
authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
|
|
82450
|
+
tokenEndpoint: `${base}/api/auth/oauth2/token`
|
|
82451
|
+
};
|
|
82535
82452
|
}
|
|
82536
|
-
function
|
|
82537
|
-
const
|
|
82538
|
-
|
|
82453
|
+
function buildAuthorizeUrl(endpoint, p) {
|
|
82454
|
+
const u = new URL(endpoint);
|
|
82455
|
+
u.searchParams.set("response_type", "code");
|
|
82456
|
+
u.searchParams.set("client_id", p.clientId);
|
|
82457
|
+
u.searchParams.set("redirect_uri", p.redirectUri);
|
|
82458
|
+
u.searchParams.set("scope", p.scope);
|
|
82459
|
+
u.searchParams.set("state", p.state);
|
|
82460
|
+
u.searchParams.set("code_challenge", p.challenge);
|
|
82461
|
+
u.searchParams.set("code_challenge_method", p.method);
|
|
82462
|
+
return u.toString();
|
|
82539
82463
|
}
|
|
82540
|
-
async function
|
|
82541
|
-
const
|
|
82542
|
-
|
|
82543
|
-
|
|
82544
|
-
|
|
82545
|
-
|
|
82546
|
-
}
|
|
82547
|
-
|
|
82548
|
-
|
|
82549
|
-
|
|
82550
|
-
|
|
82551
|
-
|
|
82552
|
-
|
|
82553
|
-
|
|
82554
|
-
|
|
82555
|
-
|
|
82556
|
-
|
|
82557
|
-
|
|
82558
|
-
|
|
82559
|
-
|
|
82560
|
-
|
|
82561
|
-
|
|
82562
|
-
|
|
82563
|
-
|
|
82564
|
-
|
|
82565
|
-
|
|
82566
|
-
|
|
82567
|
-
|
|
82568
|
-
|
|
82569
|
-
|
|
82570
|
-
|
|
82571
|
-
};
|
|
82572
|
-
const identityFor = (entry) => {
|
|
82573
|
-
if (opts.apiKey) {
|
|
82574
|
-
return {
|
|
82575
|
-
profileName: null,
|
|
82576
|
-
profile: null,
|
|
82577
|
-
rule: "env-config-path",
|
|
82578
|
-
invalid: false,
|
|
82579
|
-
reason: "daemon started with an explicit --api-key (process-pinned)"
|
|
82580
|
-
};
|
|
82581
|
-
}
|
|
82582
|
-
return resolveWorkspaceIdentity(loadConfig(), entry);
|
|
82583
|
-
};
|
|
82584
|
-
const identityBlocks = /* @__PURE__ */ new Map();
|
|
82585
|
-
const noteIdentityBlock = (wsId, wsName, identity) => {
|
|
82586
|
-
if (!identityBlocks.has(wsId)) {
|
|
82587
|
-
identityBlocks.set(wsId, {
|
|
82588
|
-
workspace: wsName,
|
|
82589
|
-
profileName: identity.profileName,
|
|
82590
|
-
reason: identity.reason,
|
|
82591
|
-
since: Date.now()
|
|
82464
|
+
async function loginOAuthLoopback(opts) {
|
|
82465
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
82466
|
+
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
82467
|
+
const pkce = generatePkce();
|
|
82468
|
+
const state = randomState();
|
|
82469
|
+
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
82470
|
+
const { code, redirectUri, clientId } = await new Promise(
|
|
82471
|
+
(resolve7, reject) => {
|
|
82472
|
+
const server = createServer((req, res) => {
|
|
82473
|
+
const u = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
82474
|
+
if (u.pathname !== "/callback") {
|
|
82475
|
+
res.writeHead(404);
|
|
82476
|
+
res.end();
|
|
82477
|
+
return;
|
|
82478
|
+
}
|
|
82479
|
+
const addr2 = server.address();
|
|
82480
|
+
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
82481
|
+
const err2 = u.searchParams.get("error");
|
|
82482
|
+
const code2 = u.searchParams.get("code");
|
|
82483
|
+
const returnedState = u.searchParams.get("state");
|
|
82484
|
+
if (err2 || !code2 || returnedState !== state) {
|
|
82485
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
82486
|
+
res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
|
|
82487
|
+
server.close();
|
|
82488
|
+
reject(new Error(err2 ?? "invalid_callback"));
|
|
82489
|
+
return;
|
|
82490
|
+
}
|
|
82491
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
82492
|
+
res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
|
|
82493
|
+
server.close();
|
|
82494
|
+
resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
|
|
82592
82495
|
});
|
|
82593
|
-
|
|
82594
|
-
|
|
82595
|
-
|
|
82596
|
-
|
|
82597
|
-
|
|
82598
|
-
|
|
82599
|
-
|
|
82600
|
-
|
|
82601
|
-
|
|
82602
|
-
|
|
82603
|
-
|
|
82604
|
-
|
|
82605
|
-
|
|
82606
|
-
|
|
82607
|
-
|
|
82608
|
-
|
|
82609
|
-
|
|
82610
|
-
|
|
82611
|
-
|
|
82612
|
-
|
|
82613
|
-
|
|
82614
|
-
|
|
82615
|
-
|
|
82616
|
-
|
|
82496
|
+
let activeClientId = oauthClientId();
|
|
82497
|
+
server.on("error", reject);
|
|
82498
|
+
server.listen(0, "127.0.0.1", () => {
|
|
82499
|
+
void (async () => {
|
|
82500
|
+
try {
|
|
82501
|
+
const addr2 = server.address();
|
|
82502
|
+
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
82503
|
+
const redirectUri2 = `http://127.0.0.1:${port}/callback`;
|
|
82504
|
+
if (endpoints.registrationEndpoint) {
|
|
82505
|
+
const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
|
|
82506
|
+
redirectUri: redirectUri2,
|
|
82507
|
+
scope,
|
|
82508
|
+
clientName: "errata daemon"
|
|
82509
|
+
});
|
|
82510
|
+
activeClientId = registered.clientId;
|
|
82511
|
+
}
|
|
82512
|
+
const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
|
|
82513
|
+
clientId: activeClientId,
|
|
82514
|
+
redirectUri: redirectUri2,
|
|
82515
|
+
scope,
|
|
82516
|
+
state,
|
|
82517
|
+
challenge: pkce.challenge,
|
|
82518
|
+
method: pkce.method
|
|
82519
|
+
});
|
|
82520
|
+
if (opts.open) opts.open(url2);
|
|
82521
|
+
else console.log(`approve this login in your browser:
|
|
82522
|
+
${url2}`);
|
|
82523
|
+
} catch (err2) {
|
|
82524
|
+
server.close();
|
|
82525
|
+
reject(err2);
|
|
82526
|
+
}
|
|
82527
|
+
})();
|
|
82617
82528
|
});
|
|
82618
|
-
|
|
82619
|
-
|
|
82620
|
-
|
|
82621
|
-
|
|
82622
|
-
|
|
82623
|
-
const sharedStore = openGraphStore({ path: sharedDbPath });
|
|
82624
|
-
const bundledRun = import.meta.url.endsWith(".mjs");
|
|
82625
|
-
const consolidateWorker = bundledRun && sharedDbPath !== ":memory:" ? new ConsolidateWorker() : null;
|
|
82626
|
-
startLoopLagMonitor();
|
|
82627
|
-
let baseUrl = "";
|
|
82628
|
-
const records = [];
|
|
82629
|
-
const mountedIds = /* @__PURE__ */ new Set();
|
|
82630
|
-
const machineDominantLanguage = () => {
|
|
82631
|
-
const langCounts = /* @__PURE__ */ new Map();
|
|
82632
|
-
for (const r of records) {
|
|
82633
|
-
for (const l of r.engine.profile.languages ?? []) {
|
|
82634
|
-
const k = String(l).trim().toLowerCase();
|
|
82635
|
-
if (k) langCounts.set(k, (langCounts.get(k) ?? 0) + 1);
|
|
82636
|
-
}
|
|
82637
|
-
}
|
|
82638
|
-
return [...langCounts.entries()].sort(
|
|
82639
|
-
(a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)
|
|
82640
|
-
)[0]?.[0];
|
|
82641
|
-
};
|
|
82642
|
-
const updatePoller = opts.updateCheck ? startUpdatePoller({
|
|
82643
|
-
channel: loadConfig().updateChannel,
|
|
82644
|
-
onChange: (pending) => {
|
|
82645
|
-
if (pending) {
|
|
82646
|
-
console.log(`\u26A1 errata update available: ${pending.current} \u2192 ${pending.latest} \u2014 run: errata update`);
|
|
82647
|
-
}
|
|
82648
|
-
for (const r of records) r.engine.markContextDirty();
|
|
82649
|
-
}
|
|
82650
|
-
}) : null;
|
|
82651
|
-
const healthSentinel = createHealthSentinel({
|
|
82652
|
-
onChange: () => {
|
|
82653
|
-
for (const r of records) r.engine.markContextDirty();
|
|
82654
|
-
},
|
|
82655
|
-
ledger: (event, d) => {
|
|
82656
|
-
const dir = records[0]?.engine.paths.configDir;
|
|
82657
|
-
if (dir) {
|
|
82658
|
-
appendPassLedger(dir, "health-sentinel", 0, {
|
|
82659
|
-
[`distress:${event}:${d.kind}`]: 1,
|
|
82660
|
-
distressSinceTs: d.sinceTs
|
|
82661
|
-
});
|
|
82529
|
+
if (opts.timeoutMs) {
|
|
82530
|
+
setTimeout(() => {
|
|
82531
|
+
server.close();
|
|
82532
|
+
reject(new Error("login timed out \u2014 run `errata login` again"));
|
|
82533
|
+
}, opts.timeoutMs).unref?.();
|
|
82662
82534
|
}
|
|
82663
82535
|
}
|
|
82536
|
+
);
|
|
82537
|
+
const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
|
|
82538
|
+
code,
|
|
82539
|
+
codeVerifier: pkce.verifier,
|
|
82540
|
+
redirectUri,
|
|
82541
|
+
clientId
|
|
82664
82542
|
});
|
|
82665
|
-
|
|
82666
|
-
|
|
82667
|
-
|
|
82668
|
-
|
|
82669
|
-
|
|
82670
|
-
|
|
82671
|
-
|
|
82672
|
-
|
|
82673
|
-
|
|
82674
|
-
const
|
|
82675
|
-
|
|
82676
|
-
|
|
82677
|
-
|
|
82678
|
-
|
|
82679
|
-
|
|
82680
|
-
|
|
82681
|
-
|
|
82682
|
-
|
|
82683
|
-
|
|
82684
|
-
|
|
82685
|
-
|
|
82686
|
-
|
|
82687
|
-
|
|
82688
|
-
|
|
82689
|
-
|
|
82690
|
-
boundaryFlushBatches++;
|
|
82691
|
-
boundaryFlushUnits += Math.max(0, units);
|
|
82692
|
-
boundaryFlushItems += Math.max(0, items);
|
|
82693
|
-
if (detail) boundaryFlushDetail = detail;
|
|
82694
|
-
if (units > 0 || items > 0) boundaryFlushLastAdvanceAt = Date.now();
|
|
82543
|
+
return { tokens, tokenEndpoint: endpoints.tokenEndpoint, clientId };
|
|
82544
|
+
}
|
|
82545
|
+
|
|
82546
|
+
// src/link-flow.ts
|
|
82547
|
+
function remoteFingerprint(locator) {
|
|
82548
|
+
return createHash15("sha256").update(locator).digest("hex");
|
|
82549
|
+
}
|
|
82550
|
+
function isDecision(value) {
|
|
82551
|
+
if (!value || typeof value !== "object") return false;
|
|
82552
|
+
const v = value;
|
|
82553
|
+
return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
|
|
82554
|
+
}
|
|
82555
|
+
async function linkThroughConsole(opts) {
|
|
82556
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
82557
|
+
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
82558
|
+
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
82559
|
+
const clientId = oauthClientId();
|
|
82560
|
+
const link = {
|
|
82561
|
+
locator: opts.facts.locator,
|
|
82562
|
+
remoteFingerprint: remoteFingerprint(opts.facts.locator),
|
|
82563
|
+
...opts.facts.name ? { name: opts.facts.name } : {},
|
|
82564
|
+
...opts.facts.rootCommitSha ? { rootCommitSha: opts.facts.rootCommitSha } : {},
|
|
82565
|
+
...opts.facts.installationId ? { installationId: opts.facts.installationId } : {},
|
|
82566
|
+
...opts.facts.agents && opts.facts.agents.length > 0 ? { agents: opts.facts.agents.slice(0, 12) } : {},
|
|
82567
|
+
...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
|
|
82695
82568
|
};
|
|
82696
|
-
const
|
|
82697
|
-
|
|
82698
|
-
|
|
82699
|
-
|
|
82700
|
-
ageMs: boundaryFlushStartedAt ? Date.now() - boundaryFlushStartedAt : 0,
|
|
82701
|
-
units: boundaryFlushUnits,
|
|
82702
|
-
items: boundaryFlushItems,
|
|
82703
|
-
chunks: boundaryFlushBatches,
|
|
82704
|
-
stalledMs: boundaryFlushLastAdvanceAt ? Date.now() - boundaryFlushLastAdvanceAt : 0,
|
|
82705
|
-
// Held workspaces, so a hold is REPORTABLE and not merely logged once at the
|
|
82706
|
-
// transition. `errata status` reads this rather than the on-disk profile:
|
|
82707
|
-
// the profile records what the workspace INTENDS to assert, which is why
|
|
82708
|
-
// status could print "→ project 019f462d" for hours while every batch was
|
|
82709
|
-
// being held or shipped unlinked. A hold is the daemon's actual behavior.
|
|
82710
|
-
holds: [
|
|
82711
|
-
...[...projectHolds.values()].map((h) => ({
|
|
82712
|
-
workspace: h.workspace,
|
|
82713
|
-
kind: "project",
|
|
82714
|
-
reason: h.reason,
|
|
82715
|
-
sinceMs: Date.now() - h.since
|
|
82716
|
-
})),
|
|
82717
|
-
...[...identityBlocks.values()].map((h) => ({
|
|
82718
|
-
workspace: h.workspace,
|
|
82719
|
-
kind: "identity",
|
|
82720
|
-
reason: h.reason,
|
|
82721
|
-
sinceMs: Date.now() - h.since
|
|
82722
|
-
}))
|
|
82723
|
-
]
|
|
82569
|
+
const codeResponse = await fetchFn(device.code, {
|
|
82570
|
+
method: "POST",
|
|
82571
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82572
|
+
body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
|
|
82724
82573
|
});
|
|
82725
|
-
|
|
82726
|
-
|
|
82727
|
-
|
|
82728
|
-
|
|
82729
|
-
const fireBoundary = () => {
|
|
82730
|
-
for (const l of boundaryListeners) {
|
|
82731
|
-
try {
|
|
82732
|
-
l();
|
|
82733
|
-
} catch {
|
|
82734
|
-
}
|
|
82735
|
-
}
|
|
82736
|
-
};
|
|
82737
|
-
const buildRecord = (entry) => {
|
|
82738
|
-
const id = entry.id;
|
|
82739
|
-
const wsIdentity = identityFor(entry);
|
|
82740
|
-
let wsCloud = cloud;
|
|
82741
|
-
if (wsIdentity.rule === "workspace-binding") {
|
|
82742
|
-
if (wsIdentity.invalid) {
|
|
82743
|
-
noteIdentityBlock(entry.id, entry.name, wsIdentity);
|
|
82744
|
-
wsCloud = blockedCloudClient(wsIdentity.reason);
|
|
82745
|
-
} else {
|
|
82746
|
-
wsCloud = authedCloudClientForProfile(loadConfig(), wsIdentity.profileName, {
|
|
82747
|
-
timeoutMs: CLOUD_TIMEOUT_MS,
|
|
82748
|
-
daemonVersion: DAEMON_VERSION
|
|
82749
|
-
});
|
|
82750
|
-
console.log(`[identity] workspace "${entry.name}" bound \u2192 profile "${wsIdentity.profileName}"`);
|
|
82751
|
-
}
|
|
82574
|
+
if (!codeResponse.ok) {
|
|
82575
|
+
const text = (await codeResponse.text()).slice(0, 200);
|
|
82576
|
+
if (codeResponse.status === 404) {
|
|
82577
|
+
throw new Error("this console does not offer the link flow yet (device authorization is off)");
|
|
82752
82578
|
}
|
|
82753
|
-
|
|
82754
|
-
workspaceRoot: entry.path,
|
|
82755
|
-
cloud: wsCloud,
|
|
82756
|
-
apiKey: opts.apiKey,
|
|
82757
|
-
reviewUrl: () => `${baseUrl}/ws/${id}/review`,
|
|
82758
|
-
sharedStore,
|
|
82759
|
-
pendingUpdate: () => serverPending ?? updatePoller?.current() ?? null,
|
|
82760
|
-
// Ops-gated (default OFF): the 🚨 notice reads as "errata broke my
|
|
82761
|
-
// machine" to a non-operator. Detection + ledger run regardless — only
|
|
82762
|
-
// this push surface (and the toast, gated in notifyOpsEvent) is scoped.
|
|
82763
|
-
daemonDistress: () => loadConfig().opsNotices ? healthSentinel.current() : null,
|
|
82764
|
-
...opts.skipWatchers ? { skipWatchers: true } : {}
|
|
82765
|
-
});
|
|
82766
|
-
const webApp = buildWebUi({
|
|
82767
|
-
paths: engine.paths,
|
|
82768
|
-
store: engine.store,
|
|
82769
|
-
log: engine.log,
|
|
82770
|
-
profile: engine.profile,
|
|
82771
|
-
daemonVersion: DAEMON_VERSION,
|
|
82772
|
-
basePath: `/ws/${id}`,
|
|
82773
|
-
onWorkingFileChanged: engine.webHooks.onWorkingFileChanged,
|
|
82774
|
-
onHook: engine.webHooks.onHook,
|
|
82775
|
-
onHookEvent: engine.webHooks.onHookEvent,
|
|
82776
|
-
onShellEvent: engine.webHooks.onShellEvent,
|
|
82777
|
-
// The tool-run RECORD seam (SP-tool-ground). The single-engine path wired
|
|
82778
|
-
// this in #1172; this sibling builder never did, so on every multi
|
|
82779
|
-
// deployment (i.e. every real one) the webui logged "tool-run recorded"
|
|
82780
|
-
// and dropped the record on the floor — toolSessionsTracked read 0 across
|
|
82781
|
-
// 936 turns while the log showed records landing (VS-corroboration-rate).
|
|
82782
|
-
onToolsObserved: engine.webHooks.onToolsObserved,
|
|
82783
|
-
onToolError: engine.webHooks.onToolError,
|
|
82784
|
-
onDiagnostics: engine.webHooks.onDiagnostics,
|
|
82785
|
-
onTurn: engine.webHooks.onTurn,
|
|
82786
|
-
onMessage: engine.webHooks.onMessage,
|
|
82787
|
-
onSessionEnd: (e) => {
|
|
82788
|
-
engine.webHooks.onSessionEnd(e);
|
|
82789
|
-
fireBoundary();
|
|
82790
|
-
}
|
|
82791
|
-
});
|
|
82792
|
-
return { entry, engine, webApp, root: entry.path, id, lastHookAt: Date.now() };
|
|
82793
|
-
};
|
|
82794
|
-
const pruned = pruneMissingWorkspaces();
|
|
82795
|
-
if (pruned.length > 0) {
|
|
82796
|
-
console.log(`[errata] pruned ${pruned.length} registered workspace(s) whose path no longer exists`);
|
|
82579
|
+
throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
|
|
82797
82580
|
}
|
|
82798
|
-
|
|
82799
|
-
|
|
82800
|
-
|
|
82801
|
-
for (const b of boundWorkspaceEntries()) {
|
|
82802
|
-
console.log(`[identity] bound workspace: ${b.name} \u2192 profile "${b.installationProfile}"`);
|
|
82803
|
-
}
|
|
82581
|
+
const code = await codeResponse.json();
|
|
82582
|
+
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
82583
|
+
throw new Error("link request returned an incomplete response");
|
|
82804
82584
|
}
|
|
82805
|
-
|
|
82806
|
-
|
|
82807
|
-
|
|
82808
|
-
|
|
82809
|
-
|
|
82810
|
-
|
|
82811
|
-
|
|
82585
|
+
opts.onVerification?.({
|
|
82586
|
+
url: code.verification_uri_complete ?? code.verification_uri,
|
|
82587
|
+
userCode: code.user_code,
|
|
82588
|
+
expiresInSec: code.expires_in
|
|
82589
|
+
});
|
|
82590
|
+
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
82591
|
+
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
82592
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
|
|
82593
|
+
while (Date.now() < deadline) {
|
|
82594
|
+
await sleep(intervalMs);
|
|
82595
|
+
const poll = await fetchFn(device.token, {
|
|
82596
|
+
method: "POST",
|
|
82597
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
82598
|
+
body: JSON.stringify({
|
|
82599
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
82600
|
+
device_code: code.device_code,
|
|
82601
|
+
client_id: clientId
|
|
82602
|
+
})
|
|
82603
|
+
});
|
|
82604
|
+
const payload = await poll.json().catch(() => null);
|
|
82605
|
+
if (!poll.ok) {
|
|
82606
|
+
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
82607
|
+
if (error48 === "authorization_pending") continue;
|
|
82608
|
+
if (error48 === "slow_down") {
|
|
82609
|
+
intervalMs += 5e3;
|
|
82610
|
+
continue;
|
|
82812
82611
|
}
|
|
82612
|
+
if (error48 === "access_denied") return { status: "denied" };
|
|
82613
|
+
if (error48 === "expired_token") return { status: "expired" };
|
|
82614
|
+
throw new Error(`link approval failed: ${error48}`);
|
|
82813
82615
|
}
|
|
82814
|
-
|
|
82815
|
-
|
|
82816
|
-
console
|
|
82817
|
-
`[errata] \u26A0 ${failed} of ${entries.length} project(s) failed to start \u2014 likely a stale daemon holding DB locks. Run \`errata stop\` (or kill the old errata process) and retry.`
|
|
82818
|
-
);
|
|
82616
|
+
const decision = payload?.["link"];
|
|
82617
|
+
if (!isDecision(decision)) {
|
|
82618
|
+
throw new Error("the console approved the device but returned no link decision");
|
|
82819
82619
|
}
|
|
82820
|
-
|
|
82821
|
-
|
|
82822
|
-
|
|
82823
|
-
|
|
82620
|
+
return {
|
|
82621
|
+
status: "approved",
|
|
82622
|
+
decision,
|
|
82623
|
+
installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
|
|
82624
|
+
};
|
|
82824
82625
|
}
|
|
82825
|
-
|
|
82826
|
-
|
|
82827
|
-
|
|
82828
|
-
|
|
82829
|
-
const entry = listWorkspaceEntries().find((e) => normPath(e.path) === norm);
|
|
82830
|
-
if (!entry) return { attached: false, id: null, reason: "not-registered" };
|
|
82831
|
-
let rec;
|
|
82832
|
-
try {
|
|
82833
|
-
rec = buildRecord(entry);
|
|
82834
|
-
} catch (err2) {
|
|
82835
|
-
return { attached: false, id: null, reason: err2 instanceof Error ? err2.message : "build-failed" };
|
|
82836
|
-
}
|
|
82837
|
-
records.push(rec);
|
|
82838
|
-
void ambientLinkAll();
|
|
82626
|
+
return { status: "expired" };
|
|
82627
|
+
}
|
|
82628
|
+
function collectRepoFacts(root) {
|
|
82629
|
+
const git3 = (args2) => {
|
|
82839
82630
|
try {
|
|
82840
|
-
|
|
82841
|
-
rec.engine.paths.daemonLock,
|
|
82842
|
-
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${rec.id}`, startedAt: Date.now() }),
|
|
82843
|
-
"utf8"
|
|
82844
|
-
);
|
|
82845
|
-
registerWorkspace(rec.engine.profile, rec.root);
|
|
82631
|
+
return execFileSync4("git", args2, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
82846
82632
|
} catch {
|
|
82633
|
+
return null;
|
|
82847
82634
|
}
|
|
82848
|
-
void rec.engine.reindex({ skipEmbed: true, quiet: rec.engine.store.nodeCount() > 0 }).catch(() => {
|
|
82849
|
-
});
|
|
82850
|
-
console.log(`[errata] hot-registered ${rec.entry.name} (${rec.id}) \u2014 now watching live`);
|
|
82851
|
-
return { attached: true, id: rec.id };
|
|
82852
82635
|
};
|
|
82853
|
-
|
|
82854
|
-
|
|
82855
|
-
|
|
82856
|
-
|
|
82857
|
-
|
|
82858
|
-
|
|
82859
|
-
}
|
|
82860
|
-
const root = typeof body2["path"] === "string" ? body2["path"] : null;
|
|
82861
|
-
if (!root) return c.json({ error: "path-required" }, 400);
|
|
82862
|
-
return c.json(attachWorkspace(root));
|
|
82863
|
-
});
|
|
82864
|
-
app.post("/api/notify-test", (c) => {
|
|
82865
|
-
notify("Errata \xB7 test", "Fired from the daemon \u2014 if you see this, daemon toasts render.");
|
|
82866
|
-
return c.json({ ok: true });
|
|
82867
|
-
});
|
|
82868
|
-
app.get(
|
|
82869
|
-
"/",
|
|
82870
|
-
(c) => c.json({
|
|
82871
|
-
daemonVersion: DAEMON_VERSION,
|
|
82872
|
-
pendingUpdate: updatePoller?.current() ?? null,
|
|
82873
|
-
projects: records.map((r) => ({
|
|
82874
|
-
id: r.id,
|
|
82875
|
-
name: r.entry.name,
|
|
82876
|
-
path: r.root,
|
|
82877
|
-
stack: r.entry.stack,
|
|
82878
|
-
nodes: r.engine.store.nodeCount(),
|
|
82879
|
-
// COUNT(*) — the materializing findNodesByLabel(...).length form held
|
|
82880
|
-
// the event loop for seconds per poll once stores grew (HZ-index-hydrate).
|
|
82881
|
-
problems: r.engine.store.countNodesByLabel("Problem"),
|
|
82882
|
-
solutions: r.engine.store.countNodesByLabel("Solution"),
|
|
82883
|
-
endpoints: `/ws/${r.id}/`
|
|
82884
|
-
})),
|
|
82885
|
-
humanView: "run `errata report` \u2014 the dashboard was retired (GRAFT 4e)"
|
|
82886
|
-
})
|
|
82887
|
-
);
|
|
82888
|
-
const STRANDED_TTL_MS = 6e4;
|
|
82889
|
-
const strandedCache = /* @__PURE__ */ new Map();
|
|
82890
|
-
const strandedRefreshing = /* @__PURE__ */ new Set();
|
|
82891
|
-
const strandedCount = (r) => {
|
|
82892
|
-
const hit = strandedCache.get(r.id);
|
|
82893
|
-
if (hit && Date.now() - hit.at < STRANDED_TTL_MS) return hit.count;
|
|
82894
|
-
if (!strandedRefreshing.has(r.id)) {
|
|
82895
|
-
strandedRefreshing.add(r.id);
|
|
82896
|
-
const finish = (count) => {
|
|
82897
|
-
strandedCache.set(r.id, { at: Date.now(), count });
|
|
82898
|
-
strandedRefreshing.delete(r.id);
|
|
82899
|
-
};
|
|
82900
|
-
if (consolidateWorker && r.engine.paths.castalia !== ":memory:") {
|
|
82901
|
-
consolidateWorker.run({ kind: "strandedCount", sharedDbPath, castaliaPath: r.engine.paths.castalia }).then((n) => finish(Number(n))).catch(() => {
|
|
82902
|
-
setImmediate(() => {
|
|
82903
|
-
const done = markPass(`stranded-scan:${r.entry.name}`);
|
|
82904
|
-
try {
|
|
82905
|
-
finish(findStrandedSymbols(r.engine.store).length);
|
|
82906
|
-
} catch {
|
|
82907
|
-
strandedRefreshing.delete(r.id);
|
|
82908
|
-
} finally {
|
|
82909
|
-
done();
|
|
82910
|
-
}
|
|
82911
|
-
});
|
|
82912
|
-
});
|
|
82913
|
-
} else {
|
|
82914
|
-
setImmediate(() => {
|
|
82915
|
-
const done = markPass(`stranded-scan:${r.entry.name}`);
|
|
82916
|
-
try {
|
|
82917
|
-
finish(findStrandedSymbols(r.engine.store).length);
|
|
82918
|
-
} catch {
|
|
82919
|
-
strandedRefreshing.delete(r.id);
|
|
82920
|
-
} finally {
|
|
82921
|
-
done();
|
|
82922
|
-
}
|
|
82923
|
-
});
|
|
82924
|
-
}
|
|
82925
|
-
}
|
|
82926
|
-
return hit?.count ?? -1;
|
|
82636
|
+
const roots = git3(["rev-list", "--max-parents=0", "HEAD"]);
|
|
82637
|
+
const count = git3(["rev-list", "--count", "HEAD"]);
|
|
82638
|
+
const rootCommitSha = roots?.split(/\s+/).filter(Boolean).pop();
|
|
82639
|
+
const commitCount = count && /^\d+$/.test(count) ? Number(count) : void 0;
|
|
82640
|
+
return {
|
|
82641
|
+
...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
|
|
82642
|
+
...typeof commitCount === "number" ? { commitCount } : {}
|
|
82927
82643
|
};
|
|
82928
|
-
|
|
82929
|
-
|
|
82930
|
-
|
|
82931
|
-
|
|
82932
|
-
|
|
82933
|
-
|
|
82934
|
-
|
|
82935
|
-
|
|
82936
|
-
|
|
82937
|
-
|
|
82938
|
-
|
|
82939
|
-
|
|
82940
|
-
|
|
82941
|
-
|
|
82942
|
-
|
|
82943
|
-
|
|
82944
|
-
|
|
82945
|
-
|
|
82946
|
-
|
|
82947
|
-
|
|
82948
|
-
|
|
82949
|
-
|
|
82950
|
-
|
|
82951
|
-
|
|
82952
|
-
|
|
82953
|
-
|
|
82954
|
-
|
|
82955
|
-
|
|
82956
|
-
|
|
82957
|
-
|
|
82958
|
-
|
|
82959
|
-
|
|
82960
|
-
|
|
82961
|
-
|
|
82962
|
-
|
|
82963
|
-
|
|
82964
|
-
|
|
82965
|
-
|
|
82966
|
-
|
|
82967
|
-
|
|
82968
|
-
|
|
82969
|
-
const registeredRoot = missPath ? pickOwnerRoot(listWorkspaceEntries().map((e) => e.path), missPath) : null;
|
|
82970
|
-
const wakeRoot = registeredRoot ?? (missPath ? findGitRoot(missPath) : null);
|
|
82971
|
-
if (wakeRoot && attachWorkspace(wakeRoot).attached) {
|
|
82972
|
-
owner = ownerOf(records, missPath) ?? null;
|
|
82973
|
-
if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
|
|
82974
|
-
}
|
|
82975
|
-
}
|
|
82976
|
-
if (!owner) {
|
|
82977
|
-
rootAdopter.noteMiss(pathInBody(body2));
|
|
82978
|
-
return c.json({ error: "no-matching-project" }, 404);
|
|
82979
|
-
}
|
|
82980
|
-
owner.lastHookAt = Date.now();
|
|
82981
|
-
const sub = c.req.path;
|
|
82982
|
-
const done = markPass(`${sub}:${owner.entry.name}`);
|
|
82983
|
-
try {
|
|
82984
|
-
return await owner.webApp.fetch(
|
|
82985
|
-
new Request(`http://local${sub}`, {
|
|
82986
|
-
method: "POST",
|
|
82987
|
-
headers: { "content-type": "application/json" },
|
|
82988
|
-
body: raw2
|
|
82989
|
-
})
|
|
82990
|
-
);
|
|
82991
|
-
} finally {
|
|
82992
|
-
done();
|
|
82644
|
+
}
|
|
82645
|
+
var IGNORE_STARTER = `# .errataignore \u2014 the hard control over what errata may read or send.
|
|
82646
|
+
# Paths, content keywords and tags listed here are dropped from hunks and
|
|
82647
|
+
# narrative before anything leaves this machine, and an ignored path yields no
|
|
82648
|
+
# symbol anchor at all. Scrubbing is best effort; this list is absolute.
|
|
82649
|
+
# A personal list can only add to the organization's list, never weaken it.
|
|
82650
|
+
|
|
82651
|
+
# secrets and environment
|
|
82652
|
+
.env
|
|
82653
|
+
.env.*
|
|
82654
|
+
*.pem
|
|
82655
|
+
*.key
|
|
82656
|
+
*.p12
|
|
82657
|
+
secrets/
|
|
82658
|
+
credentials*
|
|
82659
|
+
|
|
82660
|
+
# fixtures and captured data
|
|
82661
|
+
fixtures/
|
|
82662
|
+
**/testdata/
|
|
82663
|
+
*.dump
|
|
82664
|
+
*.sql.gz
|
|
82665
|
+
`;
|
|
82666
|
+
function applyConsents(profile, config2, c, decidedAt, now = Date.now) {
|
|
82667
|
+
const nextConfig = {
|
|
82668
|
+
...config2,
|
|
82669
|
+
consent: {
|
|
82670
|
+
...config2.consent,
|
|
82671
|
+
sync: c.sync !== "off",
|
|
82672
|
+
telemetry: c.telemetry,
|
|
82673
|
+
contributePackages: c.packages && c.sync !== "off"
|
|
82674
|
+
},
|
|
82675
|
+
onboardedAt: config2.onboardedAt ?? now()
|
|
82676
|
+
};
|
|
82677
|
+
const nextProfile = {
|
|
82678
|
+
...profile,
|
|
82679
|
+
syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
|
|
82680
|
+
linkConsents: {
|
|
82681
|
+
structure: c.structure,
|
|
82682
|
+
mineHistory: c.mineHistory,
|
|
82683
|
+
contributeHistory: c.contributeHistory && c.mineHistory && c.sync !== "off",
|
|
82684
|
+
decidedAt
|
|
82993
82685
|
}
|
|
82994
82686
|
};
|
|
82995
|
-
|
|
82996
|
-
|
|
82997
|
-
|
|
82998
|
-
|
|
82999
|
-
|
|
83000
|
-
|
|
83001
|
-
|
|
83002
|
-
|
|
83003
|
-
|
|
83004
|
-
|
|
83005
|
-
|
|
83006
|
-
(c) => forward(c, (b) => {
|
|
83007
|
-
const loc = b["path"] ?? b["uri"];
|
|
83008
|
-
return typeof loc === "string" ? fileUriToFsPath(loc) : void 0;
|
|
83009
|
-
})
|
|
83010
|
-
);
|
|
83011
|
-
const cwdOf = (b) => typeof b["cwd"] === "string" ? b["cwd"] : void 0;
|
|
83012
|
-
app.post("/api/turn", (c) => forward(c, cwdOf));
|
|
83013
|
-
app.post("/api/message", (c) => forward(c, cwdOf));
|
|
83014
|
-
app.post("/api/session-end", (c) => forward(c, cwdOf));
|
|
83015
|
-
app.post("/api/context-inject", (c) => forward(c, cwdOf));
|
|
83016
|
-
for (const r of records) {
|
|
83017
|
-
mountedIds.add(r.id);
|
|
83018
|
-
app.route(`/ws/${r.id}`, r.webApp);
|
|
83019
|
-
}
|
|
83020
|
-
const dispatchHot = async (c) => {
|
|
83021
|
-
const id = c.req.param("id");
|
|
83022
|
-
const rec = records.find((r) => r.id === id);
|
|
83023
|
-
if (!rec) return c.notFound();
|
|
83024
|
-
const url2 = new URL(c.req.url);
|
|
83025
|
-
url2.pathname = url2.pathname.slice(`/ws/${id}`.length) || "/";
|
|
83026
|
-
const method = c.req.method;
|
|
83027
|
-
const body2 = method === "GET" || method === "HEAD" ? void 0 : await c.req.raw.arrayBuffer();
|
|
83028
|
-
return rec.webApp.fetch(new Request(url2.toString(), { method, headers: c.req.raw.headers, body: body2 }));
|
|
82687
|
+
const prev = profile.linkConsents;
|
|
82688
|
+
const changed = profile.syncConsent !== nextProfile.syncConsent || config2.consent.sync !== nextConfig.consent.sync || config2.consent.telemetry !== nextConfig.consent.telemetry || config2.consent.contributePackages !== nextConfig.consent.contributePackages || !prev || prev.structure !== nextProfile.linkConsents.structure || prev.mineHistory !== nextProfile.linkConsents.mineHistory || prev.contributeHistory !== nextProfile.linkConsents.contributeHistory;
|
|
82689
|
+
return { profile: nextProfile, config: nextConfig, changed };
|
|
82690
|
+
}
|
|
82691
|
+
function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
|
|
82692
|
+
const applied = applyConsents(profile, config2, decision.consents, decision.approvedAt, now);
|
|
82693
|
+
const nextConfig = applied.config;
|
|
82694
|
+
const nextProfile = {
|
|
82695
|
+
...applied.profile,
|
|
82696
|
+
projectId: decision.projectId,
|
|
82697
|
+
projectLocator: decision.locator
|
|
83029
82698
|
};
|
|
83030
|
-
|
|
83031
|
-
|
|
83032
|
-
|
|
83033
|
-
const
|
|
83034
|
-
if (
|
|
83035
|
-
|
|
83036
|
-
|
|
83037
|
-
await rec.engine.stop();
|
|
83038
|
-
} catch (err2) {
|
|
83039
|
-
console.warn(`[errata] retire ${rec.entry.name}: engine.stop failed \u2014 ${err2 instanceof Error ? err2.message : err2}`);
|
|
82699
|
+
delete nextProfile.projectLinkDisabled;
|
|
82700
|
+
let wroteIgnore = false;
|
|
82701
|
+
if (decision.writeIgnoreStarter !== false) {
|
|
82702
|
+
const target = join34(root, ".errataignore");
|
|
82703
|
+
if (!existsSync29(target)) {
|
|
82704
|
+
writeFileSync22(target, IGNORE_STARTER, "utf8");
|
|
82705
|
+
wroteIgnore = true;
|
|
83040
82706
|
}
|
|
83041
|
-
|
|
83042
|
-
|
|
83043
|
-
|
|
83044
|
-
|
|
83045
|
-
|
|
82707
|
+
}
|
|
82708
|
+
return { profile: nextProfile, config: nextConfig, wroteIgnore };
|
|
82709
|
+
}
|
|
82710
|
+
|
|
82711
|
+
// src/multi.ts
|
|
82712
|
+
init_registry();
|
|
82713
|
+
|
|
82714
|
+
// src/adopt.ts
|
|
82715
|
+
init_git_remote();
|
|
82716
|
+
import { existsSync as existsSync30 } from "node:fs";
|
|
82717
|
+
import { dirname as dirname12, join as join35 } from "node:path";
|
|
82718
|
+
function findGitRoot(absPath) {
|
|
82719
|
+
let dir = absPath;
|
|
82720
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
82721
|
+
if (existsSync30(join35(dir, ".git"))) return dir;
|
|
82722
|
+
const parent = dirname12(dir);
|
|
82723
|
+
if (parent === dir) return null;
|
|
82724
|
+
dir = parent;
|
|
82725
|
+
}
|
|
82726
|
+
return null;
|
|
82727
|
+
}
|
|
82728
|
+
var RETRY_COOLDOWN_MS = 15 * 6e4;
|
|
82729
|
+
function createRootAdopter(deps) {
|
|
82730
|
+
const attempted = /* @__PURE__ */ new Map();
|
|
82731
|
+
let chain2 = Promise.resolve();
|
|
82732
|
+
const tryAdopt = async (path2) => {
|
|
82733
|
+
if (!deps.linkingAllowed()) return;
|
|
82734
|
+
const root = findGitRoot(path2);
|
|
82735
|
+
if (!root || deps.isKnownRoot(root)) return;
|
|
82736
|
+
const last = attempted.get(root);
|
|
83046
82737
|
const now = Date.now();
|
|
83047
|
-
|
|
83048
|
-
|
|
83049
|
-
|
|
83050
|
-
|
|
83051
|
-
|
|
83052
|
-
|
|
83053
|
-
|
|
83054
|
-
|
|
83055
|
-
}
|
|
82738
|
+
if (last !== void 0 && now - last < RETRY_COOLDOWN_MS) return;
|
|
82739
|
+
attempted.set(root, now);
|
|
82740
|
+
if (!resolveGitDir(root)) return;
|
|
82741
|
+
const locator = detectRepoLocator(root);
|
|
82742
|
+
if (!locator) return;
|
|
82743
|
+
const claimed = await deps.lookup(locator);
|
|
82744
|
+
if (!claimed) return;
|
|
82745
|
+
await deps.adopt(root);
|
|
82746
|
+
deps.log(`[errata] adopted ${root} \u2014 ${locator} is claimed by your org (project ${claimed.id})`);
|
|
83056
82747
|
};
|
|
83057
|
-
|
|
83058
|
-
|
|
83059
|
-
|
|
83060
|
-
|
|
83061
|
-
|
|
83062
|
-
|
|
83063
|
-
let entries2;
|
|
83064
|
-
try {
|
|
83065
|
-
entries2 = listWorkspaceEntries();
|
|
83066
|
-
} catch {
|
|
83067
|
-
return;
|
|
83068
|
-
}
|
|
83069
|
-
const candidates = codexWakeCandidates({
|
|
83070
|
-
entries: entries2,
|
|
83071
|
-
liveRoots: new Set(records.map((r) => normPath(r.root))),
|
|
83072
|
-
normPath,
|
|
83073
|
-
sinceMs: Date.now() - lookback,
|
|
83074
|
-
disabled: process.env["ERRATA_CODEX_WAKE_DISABLE"] === "1"
|
|
83075
|
-
});
|
|
83076
|
-
for (const c of candidates) {
|
|
83077
|
-
if (attachWorkspace(c.path).attached) {
|
|
83078
|
-
console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
|
|
83079
|
-
}
|
|
83080
|
-
}
|
|
82748
|
+
return {
|
|
82749
|
+
noteMiss(path2) {
|
|
82750
|
+
if (!path2) return;
|
|
82751
|
+
chain2 = chain2.then(() => tryAdopt(path2)).catch(() => void 0);
|
|
82752
|
+
},
|
|
82753
|
+
idle: () => chain2
|
|
83081
82754
|
};
|
|
83082
|
-
|
|
83083
|
-
|
|
83084
|
-
|
|
83085
|
-
|
|
83086
|
-
const
|
|
83087
|
-
|
|
83088
|
-
|
|
83089
|
-
|
|
83090
|
-
|
|
83091
|
-
|
|
82755
|
+
}
|
|
82756
|
+
|
|
82757
|
+
// src/health-sentinel.ts
|
|
82758
|
+
function numEnv4(key, fallback) {
|
|
82759
|
+
const n = Number(process.env[key]);
|
|
82760
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
82761
|
+
}
|
|
82762
|
+
var SENTINEL_TICK_MS = numEnv4("ERRATA_SENTINEL_TICK_MS", 3e4);
|
|
82763
|
+
var SENTINEL_WINDOW_TICKS = numEnv4("ERRATA_SENTINEL_WINDOW_TICKS", 6);
|
|
82764
|
+
var SENTINEL_BURN_CORES = numEnv4("ERRATA_SENTINEL_BURN_CORES", 1.2);
|
|
82765
|
+
var SENTINEL_STORM_RATE = numEnv4("ERRATA_SENTINEL_STORM_RATE", 1e3);
|
|
82766
|
+
var SENTINEL_STORM_TICKS = numEnv4("ERRATA_SENTINEL_STORM_TICKS", 4);
|
|
82767
|
+
var SENTINEL_RENOTIFY_MS = numEnv4("ERRATA_SENTINEL_RENOTIFY_MS", 18e5);
|
|
82768
|
+
var SENTINEL_CALM_TICKS = numEnv4("ERRATA_SENTINEL_CALM_TICKS", 2);
|
|
82769
|
+
var SENTINEL_TOAST_COOLDOWN_MS = numEnv4("ERRATA_SENTINEL_TOAST_COOLDOWN_MS", 6e5);
|
|
82770
|
+
function createHealthSentinel(deps = {}) {
|
|
82771
|
+
const now = deps.now ?? Date.now;
|
|
82772
|
+
const cpuUsage = deps.cpuUsage ?? (() => process.cpuUsage());
|
|
82773
|
+
const readCensus = deps.readCensus ?? (() => {
|
|
82774
|
+
const c = watchCensus();
|
|
82775
|
+
return {
|
|
82776
|
+
dispatched: c.dispatched,
|
|
82777
|
+
logged: c.logged,
|
|
82778
|
+
topOffender: c.topOffender ? { path: c.topOffender.path, count: c.topOffender.count } : null
|
|
82779
|
+
};
|
|
82780
|
+
});
|
|
82781
|
+
const activePasses = deps.activePasses ?? (() => passState().active.map((p) => p.name));
|
|
82782
|
+
const rssMb = deps.rssMb ?? (() => Math.round(process.memoryUsage.rss() / 1048576));
|
|
82783
|
+
const toast = deps.notify ?? ((body2, key) => notifyOpsEvent("daemon-distress", body2, { key }));
|
|
82784
|
+
let prevCpu = cpuUsage();
|
|
82785
|
+
let prevTs = now();
|
|
82786
|
+
let prevDispatched = null;
|
|
82787
|
+
let prevLogged = 0;
|
|
82788
|
+
const coreSamples = [];
|
|
82789
|
+
let stormTicks = 0;
|
|
82790
|
+
let calmTicks = 0;
|
|
82791
|
+
let distress = null;
|
|
82792
|
+
let lastNotifiedAt = 0;
|
|
82793
|
+
const lastToastByKind = /* @__PURE__ */ new Map();
|
|
82794
|
+
const attribution = () => {
|
|
82795
|
+
const parts2 = [];
|
|
82796
|
+
const passes = activePasses();
|
|
82797
|
+
if (passes.length > 0) parts2.push(`active pass: ${passes.join(", ")}`);
|
|
82798
|
+
const c = readCensus();
|
|
82799
|
+
if (c.topOffender) parts2.push(`hottest watch path: ${c.topOffender.path}`);
|
|
82800
|
+
parts2.push(`rss ${rssMb()}MB`);
|
|
82801
|
+
return parts2.join(" \xB7 ");
|
|
82802
|
+
};
|
|
82803
|
+
const maybeToast = (d, t) => {
|
|
82804
|
+
if (t - (lastToastByKind.get(d.kind) ?? 0) < SENTINEL_TOAST_COOLDOWN_MS) return;
|
|
82805
|
+
lastToastByKind.set(d.kind, t);
|
|
82806
|
+
toast(d.headline, d.kind);
|
|
82807
|
+
lastNotifiedAt = t;
|
|
82808
|
+
};
|
|
82809
|
+
const transition = (next) => {
|
|
82810
|
+
const t = now();
|
|
82811
|
+
if (next && !distress) {
|
|
82812
|
+
distress = next;
|
|
82813
|
+
deps.ledger?.("enter", next);
|
|
82814
|
+
maybeToast(next, t);
|
|
82815
|
+
deps.onChange?.(next);
|
|
82816
|
+
} else if (next && distress) {
|
|
82817
|
+
distress.body = next.body;
|
|
82818
|
+
distress.headline = next.headline;
|
|
82819
|
+
if (t - lastNotifiedAt >= SENTINEL_RENOTIFY_MS) maybeToast(distress, t);
|
|
82820
|
+
} else if (!next && distress) {
|
|
82821
|
+
deps.ledger?.("clear", distress);
|
|
82822
|
+
distress = null;
|
|
82823
|
+
deps.onChange?.(null);
|
|
83092
82824
|
}
|
|
83093
|
-
|
|
83094
|
-
|
|
83095
|
-
|
|
83096
|
-
|
|
83097
|
-
|
|
83098
|
-
|
|
83099
|
-
|
|
83100
|
-
|
|
83101
|
-
|
|
83102
|
-
|
|
83103
|
-
|
|
83104
|
-
|
|
83105
|
-
|
|
83106
|
-
|
|
83107
|
-
|
|
83108
|
-
|
|
83109
|
-
|
|
82825
|
+
};
|
|
82826
|
+
const evaluate = () => {
|
|
82827
|
+
const t = now();
|
|
82828
|
+
const wallMs = Math.max(1, t - prevTs);
|
|
82829
|
+
const cpu = cpuUsage();
|
|
82830
|
+
const cores = (cpu.user + cpu.system - prevCpu.user - prevCpu.system) / 1e3 / wallMs;
|
|
82831
|
+
prevCpu = cpu;
|
|
82832
|
+
prevTs = t;
|
|
82833
|
+
coreSamples.push(cores);
|
|
82834
|
+
if (coreSamples.length > SENTINEL_WINDOW_TICKS) coreSamples.shift();
|
|
82835
|
+
const c = readCensus();
|
|
82836
|
+
const dispatchRate = prevDispatched === null ? 0 : (c.dispatched - prevDispatched) / wallMs * 1e3;
|
|
82837
|
+
const survived = c.logged - prevLogged;
|
|
82838
|
+
prevDispatched = c.dispatched;
|
|
82839
|
+
prevLogged = c.logged;
|
|
82840
|
+
stormTicks = dispatchRate > SENTINEL_STORM_RATE && survived === 0 ? stormTicks + 1 : 0;
|
|
82841
|
+
let wanted = null;
|
|
82842
|
+
if (stormTicks >= SENTINEL_STORM_TICKS) {
|
|
82843
|
+
wanted = {
|
|
82844
|
+
kind: "watch-storm",
|
|
82845
|
+
headline: `errata's file watcher is stuck busy (~${Math.round(dispatchRate)} events/sec with nothing changing) \u2014 run \`errata status\` for detail`,
|
|
82846
|
+
body: `filesystem watcher is processing ~${Math.round(dispatchRate)} events/sec with NOTHING surviving \u2014 a wedged or ignored-tree watch is burning CPU for no signal \xB7 ${attribution()}`,
|
|
82847
|
+
sinceTs: distress?.kind === "watch-storm" ? distress.sinceTs : t
|
|
82848
|
+
};
|
|
82849
|
+
} else {
|
|
82850
|
+
const windowFull = coreSamples.length >= SENTINEL_WINDOW_TICKS;
|
|
82851
|
+
const avgCores = coreSamples.reduce((a, b) => a + b, 0) / Math.max(1, coreSamples.length);
|
|
82852
|
+
if (windowFull && avgCores >= SENTINEL_BURN_CORES && cores >= SENTINEL_BURN_CORES && activePasses().length === 0) {
|
|
82853
|
+
wanted = {
|
|
82854
|
+
kind: "cpu-burn",
|
|
82855
|
+
headline: `errata is using more CPU than expected (${avgCores.toFixed(1)} cores with no work running) \u2014 run \`errata status\` for detail`,
|
|
82856
|
+
body: `sustained CPU with no pass running \u2014 avg ${avgCores.toFixed(1)} cores \xB7 ${attribution()}`,
|
|
82857
|
+
sinceTs: distress?.kind === "cpu-burn" ? distress.sinceTs : t
|
|
82858
|
+
};
|
|
83110
82859
|
}
|
|
83111
82860
|
}
|
|
83112
|
-
if (
|
|
83113
|
-
|
|
82861
|
+
if (wanted) {
|
|
82862
|
+
calmTicks = 0;
|
|
82863
|
+
transition(wanted);
|
|
82864
|
+
} else if (distress) {
|
|
82865
|
+
calmTicks++;
|
|
82866
|
+
if (calmTicks >= SENTINEL_CALM_TICKS) {
|
|
82867
|
+
calmTicks = 0;
|
|
82868
|
+
transition(null);
|
|
82869
|
+
}
|
|
83114
82870
|
}
|
|
83115
|
-
return out2;
|
|
83116
82871
|
};
|
|
83117
|
-
const
|
|
83118
|
-
baseUrl = `http://127.0.0.1:${port}`;
|
|
83119
|
-
try {
|
|
83120
|
-
ensureDir(globalDir());
|
|
83121
|
-
writeFileSync22(
|
|
83122
|
-
lockPath,
|
|
83123
|
-
JSON.stringify({ pid: process.pid, webUiUrl: baseUrl, startedAt: Date.now() }),
|
|
83124
|
-
"utf8"
|
|
83125
|
-
);
|
|
83126
|
-
} catch {
|
|
83127
|
-
}
|
|
83128
|
-
for (const r of records) {
|
|
82872
|
+
const interval = deps.scheduleTicks === false ? null : setInterval(() => {
|
|
83129
82873
|
try {
|
|
83130
|
-
|
|
83131
|
-
r.engine.paths.daemonLock,
|
|
83132
|
-
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${r.id}`, startedAt: Date.now() }),
|
|
83133
|
-
"utf8"
|
|
83134
|
-
);
|
|
83135
|
-
registerWorkspace(r.engine.profile, r.root);
|
|
82874
|
+
evaluate();
|
|
83136
82875
|
} catch {
|
|
83137
82876
|
}
|
|
83138
|
-
}
|
|
83139
|
-
|
|
83140
|
-
|
|
83141
|
-
|
|
83142
|
-
|
|
83143
|
-
|
|
83144
|
-
|
|
83145
|
-
|
|
83146
|
-
|
|
83147
|
-
|
|
83148
|
-
|
|
83149
|
-
|
|
83150
|
-
|
|
83151
|
-
|
|
83152
|
-
|
|
83153
|
-
|
|
83154
|
-
|
|
83155
|
-
|
|
83156
|
-
|
|
83157
|
-
|
|
83158
|
-
|
|
83159
|
-
|
|
83160
|
-
|
|
83161
|
-
|
|
82877
|
+
}, SENTINEL_TICK_MS);
|
|
82878
|
+
interval?.unref?.();
|
|
82879
|
+
return {
|
|
82880
|
+
current: () => distress ? { ...distress } : null,
|
|
82881
|
+
tick: evaluate,
|
|
82882
|
+
stop: () => {
|
|
82883
|
+
if (interval) clearInterval(interval);
|
|
82884
|
+
}
|
|
82885
|
+
};
|
|
82886
|
+
}
|
|
82887
|
+
|
|
82888
|
+
// src/codex-wake.ts
|
|
82889
|
+
var CODEX_WAKE_LOOKBACK_MS = 10 * 6e4;
|
|
82890
|
+
var CODEX_WAKE_BOOT_LOOKBACK_MS = 24 * 60 * 6e4;
|
|
82891
|
+
function codexWakeCandidates(input) {
|
|
82892
|
+
if (input.disabled) return [];
|
|
82893
|
+
const find = input.find ?? ((cwd, since) => codexRolloutsForCwd(cwd, { sinceMs: since, limit: 1 }).length + geminiTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length + opencodeTranscriptsForCwd(cwd, { sinceMs: since, limit: 1 }).length);
|
|
82894
|
+
const out2 = [];
|
|
82895
|
+
for (const entry of input.entries) {
|
|
82896
|
+
if (input.liveRoots.has(input.normPath(entry.path))) continue;
|
|
82897
|
+
let n = 0;
|
|
82898
|
+
try {
|
|
82899
|
+
n = find(entry.path, input.sinceMs);
|
|
82900
|
+
} catch {
|
|
82901
|
+
continue;
|
|
82902
|
+
}
|
|
82903
|
+
if (n > 0) out2.push({ name: entry.name, path: entry.path });
|
|
82904
|
+
}
|
|
82905
|
+
return out2;
|
|
82906
|
+
}
|
|
82907
|
+
|
|
82908
|
+
// src/multi.ts
|
|
82909
|
+
var projectSymbolSalts = /* @__PURE__ */ new Map();
|
|
82910
|
+
async function resolveProjectSymbolSalt(client, projectId) {
|
|
82911
|
+
const cached2 = projectSymbolSalts.get(projectId);
|
|
82912
|
+
if (cached2) return { projectId, salt: cached2 };
|
|
82913
|
+
const { salt } = await client.projectSymbolSalt(projectId);
|
|
82914
|
+
projectSymbolSalts.set(projectId, salt);
|
|
82915
|
+
return { projectId, salt };
|
|
82916
|
+
}
|
|
82917
|
+
function normPath(p) {
|
|
82918
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
82919
|
+
}
|
|
82920
|
+
function contextKey(p) {
|
|
82921
|
+
return `${[...p.stack].sort().join(",")}|${p.domains[0] ?? p.languages[0] ?? "?"}`;
|
|
82922
|
+
}
|
|
82923
|
+
function pickOwnerRoot(roots, absPath) {
|
|
82924
|
+
if (!absPath) return null;
|
|
82925
|
+
const n = normPath(absPath);
|
|
82926
|
+
let best = null;
|
|
82927
|
+
let bestLen = -1;
|
|
82928
|
+
for (const root of roots) {
|
|
82929
|
+
const r = normPath(root);
|
|
82930
|
+
if ((n === r || n.startsWith(r + "/")) && r.length > bestLen) {
|
|
82931
|
+
best = root;
|
|
82932
|
+
bestLen = r.length;
|
|
82933
|
+
}
|
|
82934
|
+
}
|
|
82935
|
+
return best;
|
|
82936
|
+
}
|
|
82937
|
+
function ownerOf(records, absPath) {
|
|
82938
|
+
const root = pickOwnerRoot(records.map((r) => r.root), absPath);
|
|
82939
|
+
return root == null ? null : records.find((r) => r.root === root) ?? null;
|
|
82940
|
+
}
|
|
82941
|
+
async function startMultiDaemon(opts = {}) {
|
|
82942
|
+
const lockPath = globalDaemonLock();
|
|
82943
|
+
if (opts.enforceSingleton !== false && isDaemonAlive(lockPath)) {
|
|
82944
|
+
throw new Error(
|
|
82945
|
+
`an errata daemon is already running (lock: ${lockPath}). Run \`errata stop\` first, or attach to the running one.`
|
|
82946
|
+
);
|
|
82947
|
+
}
|
|
82948
|
+
const cfg = loadConfig();
|
|
82949
|
+
const CLOUD_TIMEOUT_MS = 12e4;
|
|
82950
|
+
const initialCloudUrl = normalizeCloudUrl(opts.cloudUrl ?? cfg.cloudUrl);
|
|
82951
|
+
const initialCfg = { ...cfg, cloudUrl: initialCloudUrl };
|
|
82952
|
+
if (opts.apiKey) {
|
|
82953
|
+
initialCfg.apiKey = opts.apiKey;
|
|
82954
|
+
initialCfg.accessToken = null;
|
|
82955
|
+
initialCfg.refreshToken = null;
|
|
82956
|
+
initialCfg.tokenEndpoint = null;
|
|
82957
|
+
initialCfg.oauthClientId = null;
|
|
82958
|
+
}
|
|
82959
|
+
const cloud = authedCloudClient(initialCfg, { timeoutMs: CLOUD_TIMEOUT_MS, daemonVersion: DAEMON_VERSION });
|
|
82960
|
+
const cloudNow = () => {
|
|
83162
82961
|
const c = loadConfig();
|
|
83163
|
-
|
|
83164
|
-
const
|
|
83165
|
-
|
|
83166
|
-
|
|
83167
|
-
|
|
83168
|
-
|
|
83169
|
-
|
|
83170
|
-
|
|
83171
|
-
|
|
83172
|
-
|
|
83173
|
-
|
|
83174
|
-
|
|
83175
|
-
|
|
83176
|
-
|
|
83177
|
-
|
|
83178
|
-
|
|
82962
|
+
const baseUrl2 = normalizeCloudUrl(opts.cloudUrl ?? c.cloudUrl);
|
|
82963
|
+
const current = { ...c, cloudUrl: baseUrl2 };
|
|
82964
|
+
if (opts.apiKey) {
|
|
82965
|
+
current.apiKey = opts.apiKey;
|
|
82966
|
+
current.accessToken = null;
|
|
82967
|
+
current.refreshToken = null;
|
|
82968
|
+
current.tokenEndpoint = null;
|
|
82969
|
+
current.oauthClientId = null;
|
|
82970
|
+
}
|
|
82971
|
+
return authedCloudClient(current, { timeoutMs: CLOUD_TIMEOUT_MS, daemonVersion: DAEMON_VERSION });
|
|
82972
|
+
};
|
|
82973
|
+
const identityFor = (entry) => {
|
|
82974
|
+
if (opts.apiKey) {
|
|
82975
|
+
return {
|
|
82976
|
+
profileName: null,
|
|
82977
|
+
profile: null,
|
|
82978
|
+
rule: "env-config-path",
|
|
82979
|
+
invalid: false,
|
|
82980
|
+
reason: "daemon started with an explicit --api-key (process-pinned)"
|
|
82981
|
+
};
|
|
82982
|
+
}
|
|
82983
|
+
return resolveWorkspaceIdentity(loadConfig(), entry);
|
|
82984
|
+
};
|
|
82985
|
+
const identityBlocks = /* @__PURE__ */ new Map();
|
|
82986
|
+
const noteIdentityBlock = (wsId, wsName, identity) => {
|
|
82987
|
+
if (!identityBlocks.has(wsId)) {
|
|
82988
|
+
identityBlocks.set(wsId, {
|
|
82989
|
+
workspace: wsName,
|
|
82990
|
+
profileName: identity.profileName,
|
|
82991
|
+
reason: identity.reason,
|
|
82992
|
+
since: Date.now()
|
|
83179
82993
|
});
|
|
83180
|
-
|
|
83181
|
-
r.engine.markContextDirty();
|
|
83182
|
-
console.log(
|
|
83183
|
-
`[errata] primed ${r.engine.profile.name} from project \u2014 ${hyd.nodes} priors, ${hyd.edges} links`
|
|
83184
|
-
);
|
|
83185
|
-
} else if (hyd.reason === "error") {
|
|
83186
|
-
console.warn(`[errata] project prime failed for ${r.engine.profile.name}: ${hyd.detail}`);
|
|
83187
|
-
}
|
|
83188
|
-
if (r.engine.profile.projectId) {
|
|
83189
|
-
try {
|
|
83190
|
-
const { salt } = await resolveProjectSymbolSalt(client, r.engine.profile.projectId);
|
|
83191
|
-
const re = await reanchorProject({
|
|
83192
|
-
root: r.root,
|
|
83193
|
-
profile: r.engine.profile,
|
|
83194
|
-
store: r.engine.store,
|
|
83195
|
-
cloud: client,
|
|
83196
|
-
salt
|
|
83197
|
-
});
|
|
83198
|
-
if (re.reanchored) {
|
|
83199
|
-
r.engine.markContextDirty();
|
|
83200
|
-
console.log(
|
|
83201
|
-
`[errata] reanchored ${r.engine.profile.name} \u2014 ${re.anchors} anchors onto local code (${re.dropped} unresolved, dropped)`
|
|
83202
|
-
);
|
|
83203
|
-
} else if (re.reason === "error") {
|
|
83204
|
-
console.warn(`[errata] project reanchor failed for ${r.engine.profile.name}: ${re.detail}`);
|
|
83205
|
-
}
|
|
83206
|
-
if (r.engine.profile.linkConsents && r.engine.profile.projectHydratedAt && !historyInFlight.has(r.root)) {
|
|
83207
|
-
historyInFlight.add(r.root);
|
|
83208
|
-
const engine = r.engine;
|
|
83209
|
-
const root = r.root;
|
|
83210
|
-
const anchorIndex = {
|
|
83211
|
-
nodesByRelPath: (paths) => {
|
|
83212
|
-
if (!records.some((x) => x.root === root)) {
|
|
83213
|
-
throw new Error("workspace detached mid-walk; the watermark holds and the next pass resumes");
|
|
83214
|
-
}
|
|
83215
|
-
return engine.store.nodesByRelPath(paths);
|
|
83216
|
-
}
|
|
83217
|
-
};
|
|
83218
|
-
void startHistoryStage(root, engine.profile, client, {
|
|
83219
|
-
anchors: { index: anchorIndex, salt },
|
|
83220
|
-
saveProfile: (p) => {
|
|
83221
|
-
Object.assign(engine.profile, p);
|
|
83222
|
-
saveProfile(root, engine.profile);
|
|
83223
|
-
},
|
|
83224
|
-
log: (line) => console.warn(line)
|
|
83225
|
-
}).then((stage2) => {
|
|
83226
|
-
if (stage2.profile !== engine.profile) {
|
|
83227
|
-
Object.assign(engine.profile, stage2.profile);
|
|
83228
|
-
saveProfile(root, engine.profile);
|
|
83229
|
-
}
|
|
83230
|
-
const state = engine.profile.historyStage?.state ?? "consent-pending";
|
|
83231
|
-
const c2 = engine.profile.historyStage?.counts;
|
|
83232
|
-
console.log(
|
|
83233
|
-
`[errata] history ${state} for ${engine.profile.name}` + (c2 ? ` \xB7 ${c2.commitsWalked ?? 0} commits walked \xB7 ${c2.changesExtracted ?? 0} changes extracted \xB7 ${c2.pairs ?? 0} pairs \xB7 ${c2.claims ?? 0} claims \xB7 ${c2.anchors ?? 0} anchors \xB7 ${c2.packages ?? 0} packages` : "") + (engine.profile.historyStage?.lastError ? ` \xB7 ${engine.profile.historyStage.lastError}` : "")
|
|
83234
|
-
);
|
|
83235
|
-
}).catch((err2) => {
|
|
83236
|
-
console.warn(`[errata] history walk failed for ${engine.profile.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83237
|
-
}).finally(() => historyInFlight.delete(root));
|
|
83238
|
-
}
|
|
83239
|
-
} catch {
|
|
83240
|
-
}
|
|
83241
|
-
}
|
|
82994
|
+
console.error(`[identity] HOLDING cloud traffic for workspace "${wsName}": ${identity.reason}`);
|
|
83242
82995
|
}
|
|
83243
82996
|
};
|
|
83244
|
-
const
|
|
83245
|
-
|
|
83246
|
-
|
|
83247
|
-
|
|
83248
|
-
|
|
83249
|
-
|
|
83250
|
-
|
|
83251
|
-
|
|
83252
|
-
|
|
83253
|
-
|
|
83254
|
-
runLockfilePass({
|
|
83255
|
-
root: r.root,
|
|
83256
|
-
store: r.engine.store,
|
|
83257
|
-
workspaceId: r.id,
|
|
83258
|
-
languages: r.engine.profile.languages,
|
|
83259
|
-
log: (m) => console.log(m)
|
|
83260
|
-
})
|
|
83261
|
-
);
|
|
83262
|
-
} catch (err2) {
|
|
83263
|
-
console.warn(`[errata] package index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83264
|
-
} finally {
|
|
83265
|
-
done();
|
|
83266
|
-
}
|
|
82997
|
+
const clearIdentityBlock = (wsId) => {
|
|
82998
|
+
if (identityBlocks.delete(wsId)) console.log(`[identity] workspace unblocked \u2014 spooled capture drains on this pass`);
|
|
82999
|
+
};
|
|
83000
|
+
const projectHolds = /* @__PURE__ */ new Map();
|
|
83001
|
+
const noteProjectHold = (wsId, wsName, reason) => {
|
|
83002
|
+
if (!projectHolds.has(wsId)) {
|
|
83003
|
+
projectHolds.set(wsId, { workspace: wsName, reason, since: Date.now() });
|
|
83004
|
+
console.error(
|
|
83005
|
+
`[project] HOLDING cloud push for workspace "${wsName}": cannot prove its project \u2014 ${reason}. Shipping without the assertion would publish project-scoped knowledge to the whole org; the batch stays pending instead.`
|
|
83006
|
+
);
|
|
83267
83007
|
}
|
|
83268
|
-
return out2;
|
|
83269
83008
|
};
|
|
83270
|
-
const
|
|
83271
|
-
|
|
83272
|
-
|
|
83273
|
-
|
|
83274
|
-
|
|
83275
|
-
|
|
83276
|
-
|
|
83277
|
-
|
|
83278
|
-
|
|
83279
|
-
|
|
83280
|
-
|
|
83281
|
-
|
|
83282
|
-
|
|
83283
|
-
|
|
83284
|
-
|
|
83285
|
-
|
|
83286
|
-
|
|
83287
|
-
|
|
83288
|
-
|
|
83289
|
-
|
|
83290
|
-
|
|
83291
|
-
|
|
83292
|
-
|
|
83293
|
-
|
|
83294
|
-
|
|
83295
|
-
|
|
83296
|
-
|
|
83297
|
-
|
|
83298
|
-
console.warn(
|
|
83299
|
-
`[errata] semantic maintenance failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
|
|
83300
|
-
);
|
|
83301
|
-
} finally {
|
|
83302
|
-
done();
|
|
83303
|
-
}
|
|
83009
|
+
const clearProjectHold = (wsId) => {
|
|
83010
|
+
if (projectHolds.delete(wsId)) console.log(`[project] project assertion restored \u2014 held contributions drain on this pass`);
|
|
83011
|
+
};
|
|
83012
|
+
const cloudForIdentity = (identity) => {
|
|
83013
|
+
if (identity.invalid) return blockedCloudClient(identity.reason);
|
|
83014
|
+
if (identity.rule === "workspace-binding" && identity.profileName) {
|
|
83015
|
+
return authedCloudClientForProfile(loadConfig(), identity.profileName, {
|
|
83016
|
+
timeoutMs: CLOUD_TIMEOUT_MS,
|
|
83017
|
+
daemonVersion: DAEMON_VERSION
|
|
83018
|
+
});
|
|
83019
|
+
}
|
|
83020
|
+
return cloudNow();
|
|
83021
|
+
};
|
|
83022
|
+
const sharedDbPath = sharedStorePath();
|
|
83023
|
+
ensureParent(sharedDbPath);
|
|
83024
|
+
const sharedStore = openGraphStore({ path: sharedDbPath });
|
|
83025
|
+
const bundledRun = import.meta.url.endsWith(".mjs");
|
|
83026
|
+
const consolidateWorker = bundledRun && sharedDbPath !== ":memory:" ? new ConsolidateWorker() : null;
|
|
83027
|
+
startLoopLagMonitor();
|
|
83028
|
+
let baseUrl = "";
|
|
83029
|
+
const records = [];
|
|
83030
|
+
const mountedIds = /* @__PURE__ */ new Set();
|
|
83031
|
+
const machineDominantLanguage = () => {
|
|
83032
|
+
const langCounts = /* @__PURE__ */ new Map();
|
|
83033
|
+
for (const r of records) {
|
|
83034
|
+
for (const l of r.engine.profile.languages ?? []) {
|
|
83035
|
+
const k = String(l).trim().toLowerCase();
|
|
83036
|
+
if (k) langCounts.set(k, (langCounts.get(k) ?? 0) + 1);
|
|
83304
83037
|
}
|
|
83305
|
-
|
|
83306
|
-
|
|
83307
|
-
|
|
83308
|
-
|
|
83309
|
-
|
|
83310
|
-
|
|
83311
|
-
|
|
83312
|
-
|
|
83313
|
-
|
|
83314
|
-
|
|
83315
|
-
} catch (err2) {
|
|
83316
|
-
console.warn(`[errata] nightly failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83317
|
-
} finally {
|
|
83318
|
-
done();
|
|
83319
|
-
}
|
|
83038
|
+
}
|
|
83039
|
+
return [...langCounts.entries()].sort(
|
|
83040
|
+
(a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)
|
|
83041
|
+
)[0]?.[0];
|
|
83042
|
+
};
|
|
83043
|
+
const updatePoller = opts.updateCheck ? startUpdatePoller({
|
|
83044
|
+
channel: loadConfig().updateChannel,
|
|
83045
|
+
onChange: (pending) => {
|
|
83046
|
+
if (pending) {
|
|
83047
|
+
console.log(`\u26A1 errata update available: ${pending.current} \u2192 ${pending.latest} \u2014 run: errata update`);
|
|
83320
83048
|
}
|
|
83321
|
-
|
|
83322
|
-
}
|
|
83323
|
-
|
|
83324
|
-
|
|
83049
|
+
for (const r of records) r.engine.markContextDirty();
|
|
83050
|
+
}
|
|
83051
|
+
}) : null;
|
|
83052
|
+
const healthSentinel = createHealthSentinel({
|
|
83053
|
+
onChange: () => {
|
|
83054
|
+
for (const r of records) r.engine.markContextDirty();
|
|
83325
83055
|
},
|
|
83326
|
-
|
|
83327
|
-
const
|
|
83328
|
-
|
|
83329
|
-
|
|
83330
|
-
|
|
83331
|
-
|
|
83332
|
-
|
|
83333
|
-
const locator = profile.repoLocator;
|
|
83334
|
-
if (!locator || aliases.length === 0) continue;
|
|
83335
|
-
try {
|
|
83336
|
-
for (const alias of aliases) {
|
|
83337
|
-
if (repoBasename(alias) !== repoBasename(locator)) {
|
|
83338
|
-
console.log(
|
|
83339
|
-
`[errata] project alias NOT auto-migrated for ${r.entry.name} (${alias} \u2192 ${locator}: repo name differs \u2014 a fork/repoint, not a rename). Run scripts/migrate-project-alias.ts if this really is the same repo.`
|
|
83340
|
-
);
|
|
83341
|
-
continue;
|
|
83342
|
-
}
|
|
83343
|
-
const m = migrateProjectAlias(sharedStore, { from: alias, to: locator, ts });
|
|
83344
|
-
if (m.nodesRewritten || m.edgesRewritten) {
|
|
83345
|
-
console.log(
|
|
83346
|
-
`[errata] project alias migrated for ${r.entry.name}: ${alias} \u2192 ${locator} (${m.nodesRewritten} nodes, ${m.edgesRewritten} edges)`
|
|
83347
|
-
);
|
|
83348
|
-
}
|
|
83349
|
-
}
|
|
83350
|
-
delete profile.repoLocatorAliases;
|
|
83351
|
-
const onDisk = loadProfile(r.root);
|
|
83352
|
-
if (onDisk) {
|
|
83353
|
-
delete onDisk.repoLocatorAliases;
|
|
83354
|
-
saveProfile(r.root, onDisk);
|
|
83355
|
-
}
|
|
83356
|
-
} catch (err2) {
|
|
83357
|
-
console.warn(`[errata] project-alias migration failed for ${r.entry.name} (kept for next pass): ${err2 instanceof Error ? err2.message : err2}`);
|
|
83358
|
-
}
|
|
83056
|
+
ledger: (event, d) => {
|
|
83057
|
+
const dir = records[0]?.engine.paths.configDir;
|
|
83058
|
+
if (dir) {
|
|
83059
|
+
appendPassLedger(dir, "health-sentinel", 0, {
|
|
83060
|
+
[`distress:${event}:${d.kind}`]: 1,
|
|
83061
|
+
distressSinceTs: d.sinceTs
|
|
83062
|
+
});
|
|
83359
83063
|
}
|
|
83360
|
-
|
|
83361
|
-
|
|
83362
|
-
|
|
83363
|
-
|
|
83364
|
-
|
|
83365
|
-
|
|
83366
|
-
|
|
83367
|
-
|
|
83368
|
-
|
|
83369
|
-
|
|
83370
|
-
|
|
83371
|
-
|
|
83372
|
-
|
|
83373
|
-
|
|
83374
|
-
|
|
83375
|
-
|
|
83376
|
-
|
|
83377
|
-
|
|
83378
|
-
|
|
83379
|
-
|
|
83380
|
-
|
|
83381
|
-
|
|
83382
|
-
|
|
83383
|
-
|
|
83064
|
+
}
|
|
83065
|
+
});
|
|
83066
|
+
let serverPending = null;
|
|
83067
|
+
const boundaryListeners = [];
|
|
83068
|
+
let boundaryFlushing = false;
|
|
83069
|
+
let settleRunning = false;
|
|
83070
|
+
let boundaryFlushStartedAt = 0;
|
|
83071
|
+
let boundaryFlushStage = "";
|
|
83072
|
+
let boundaryFlushUnits = 0;
|
|
83073
|
+
let boundaryFlushLastAdvanceAt = 0;
|
|
83074
|
+
let boundaryFlushBatches = 0;
|
|
83075
|
+
const beginFlush = (stage2) => {
|
|
83076
|
+
boundaryFlushing = true;
|
|
83077
|
+
boundaryFlushStartedAt = Date.now();
|
|
83078
|
+
boundaryFlushLastAdvanceAt = Date.now();
|
|
83079
|
+
boundaryFlushUnits = 0;
|
|
83080
|
+
boundaryFlushBatches = 0;
|
|
83081
|
+
boundaryFlushItems = 0;
|
|
83082
|
+
boundaryFlushDetail = "";
|
|
83083
|
+
boundaryFlushStage = stage2;
|
|
83084
|
+
};
|
|
83085
|
+
const flushStage = (stage2) => {
|
|
83086
|
+
boundaryFlushStage = stage2;
|
|
83087
|
+
};
|
|
83088
|
+
let boundaryFlushItems = 0;
|
|
83089
|
+
let boundaryFlushDetail = "";
|
|
83090
|
+
const noteFlushProgress = (units, items = 0, detail = "") => {
|
|
83091
|
+
boundaryFlushBatches++;
|
|
83092
|
+
boundaryFlushUnits += Math.max(0, units);
|
|
83093
|
+
boundaryFlushItems += Math.max(0, items);
|
|
83094
|
+
if (detail) boundaryFlushDetail = detail;
|
|
83095
|
+
if (units > 0 || items > 0) boundaryFlushLastAdvanceAt = Date.now();
|
|
83096
|
+
};
|
|
83097
|
+
const flushSnapshot = () => ({
|
|
83098
|
+
running: boundaryFlushing,
|
|
83099
|
+
stage: boundaryFlushStage,
|
|
83100
|
+
detail: boundaryFlushDetail,
|
|
83101
|
+
ageMs: boundaryFlushStartedAt ? Date.now() - boundaryFlushStartedAt : 0,
|
|
83102
|
+
units: boundaryFlushUnits,
|
|
83103
|
+
items: boundaryFlushItems,
|
|
83104
|
+
chunks: boundaryFlushBatches,
|
|
83105
|
+
stalledMs: boundaryFlushLastAdvanceAt ? Date.now() - boundaryFlushLastAdvanceAt : 0,
|
|
83106
|
+
// Held workspaces, so a hold is REPORTABLE and not merely logged once at the
|
|
83107
|
+
// transition. `errata status` reads this rather than the on-disk profile:
|
|
83108
|
+
// the profile records what the workspace INTENDS to assert, which is why
|
|
83109
|
+
// status could print "→ project 019f462d" for hours while every batch was
|
|
83110
|
+
// being held or shipped unlinked. A hold is the daemon's actual behavior.
|
|
83111
|
+
holds: [
|
|
83112
|
+
...[...projectHolds.values()].map((h) => ({
|
|
83113
|
+
workspace: h.workspace,
|
|
83114
|
+
kind: "project",
|
|
83115
|
+
reason: h.reason,
|
|
83116
|
+
sinceMs: Date.now() - h.since
|
|
83117
|
+
})),
|
|
83118
|
+
...[...identityBlocks.values()].map((h) => ({
|
|
83119
|
+
workspace: h.workspace,
|
|
83120
|
+
kind: "identity",
|
|
83121
|
+
reason: h.reason,
|
|
83122
|
+
sinceMs: Date.now() - h.since
|
|
83123
|
+
}))
|
|
83124
|
+
]
|
|
83125
|
+
});
|
|
83126
|
+
const endFlush = () => {
|
|
83127
|
+
boundaryFlushing = false;
|
|
83128
|
+
boundaryFlushStage = "";
|
|
83129
|
+
};
|
|
83130
|
+
const fireBoundary = () => {
|
|
83131
|
+
for (const l of boundaryListeners) {
|
|
83132
|
+
try {
|
|
83133
|
+
l();
|
|
83134
|
+
} catch {
|
|
83384
83135
|
}
|
|
83385
|
-
|
|
83386
|
-
|
|
83387
|
-
|
|
83388
|
-
|
|
83389
|
-
|
|
83390
|
-
|
|
83391
|
-
|
|
83392
|
-
|
|
83393
|
-
|
|
83394
|
-
|
|
83395
|
-
|
|
83396
|
-
|
|
83397
|
-
|
|
83398
|
-
|
|
83399
|
-
}
|
|
83400
|
-
|
|
83401
|
-
}
|
|
83136
|
+
}
|
|
83137
|
+
};
|
|
83138
|
+
const buildRecord = (entry) => {
|
|
83139
|
+
const id = entry.id;
|
|
83140
|
+
const wsIdentity = identityFor(entry);
|
|
83141
|
+
let wsCloud = cloud;
|
|
83142
|
+
if (wsIdentity.rule === "workspace-binding") {
|
|
83143
|
+
if (wsIdentity.invalid) {
|
|
83144
|
+
noteIdentityBlock(entry.id, entry.name, wsIdentity);
|
|
83145
|
+
wsCloud = blockedCloudClient(wsIdentity.reason);
|
|
83146
|
+
} else {
|
|
83147
|
+
wsCloud = authedCloudClientForProfile(loadConfig(), wsIdentity.profileName, {
|
|
83148
|
+
timeoutMs: CLOUD_TIMEOUT_MS,
|
|
83149
|
+
daemonVersion: DAEMON_VERSION
|
|
83150
|
+
});
|
|
83151
|
+
console.log(`[identity] workspace "${entry.name}" bound \u2192 profile "${wsIdentity.profileName}"`);
|
|
83402
83152
|
}
|
|
83403
|
-
|
|
83404
|
-
|
|
83405
|
-
|
|
83406
|
-
|
|
83407
|
-
|
|
83408
|
-
|
|
83409
|
-
|
|
83410
|
-
|
|
83411
|
-
|
|
83412
|
-
|
|
83413
|
-
|
|
83414
|
-
|
|
83415
|
-
|
|
83416
|
-
|
|
83417
|
-
|
|
83418
|
-
|
|
83419
|
-
|
|
83420
|
-
|
|
83421
|
-
|
|
83153
|
+
}
|
|
83154
|
+
const engine = createWorkspaceEngine({
|
|
83155
|
+
workspaceRoot: entry.path,
|
|
83156
|
+
cloud: wsCloud,
|
|
83157
|
+
apiKey: opts.apiKey,
|
|
83158
|
+
reviewUrl: () => `${baseUrl}/ws/${id}/review`,
|
|
83159
|
+
sharedStore,
|
|
83160
|
+
pendingUpdate: () => serverPending ?? updatePoller?.current() ?? null,
|
|
83161
|
+
// Ops-gated (default OFF): the 🚨 notice reads as "errata broke my
|
|
83162
|
+
// machine" to a non-operator. Detection + ledger run regardless — only
|
|
83163
|
+
// this push surface (and the toast, gated in notifyOpsEvent) is scoped.
|
|
83164
|
+
daemonDistress: () => loadConfig().opsNotices ? healthSentinel.current() : null,
|
|
83165
|
+
...opts.skipWatchers ? { skipWatchers: true } : {}
|
|
83166
|
+
});
|
|
83167
|
+
const webApp = buildWebUi({
|
|
83168
|
+
paths: engine.paths,
|
|
83169
|
+
store: engine.store,
|
|
83170
|
+
log: engine.log,
|
|
83171
|
+
profile: engine.profile,
|
|
83172
|
+
daemonVersion: DAEMON_VERSION,
|
|
83173
|
+
basePath: `/ws/${id}`,
|
|
83174
|
+
onWorkingFileChanged: engine.webHooks.onWorkingFileChanged,
|
|
83175
|
+
onHook: engine.webHooks.onHook,
|
|
83176
|
+
onHookEvent: engine.webHooks.onHookEvent,
|
|
83177
|
+
onShellEvent: engine.webHooks.onShellEvent,
|
|
83178
|
+
// The tool-run RECORD seam (SP-tool-ground). The single-engine path wired
|
|
83179
|
+
// this in #1172; this sibling builder never did, so on every multi
|
|
83180
|
+
// deployment (i.e. every real one) the webui logged "tool-run recorded"
|
|
83181
|
+
// and dropped the record on the floor — toolSessionsTracked read 0 across
|
|
83182
|
+
// 936 turns while the log showed records landing (VS-corroboration-rate).
|
|
83183
|
+
onToolsObserved: engine.webHooks.onToolsObserved,
|
|
83184
|
+
onToolError: engine.webHooks.onToolError,
|
|
83185
|
+
onDiagnostics: engine.webHooks.onDiagnostics,
|
|
83186
|
+
onTurn: engine.webHooks.onTurn,
|
|
83187
|
+
onMessage: engine.webHooks.onMessage,
|
|
83188
|
+
onSessionEnd: (e) => {
|
|
83189
|
+
engine.webHooks.onSessionEnd(e);
|
|
83190
|
+
fireBoundary();
|
|
83422
83191
|
}
|
|
83423
|
-
|
|
83424
|
-
|
|
83425
|
-
|
|
83426
|
-
|
|
83427
|
-
|
|
83428
|
-
|
|
83429
|
-
|
|
83430
|
-
|
|
83431
|
-
|
|
83432
|
-
|
|
83433
|
-
|
|
83434
|
-
|
|
83435
|
-
|
|
83436
|
-
|
|
83437
|
-
|
|
83438
|
-
|
|
83192
|
+
});
|
|
83193
|
+
return { entry, engine, webApp, root: entry.path, id, lastHookAt: Date.now() };
|
|
83194
|
+
};
|
|
83195
|
+
const pruned = pruneMissingWorkspaces();
|
|
83196
|
+
if (pruned.length > 0) {
|
|
83197
|
+
console.log(`[errata] pruned ${pruned.length} registered workspace(s) whose path no longer exists`);
|
|
83198
|
+
}
|
|
83199
|
+
if (registryCorrupt()) {
|
|
83200
|
+
console.error("[identity] workspaces.json is UNREADABLE \u2014 all per-workspace cloud push will hold until it parses (fail-closed)");
|
|
83201
|
+
} else {
|
|
83202
|
+
for (const b of boundWorkspaceEntries()) {
|
|
83203
|
+
console.log(`[identity] bound workspace: ${b.name} \u2192 profile "${b.installationProfile}"`);
|
|
83204
|
+
}
|
|
83205
|
+
}
|
|
83206
|
+
const entries = listWorkspaceEntries();
|
|
83207
|
+
if (opts.eagerWorkspaces ?? EAGER_WORKSPACES_AT_BOOT) {
|
|
83208
|
+
for (const entry of entries) {
|
|
83209
|
+
try {
|
|
83210
|
+
records.push(buildRecord(entry));
|
|
83211
|
+
} catch (err2) {
|
|
83212
|
+
console.warn(`[errata] skipping ${entry.name} (${entry.path}): ${err2 instanceof Error ? err2.message : err2}`);
|
|
83439
83213
|
}
|
|
83440
|
-
|
|
83441
|
-
|
|
83442
|
-
|
|
83443
|
-
|
|
83444
|
-
|
|
83445
|
-
|
|
83446
|
-
|
|
83447
|
-
|
|
83214
|
+
}
|
|
83215
|
+
if (entries.length > 0 && records.length < entries.length) {
|
|
83216
|
+
const failed = entries.length - records.length;
|
|
83217
|
+
console.warn(
|
|
83218
|
+
`[errata] \u26A0 ${failed} of ${entries.length} project(s) failed to start \u2014 likely a stale daemon holding DB locks. Run \`errata stop\` (or kill the old errata process) and retry.`
|
|
83219
|
+
);
|
|
83220
|
+
}
|
|
83221
|
+
} else if (entries.length > 0) {
|
|
83222
|
+
console.log(
|
|
83223
|
+
`[errata] ${entries.length} workspace(s) registered \xB7 cold \u2014 an engine spawns on the first hook from each`
|
|
83224
|
+
);
|
|
83225
|
+
}
|
|
83226
|
+
const app = new Hono2();
|
|
83227
|
+
const attachWorkspace = (root) => {
|
|
83228
|
+
const norm = normPath(root);
|
|
83229
|
+
if (records.some((r) => normPath(r.root) === norm)) return { attached: false, id: null, reason: "already-watched" };
|
|
83230
|
+
const entry = listWorkspaceEntries().find((e) => normPath(e.path) === norm);
|
|
83231
|
+
if (!entry) return { attached: false, id: null, reason: "not-registered" };
|
|
83232
|
+
let rec;
|
|
83233
|
+
try {
|
|
83234
|
+
rec = buildRecord(entry);
|
|
83235
|
+
} catch (err2) {
|
|
83236
|
+
return { attached: false, id: null, reason: err2 instanceof Error ? err2.message : "build-failed" };
|
|
83237
|
+
}
|
|
83238
|
+
records.push(rec);
|
|
83239
|
+
void ambientLinkAll();
|
|
83240
|
+
try {
|
|
83241
|
+
writeFileSync23(
|
|
83242
|
+
rec.engine.paths.daemonLock,
|
|
83243
|
+
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${rec.id}`, startedAt: Date.now() }),
|
|
83244
|
+
"utf8"
|
|
83245
|
+
);
|
|
83246
|
+
registerWorkspace(rec.engine.profile, rec.root);
|
|
83247
|
+
} catch {
|
|
83248
|
+
}
|
|
83249
|
+
void rec.engine.reindex({ skipEmbed: true, quiet: rec.engine.store.nodeCount() > 0 }).catch(() => {
|
|
83250
|
+
});
|
|
83251
|
+
console.log(`[errata] hot-registered ${rec.entry.name} (${rec.id}) \u2014 now watching live`);
|
|
83252
|
+
return { attached: true, id: rec.id };
|
|
83253
|
+
};
|
|
83254
|
+
app.post("/api/attach", async (c) => {
|
|
83255
|
+
let body2 = {};
|
|
83256
|
+
try {
|
|
83257
|
+
body2 = await c.req.json();
|
|
83258
|
+
} catch {
|
|
83259
|
+
return c.json({ error: "bad-json" }, 400);
|
|
83260
|
+
}
|
|
83261
|
+
const root = typeof body2["path"] === "string" ? body2["path"] : null;
|
|
83262
|
+
if (!root) return c.json({ error: "path-required" }, 400);
|
|
83263
|
+
return c.json(attachWorkspace(root));
|
|
83264
|
+
});
|
|
83265
|
+
app.post("/api/notify-test", (c) => {
|
|
83266
|
+
notify("Errata \xB7 test", "Fired from the daemon \u2014 if you see this, daemon toasts render.");
|
|
83267
|
+
return c.json({ ok: true });
|
|
83268
|
+
});
|
|
83269
|
+
app.get(
|
|
83270
|
+
"/",
|
|
83271
|
+
(c) => c.json({
|
|
83272
|
+
daemonVersion: DAEMON_VERSION,
|
|
83273
|
+
pendingUpdate: updatePoller?.current() ?? null,
|
|
83274
|
+
projects: records.map((r) => ({
|
|
83275
|
+
id: r.id,
|
|
83276
|
+
name: r.entry.name,
|
|
83277
|
+
path: r.root,
|
|
83278
|
+
stack: r.entry.stack,
|
|
83279
|
+
nodes: r.engine.store.nodeCount(),
|
|
83280
|
+
// COUNT(*) — the materializing findNodesByLabel(...).length form held
|
|
83281
|
+
// the event loop for seconds per poll once stores grew (HZ-index-hydrate).
|
|
83282
|
+
problems: r.engine.store.countNodesByLabel("Problem"),
|
|
83283
|
+
solutions: r.engine.store.countNodesByLabel("Solution"),
|
|
83284
|
+
endpoints: `/ws/${r.id}/`
|
|
83285
|
+
})),
|
|
83286
|
+
humanView: "run `errata report` \u2014 the dashboard was retired (GRAFT 4e)"
|
|
83287
|
+
})
|
|
83288
|
+
);
|
|
83289
|
+
const STRANDED_TTL_MS = 6e4;
|
|
83290
|
+
const strandedCache = /* @__PURE__ */ new Map();
|
|
83291
|
+
const strandedRefreshing = /* @__PURE__ */ new Set();
|
|
83292
|
+
const strandedCount = (r) => {
|
|
83293
|
+
const hit = strandedCache.get(r.id);
|
|
83294
|
+
if (hit && Date.now() - hit.at < STRANDED_TTL_MS) return hit.count;
|
|
83295
|
+
if (!strandedRefreshing.has(r.id)) {
|
|
83296
|
+
strandedRefreshing.add(r.id);
|
|
83297
|
+
const finish = (count) => {
|
|
83298
|
+
strandedCache.set(r.id, { at: Date.now(), count });
|
|
83299
|
+
strandedRefreshing.delete(r.id);
|
|
83300
|
+
};
|
|
83301
|
+
if (consolidateWorker && r.engine.paths.castalia !== ":memory:") {
|
|
83302
|
+
consolidateWorker.run({ kind: "strandedCount", sharedDbPath, castaliaPath: r.engine.paths.castalia }).then((n) => finish(Number(n))).catch(() => {
|
|
83303
|
+
setImmediate(() => {
|
|
83304
|
+
const done = markPass(`stranded-scan:${r.entry.name}`);
|
|
83305
|
+
try {
|
|
83306
|
+
finish(findStrandedSymbols(r.engine.store).length);
|
|
83307
|
+
} catch {
|
|
83308
|
+
strandedRefreshing.delete(r.id);
|
|
83309
|
+
} finally {
|
|
83310
|
+
done();
|
|
83311
|
+
}
|
|
83448
83312
|
});
|
|
83449
|
-
}
|
|
83450
|
-
|
|
83451
|
-
|
|
83452
|
-
|
|
83453
|
-
|
|
83454
|
-
|
|
83455
|
-
|
|
83456
|
-
|
|
83457
|
-
|
|
83458
|
-
|
|
83459
|
-
|
|
83460
|
-
|
|
83461
|
-
return await consolidateWorker.run({ kind: "revisitTriage", sharedDbPath, ts: Date.now() });
|
|
83462
|
-
} catch {
|
|
83463
|
-
}
|
|
83464
|
-
}
|
|
83465
|
-
return revisitStaleRoutes(sharedStore, Date.now());
|
|
83466
|
-
},
|
|
83467
|
-
async revisitStalePrinciples() {
|
|
83468
|
-
if (consolidateWorker) {
|
|
83469
|
-
try {
|
|
83470
|
-
return await consolidateWorker.run({ kind: "revisitPrinciples", sharedDbPath, ts: Date.now() });
|
|
83471
|
-
} catch {
|
|
83472
|
-
}
|
|
83313
|
+
});
|
|
83314
|
+
} else {
|
|
83315
|
+
setImmediate(() => {
|
|
83316
|
+
const done = markPass(`stranded-scan:${r.entry.name}`);
|
|
83317
|
+
try {
|
|
83318
|
+
finish(findStrandedSymbols(r.engine.store).length);
|
|
83319
|
+
} catch {
|
|
83320
|
+
strandedRefreshing.delete(r.id);
|
|
83321
|
+
} finally {
|
|
83322
|
+
done();
|
|
83323
|
+
}
|
|
83324
|
+
});
|
|
83473
83325
|
}
|
|
83474
|
-
|
|
83475
|
-
|
|
83476
|
-
|
|
83477
|
-
|
|
83478
|
-
|
|
83479
|
-
|
|
83480
|
-
|
|
83481
|
-
|
|
83482
|
-
|
|
83483
|
-
|
|
83484
|
-
|
|
83485
|
-
|
|
83486
|
-
|
|
83487
|
-
|
|
83488
|
-
|
|
83489
|
-
|
|
83490
|
-
|
|
83491
|
-
|
|
83492
|
-
|
|
83493
|
-
|
|
83494
|
-
|
|
83495
|
-
|
|
83496
|
-
|
|
83497
|
-
|
|
83498
|
-
|
|
83499
|
-
|
|
83500
|
-
|
|
83326
|
+
}
|
|
83327
|
+
return hit?.count ?? -1;
|
|
83328
|
+
};
|
|
83329
|
+
let lastHook = null;
|
|
83330
|
+
app.get(
|
|
83331
|
+
"/health",
|
|
83332
|
+
(c) => c.json({
|
|
83333
|
+
ok: true,
|
|
83334
|
+
daemonVersion: DAEMON_VERSION,
|
|
83335
|
+
lastHook,
|
|
83336
|
+
// What the daemon is DOING, not just what it holds. Without these a
|
|
83337
|
+
// pegged daemon is only diagnosable by attaching a CPU profiler to the
|
|
83338
|
+
// live process — see watch-census.ts for the incident that proved it.
|
|
83339
|
+
pass: passState(),
|
|
83340
|
+
watch: watchCensus(),
|
|
83341
|
+
// Registered but with no engine running (WS-cold-start). Listed so a cold
|
|
83342
|
+
// daemon is legible as "nothing has needed waking yet" rather than as
|
|
83343
|
+
// "the registry lost my projects" — an invisible cold workspace is
|
|
83344
|
+
// indistinguishable from a missing one, and only one of those is fine.
|
|
83345
|
+
cold: listWorkspaceEntries().filter((e) => !records.some((r) => normPath(r.root) === normPath(e.path))).map((e) => ({ name: e.name, path: e.path })),
|
|
83346
|
+
projects: records.map((r) => ({
|
|
83347
|
+
id: r.id,
|
|
83348
|
+
name: r.entry.name,
|
|
83349
|
+
path: r.root,
|
|
83350
|
+
nodes: r.engine.store.nodeCount(),
|
|
83351
|
+
// COUNT(*) — same hydration hazard as `/` (HZ-index-hydrate).
|
|
83352
|
+
problems: r.engine.store.countNodesByLabel("Problem"),
|
|
83353
|
+
solutions: r.engine.store.countNodesByLabel("Solution"),
|
|
83354
|
+
stranded: strandedCount(r)
|
|
83355
|
+
}))
|
|
83356
|
+
})
|
|
83357
|
+
);
|
|
83358
|
+
const forward = async (c, pathInBody) => {
|
|
83359
|
+
const raw2 = await c.req.text();
|
|
83360
|
+
let body2 = {};
|
|
83361
|
+
try {
|
|
83362
|
+
body2 = JSON.parse(raw2);
|
|
83363
|
+
} catch {
|
|
83364
|
+
return c.json({ error: "bad-json" }, 400);
|
|
83365
|
+
}
|
|
83366
|
+
lastHook = { at: Date.now(), event: typeof body2["hook_event_name"] === "string" ? body2["hook_event_name"] : c.req.path };
|
|
83367
|
+
let owner = ownerOf(records, pathInBody(body2));
|
|
83368
|
+
if (!owner) {
|
|
83369
|
+
const missPath = pathInBody(body2);
|
|
83370
|
+
const registeredRoot = missPath ? pickOwnerRoot(listWorkspaceEntries().map((e) => e.path), missPath) : null;
|
|
83371
|
+
const wakeRoot = registeredRoot ?? (missPath ? findGitRoot(missPath) : null);
|
|
83372
|
+
if (wakeRoot && attachWorkspace(wakeRoot).attached) {
|
|
83373
|
+
owner = ownerOf(records, missPath) ?? null;
|
|
83374
|
+
if (owner) console.log(`[errata] woke workspace ${owner.entry.name}`);
|
|
83375
|
+
}
|
|
83376
|
+
}
|
|
83377
|
+
if (!owner) {
|
|
83378
|
+
rootAdopter.noteMiss(pathInBody(body2));
|
|
83379
|
+
return c.json({ error: "no-matching-project" }, 404);
|
|
83380
|
+
}
|
|
83381
|
+
owner.lastHookAt = Date.now();
|
|
83382
|
+
const sub = c.req.path;
|
|
83383
|
+
const done = markPass(`${sub}:${owner.entry.name}`);
|
|
83384
|
+
try {
|
|
83385
|
+
return await owner.webApp.fetch(
|
|
83386
|
+
new Request(`http://local${sub}`, {
|
|
83387
|
+
method: "POST",
|
|
83388
|
+
headers: { "content-type": "application/json" },
|
|
83389
|
+
body: raw2
|
|
83390
|
+
})
|
|
83391
|
+
);
|
|
83392
|
+
} finally {
|
|
83393
|
+
done();
|
|
83394
|
+
}
|
|
83395
|
+
};
|
|
83396
|
+
app.post(
|
|
83397
|
+
"/api/hook",
|
|
83398
|
+
(c) => forward(c, (b) => {
|
|
83399
|
+
const input = b["tool_input"] ?? {};
|
|
83400
|
+
const fp = input["file_path"];
|
|
83401
|
+
const cwd = b["cwd"];
|
|
83402
|
+
return (typeof fp === "string" ? fp : void 0) ?? (typeof cwd === "string" ? cwd : void 0);
|
|
83403
|
+
})
|
|
83404
|
+
);
|
|
83405
|
+
app.post(
|
|
83406
|
+
"/api/diagnostics",
|
|
83407
|
+
(c) => forward(c, (b) => {
|
|
83408
|
+
const loc = b["path"] ?? b["uri"];
|
|
83409
|
+
return typeof loc === "string" ? fileUriToFsPath(loc) : void 0;
|
|
83410
|
+
})
|
|
83411
|
+
);
|
|
83412
|
+
const cwdOf = (b) => typeof b["cwd"] === "string" ? b["cwd"] : void 0;
|
|
83413
|
+
app.post("/api/turn", (c) => forward(c, cwdOf));
|
|
83414
|
+
app.post("/api/message", (c) => forward(c, cwdOf));
|
|
83415
|
+
app.post("/api/session-end", (c) => forward(c, cwdOf));
|
|
83416
|
+
app.post("/api/context-inject", (c) => forward(c, cwdOf));
|
|
83417
|
+
for (const r of records) {
|
|
83418
|
+
mountedIds.add(r.id);
|
|
83419
|
+
app.route(`/ws/${r.id}`, r.webApp);
|
|
83420
|
+
}
|
|
83421
|
+
const dispatchHot = async (c) => {
|
|
83422
|
+
const id = c.req.param("id");
|
|
83423
|
+
const rec = records.find((r) => r.id === id);
|
|
83424
|
+
if (!rec) return c.notFound();
|
|
83425
|
+
const url2 = new URL(c.req.url);
|
|
83426
|
+
url2.pathname = url2.pathname.slice(`/ws/${id}`.length) || "/";
|
|
83427
|
+
const method = c.req.method;
|
|
83428
|
+
const body2 = method === "GET" || method === "HEAD" ? void 0 : await c.req.raw.arrayBuffer();
|
|
83429
|
+
return rec.webApp.fetch(new Request(url2.toString(), { method, headers: c.req.raw.headers, body: body2 }));
|
|
83430
|
+
};
|
|
83431
|
+
app.all("/ws/:id", dispatchHot);
|
|
83432
|
+
app.all("/ws/:id/*", dispatchHot);
|
|
83433
|
+
const retireWorkspace = async (rec, reason) => {
|
|
83434
|
+
const idx = records.indexOf(rec);
|
|
83435
|
+
if (idx === -1) return;
|
|
83436
|
+
records.splice(idx, 1);
|
|
83437
|
+
try {
|
|
83438
|
+
await rec.engine.stop();
|
|
83439
|
+
} catch (err2) {
|
|
83440
|
+
console.warn(`[errata] retire ${rec.entry.name}: engine.stop failed \u2014 ${err2 instanceof Error ? err2.message : err2}`);
|
|
83441
|
+
}
|
|
83442
|
+
console.log(
|
|
83443
|
+
`[errata] retired ${rec.entry.name} (${reason})` + (reason === "idle" ? " \u2014 idle; the next hook from it re-attaches" : "")
|
|
83444
|
+
);
|
|
83445
|
+
};
|
|
83446
|
+
const sweepIdleWorkspaces = () => {
|
|
83447
|
+
const now = Date.now();
|
|
83448
|
+
for (const rec of [...records]) {
|
|
83449
|
+
let lastActiveAt = rec.lastHookAt;
|
|
83450
|
+
try {
|
|
83451
|
+
lastActiveAt = Math.max(lastActiveAt, rec.engine.lastActivityAt());
|
|
83452
|
+
} catch {
|
|
83453
|
+
}
|
|
83454
|
+
const reason = retireDecision({ exists: existsSync31(rec.root), lastActiveAt, now });
|
|
83455
|
+
if (reason) void retireWorkspace(rec, reason);
|
|
83456
|
+
}
|
|
83457
|
+
};
|
|
83458
|
+
const idleSweep = setInterval(sweepIdleWorkspaces, WORKSPACE_SWEEP_INTERVAL_MS);
|
|
83459
|
+
idleSweep.unref?.();
|
|
83460
|
+
let codexWakeFirstPass = true;
|
|
83461
|
+
const sweepCodexWake = () => {
|
|
83462
|
+
const lookback = codexWakeFirstPass ? CODEX_WAKE_BOOT_LOOKBACK_MS : CODEX_WAKE_LOOKBACK_MS;
|
|
83463
|
+
codexWakeFirstPass = false;
|
|
83464
|
+
let entries2;
|
|
83465
|
+
try {
|
|
83466
|
+
entries2 = listWorkspaceEntries();
|
|
83467
|
+
} catch {
|
|
83468
|
+
return;
|
|
83469
|
+
}
|
|
83470
|
+
const candidates = codexWakeCandidates({
|
|
83471
|
+
entries: entries2,
|
|
83472
|
+
liveRoots: new Set(records.map((r) => normPath(r.root))),
|
|
83473
|
+
normPath,
|
|
83474
|
+
sinceMs: Date.now() - lookback,
|
|
83475
|
+
disabled: process.env["ERRATA_CODEX_WAKE_DISABLE"] === "1"
|
|
83476
|
+
});
|
|
83477
|
+
for (const c of candidates) {
|
|
83478
|
+
if (attachWorkspace(c.path).attached) {
|
|
83479
|
+
console.log(`[errata] woke workspace ${c.name} \u2014 recent Codex/Gemini/OpenCode transcript (headless capture)`);
|
|
83480
|
+
}
|
|
83481
|
+
}
|
|
83482
|
+
};
|
|
83483
|
+
const codexWake = setInterval(sweepCodexWake, WORKSPACE_SWEEP_INTERVAL_MS);
|
|
83484
|
+
codexWake.unref?.();
|
|
83485
|
+
const codexWakeBoot = setTimeout(sweepCodexWake, 5e3);
|
|
83486
|
+
codexWakeBoot.unref?.();
|
|
83487
|
+
const reindexAll = async (rOpts) => {
|
|
83488
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
83489
|
+
const toIndex = records.filter((r) => rOpts?.force || r.engine.store.nodeCount() === 0);
|
|
83490
|
+
const plural = (n) => `repositor${n === 1 ? "y" : "ies"}`;
|
|
83491
|
+
if (toIndex.length > 0) {
|
|
83492
|
+
notifyEvent("index-started", `Indexing ${toIndex.length} ${plural(toIndex.length)}\u2026`);
|
|
83493
|
+
}
|
|
83494
|
+
let indexed = 0;
|
|
83495
|
+
for (const r of records) {
|
|
83496
|
+
if (!rOpts?.force && r.engine.store.nodeCount() > 0) {
|
|
83497
|
+
out2.set(r.id, r.engine.store.nodeCount());
|
|
83498
|
+
continue;
|
|
83499
|
+
}
|
|
83500
|
+
const done = markPass(`reindex:${r.entry.name}`);
|
|
83501
|
+
try {
|
|
83502
|
+
const idx = await r.engine.reindex({ skipEmbed: rOpts?.skipEmbed ?? true, quiet: true });
|
|
83503
|
+
out2.set(r.id, idx.nodes);
|
|
83504
|
+
indexed++;
|
|
83505
|
+
console.log(`[errata] indexed ${r.entry.name}: ${idx.nodes} nodes / ${idx.edges} edges`);
|
|
83506
|
+
} catch (err2) {
|
|
83507
|
+
console.warn(`[errata] index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83508
|
+
out2.set(r.id, r.engine.store.nodeCount());
|
|
83509
|
+
} finally {
|
|
83510
|
+
done();
|
|
83511
|
+
}
|
|
83512
|
+
}
|
|
83513
|
+
if (toIndex.length > 0) {
|
|
83514
|
+
notifyEvent("index-completed", `Indexed ${indexed} ${plural(indexed)}`);
|
|
83515
|
+
}
|
|
83516
|
+
return out2;
|
|
83517
|
+
};
|
|
83518
|
+
const { server, port } = await listenServer(app.fetch, opts.webPort ?? 7891);
|
|
83519
|
+
baseUrl = `http://127.0.0.1:${port}`;
|
|
83520
|
+
try {
|
|
83521
|
+
ensureDir(globalDir());
|
|
83522
|
+
writeFileSync23(
|
|
83523
|
+
lockPath,
|
|
83524
|
+
JSON.stringify({ pid: process.pid, webUiUrl: baseUrl, startedAt: Date.now() }),
|
|
83525
|
+
"utf8"
|
|
83526
|
+
);
|
|
83527
|
+
} catch {
|
|
83528
|
+
}
|
|
83529
|
+
for (const r of records) {
|
|
83530
|
+
try {
|
|
83531
|
+
writeFileSync23(
|
|
83532
|
+
r.engine.paths.daemonLock,
|
|
83533
|
+
JSON.stringify({ pid: process.pid, webUiUrl: `${baseUrl}/ws/${r.id}`, startedAt: Date.now() }),
|
|
83534
|
+
"utf8"
|
|
83535
|
+
);
|
|
83536
|
+
registerWorkspace(r.engine.profile, r.root);
|
|
83537
|
+
} catch {
|
|
83538
|
+
}
|
|
83539
|
+
}
|
|
83540
|
+
if (opts.reindexOnStart !== false && records.length > 0) {
|
|
83541
|
+
void reindexAll({ skipEmbed: opts.skipEmbed ?? true });
|
|
83542
|
+
}
|
|
83543
|
+
const rootAdopter = createRootAdopter({
|
|
83544
|
+
isKnownRoot: (root) => pickOwnerRoot(records.map((r) => r.root), root) !== null,
|
|
83545
|
+
linkingAllowed: () => {
|
|
83546
|
+
const c = loadConfig();
|
|
83547
|
+
return c.consent.sync && hasCloudCredential(c);
|
|
83548
|
+
},
|
|
83549
|
+
lookup: async (locator) => cloudNow().lookupProject(locator),
|
|
83550
|
+
adopt: async (root) => {
|
|
83551
|
+
let profile = loadProfile(root);
|
|
83552
|
+
if (!profile) {
|
|
83553
|
+
profile = autodetectProfile(root);
|
|
83554
|
+
saveProfile(root, profile);
|
|
83555
|
+
}
|
|
83556
|
+
registerWorkspace(profile, root);
|
|
83557
|
+
attachWorkspace(root);
|
|
83558
|
+
},
|
|
83559
|
+
// eslint-disable-next-line no-console
|
|
83560
|
+
log: (line) => console.log(line)
|
|
83561
|
+
});
|
|
83562
|
+
const ambientLinkAll = async () => {
|
|
83563
|
+
const c = loadConfig();
|
|
83564
|
+
if (!c.consent.sync || !hasCloudCredential(c)) return;
|
|
83565
|
+
const client = cloudNow();
|
|
83566
|
+
for (const r of records) {
|
|
83567
|
+
const out2 = await ensureProjectLink(r.root, r.engine.profile, client);
|
|
83568
|
+
if (out2.linked) {
|
|
83569
|
+
console.log(
|
|
83570
|
+
`[errata] linked ${r.engine.profile.name} \u2192 project ${out2.projectId}${out2.created ? " (created)" : ""}`
|
|
83571
|
+
);
|
|
83572
|
+
} else if (out2.reason === "error") {
|
|
83573
|
+
console.warn(`[errata] project link failed for ${r.engine.profile.name}: ${out2.detail}`);
|
|
83574
|
+
}
|
|
83575
|
+
const hyd = await hydrateProject({
|
|
83576
|
+
root: r.root,
|
|
83577
|
+
profile: r.engine.profile,
|
|
83578
|
+
store: r.engine.store,
|
|
83579
|
+
cloud: client
|
|
83580
|
+
});
|
|
83581
|
+
if (hyd.hydrated) {
|
|
83582
|
+
r.engine.markContextDirty();
|
|
83583
|
+
console.log(
|
|
83584
|
+
`[errata] primed ${r.engine.profile.name} from project \u2014 ${hyd.nodes} priors, ${hyd.edges} links`
|
|
83585
|
+
);
|
|
83586
|
+
} else if (hyd.reason === "error") {
|
|
83587
|
+
console.warn(`[errata] project prime failed for ${r.engine.profile.name}: ${hyd.detail}`);
|
|
83588
|
+
}
|
|
83589
|
+
if (r.engine.profile.projectId) {
|
|
83590
|
+
try {
|
|
83591
|
+
const { salt } = await resolveProjectSymbolSalt(client, r.engine.profile.projectId);
|
|
83592
|
+
const re = await reanchorProject({
|
|
83593
|
+
root: r.root,
|
|
83594
|
+
profile: r.engine.profile,
|
|
83595
|
+
store: r.engine.store,
|
|
83596
|
+
cloud: client,
|
|
83597
|
+
salt
|
|
83598
|
+
});
|
|
83599
|
+
if (re.reanchored) {
|
|
83600
|
+
r.engine.markContextDirty();
|
|
83601
|
+
console.log(
|
|
83602
|
+
`[errata] reanchored ${r.engine.profile.name} \u2014 ${re.anchors} anchors onto local code (${re.dropped} unresolved, dropped)`
|
|
83603
|
+
);
|
|
83604
|
+
} else if (re.reason === "error") {
|
|
83605
|
+
console.warn(`[errata] project reanchor failed for ${r.engine.profile.name}: ${re.detail}`);
|
|
83606
|
+
}
|
|
83607
|
+
if (r.engine.profile.projectHydratedAt && !historyInFlight.has(r.root)) {
|
|
83608
|
+
historyInFlight.add(r.root);
|
|
83609
|
+
const engine = r.engine;
|
|
83610
|
+
const root = r.root;
|
|
83611
|
+
const anchorIndex = {
|
|
83612
|
+
nodesByRelPath: (paths) => {
|
|
83613
|
+
if (!records.some((x) => x.root === root)) {
|
|
83614
|
+
throw new Error("workspace detached mid-walk; the watermark holds and the next pass resumes");
|
|
83615
|
+
}
|
|
83616
|
+
return engine.store.nodesByRelPath(paths);
|
|
83617
|
+
}
|
|
83618
|
+
};
|
|
83619
|
+
void startHistoryStage(root, engine.profile, client, {
|
|
83620
|
+
anchors: { index: anchorIndex, salt },
|
|
83621
|
+
// PS-web projects: the console's consents (Privacy tab) win over
|
|
83622
|
+
// the profile's memory; applied to config and profile here, with
|
|
83623
|
+
// the change printed so a flipped switch is visible in the log.
|
|
83624
|
+
applyConsents: (p, consents) => {
|
|
83625
|
+
const cfg2 = loadConfig();
|
|
83626
|
+
const applied = applyConsents(p, cfg2, consents, (/* @__PURE__ */ new Date()).toISOString());
|
|
83627
|
+
if (applied.changed) {
|
|
83628
|
+
saveConfig(applied.config);
|
|
83629
|
+
Object.assign(engine.profile, applied.profile);
|
|
83630
|
+
saveProfile(root, engine.profile);
|
|
83631
|
+
console.log(
|
|
83632
|
+
`[errata] consents applied from the console for ${engine.profile.name}: sync=${consents.sync} telemetry=${consents.telemetry ? "on" : "off"} packages=${consents.packages ? "on" : "off"} structure=${consents.structure ? "on" : "off"} history=${consents.mineHistory ? consents.contributeHistory ? "mine+contribute" : "mine" : "off"}`
|
|
83633
|
+
);
|
|
83634
|
+
}
|
|
83635
|
+
return applied.profile;
|
|
83636
|
+
},
|
|
83637
|
+
saveProfile: (p) => {
|
|
83638
|
+
Object.assign(engine.profile, p);
|
|
83639
|
+
saveProfile(root, engine.profile);
|
|
83640
|
+
},
|
|
83641
|
+
log: (line) => console.warn(line)
|
|
83642
|
+
}).then((stage2) => {
|
|
83643
|
+
if (stage2.profile !== engine.profile) {
|
|
83644
|
+
Object.assign(engine.profile, stage2.profile);
|
|
83645
|
+
saveProfile(root, engine.profile);
|
|
83646
|
+
}
|
|
83647
|
+
const state = engine.profile.historyStage?.state ?? "consent-pending";
|
|
83648
|
+
const c2 = engine.profile.historyStage?.counts;
|
|
83649
|
+
console.log(
|
|
83650
|
+
`[errata] history ${state} for ${engine.profile.name}` + (c2 ? ` \xB7 ${c2.commitsWalked ?? 0} commits walked \xB7 ${c2.changesExtracted ?? 0} changes extracted \xB7 ${c2.pairs ?? 0} pairs \xB7 ${c2.claims ?? 0} claims \xB7 ${c2.anchors ?? 0} anchors \xB7 ${c2.packages ?? 0} packages` : "") + (engine.profile.historyStage?.lastError ? ` \xB7 ${engine.profile.historyStage.lastError}` : "")
|
|
83651
|
+
);
|
|
83652
|
+
}).catch((err2) => {
|
|
83653
|
+
console.warn(`[errata] history walk failed for ${engine.profile.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83654
|
+
}).finally(() => historyInFlight.delete(root));
|
|
83655
|
+
}
|
|
83656
|
+
} catch {
|
|
83657
|
+
}
|
|
83658
|
+
}
|
|
83659
|
+
}
|
|
83660
|
+
};
|
|
83661
|
+
const historyInFlight = /* @__PURE__ */ new Set();
|
|
83662
|
+
void ambientLinkAll();
|
|
83663
|
+
const packageAutoAll = async () => {
|
|
83664
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
83665
|
+
for (const r of records) {
|
|
83666
|
+
await new Promise((resolve7) => setImmediate(resolve7));
|
|
83667
|
+
const done = markPass(`package-index:${r.entry.name}`);
|
|
83668
|
+
try {
|
|
83669
|
+
out2.set(
|
|
83670
|
+
r.id,
|
|
83671
|
+
runLockfilePass({
|
|
83672
|
+
root: r.root,
|
|
83673
|
+
store: r.engine.store,
|
|
83674
|
+
workspaceId: r.id,
|
|
83675
|
+
languages: r.engine.profile.languages,
|
|
83676
|
+
log: (m) => console.log(m)
|
|
83677
|
+
})
|
|
83678
|
+
);
|
|
83679
|
+
} catch (err2) {
|
|
83680
|
+
console.warn(`[errata] package index failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83681
|
+
} finally {
|
|
83682
|
+
done();
|
|
83683
|
+
}
|
|
83684
|
+
}
|
|
83685
|
+
return out2;
|
|
83686
|
+
};
|
|
83687
|
+
const daemon = {
|
|
83688
|
+
url: baseUrl,
|
|
83689
|
+
records,
|
|
83690
|
+
sharedStore,
|
|
83691
|
+
attachWorkspace,
|
|
83692
|
+
reindexAll,
|
|
83693
|
+
packageAutoAll,
|
|
83694
|
+
async tickAll() {
|
|
83695
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
83696
|
+
for (const r of records) {
|
|
83697
|
+
const done = markPass(`tick:${r.entry.name}`);
|
|
83698
|
+
try {
|
|
83699
|
+
out2.set(r.id, await r.engine.tick());
|
|
83700
|
+
} catch (err2) {
|
|
83701
|
+
console.warn(`[errata] tick failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83702
|
+
} finally {
|
|
83703
|
+
done();
|
|
83704
|
+
}
|
|
83705
|
+
}
|
|
83706
|
+
return out2;
|
|
83707
|
+
},
|
|
83708
|
+
semanticMaintenanceAll() {
|
|
83709
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
83710
|
+
for (const r of records) {
|
|
83711
|
+
const done = markPass(`semantic:${r.entry.name}`);
|
|
83712
|
+
try {
|
|
83713
|
+
out2.set(r.id, r.engine.semanticMaintenance());
|
|
83714
|
+
} catch (err2) {
|
|
83715
|
+
console.warn(
|
|
83716
|
+
`[errata] semantic maintenance failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
|
|
83717
|
+
);
|
|
83718
|
+
} finally {
|
|
83719
|
+
done();
|
|
83720
|
+
}
|
|
83721
|
+
}
|
|
83722
|
+
return out2;
|
|
83723
|
+
},
|
|
83724
|
+
async nightlyFor(ids) {
|
|
83725
|
+
const want = new Set(ids);
|
|
83726
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
83727
|
+
for (const r of records) {
|
|
83728
|
+
if (!want.has(r.id)) continue;
|
|
83729
|
+
const done = markPass(`nightly:${r.entry.name}`);
|
|
83730
|
+
try {
|
|
83731
|
+
out2.set(r.id, await r.engine.nightly());
|
|
83732
|
+
} catch (err2) {
|
|
83733
|
+
console.warn(`[errata] nightly failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83734
|
+
} finally {
|
|
83735
|
+
done();
|
|
83736
|
+
}
|
|
83737
|
+
}
|
|
83738
|
+
return out2;
|
|
83739
|
+
},
|
|
83740
|
+
async nightlyAll() {
|
|
83741
|
+
return daemon.nightlyFor(records.map((r) => r.id));
|
|
83742
|
+
},
|
|
83743
|
+
async percolateAll() {
|
|
83744
|
+
const ts = Date.now();
|
|
83745
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
83746
|
+
const repoBasename = (locator) => (locator.split("/").filter(Boolean).pop() ?? locator).toLowerCase();
|
|
83747
|
+
for (const r of records) {
|
|
83748
|
+
const profile = r.engine.profile;
|
|
83749
|
+
const aliases = profile.repoLocatorAliases ?? [];
|
|
83750
|
+
const locator = profile.repoLocator;
|
|
83751
|
+
if (!locator || aliases.length === 0) continue;
|
|
83752
|
+
try {
|
|
83753
|
+
for (const alias of aliases) {
|
|
83754
|
+
if (repoBasename(alias) !== repoBasename(locator)) {
|
|
83755
|
+
console.log(
|
|
83756
|
+
`[errata] project alias NOT auto-migrated for ${r.entry.name} (${alias} \u2192 ${locator}: repo name differs \u2014 a fork/repoint, not a rename). Run scripts/migrate-project-alias.ts if this really is the same repo.`
|
|
83757
|
+
);
|
|
83758
|
+
continue;
|
|
83759
|
+
}
|
|
83760
|
+
const m = migrateProjectAlias(sharedStore, { from: alias, to: locator, ts });
|
|
83761
|
+
if (m.nodesRewritten || m.edgesRewritten) {
|
|
83762
|
+
console.log(
|
|
83763
|
+
`[errata] project alias migrated for ${r.entry.name}: ${alias} \u2192 ${locator} (${m.nodesRewritten} nodes, ${m.edgesRewritten} edges)`
|
|
83764
|
+
);
|
|
83765
|
+
}
|
|
83766
|
+
}
|
|
83767
|
+
delete profile.repoLocatorAliases;
|
|
83768
|
+
const onDisk = loadProfile(r.root);
|
|
83769
|
+
if (onDisk) {
|
|
83770
|
+
delete onDisk.repoLocatorAliases;
|
|
83771
|
+
saveProfile(r.root, onDisk);
|
|
83772
|
+
}
|
|
83773
|
+
} catch (err2) {
|
|
83774
|
+
console.warn(`[errata] project-alias migration failed for ${r.entry.name} (kept for next pass): ${err2 instanceof Error ? err2.message : err2}`);
|
|
83775
|
+
}
|
|
83776
|
+
}
|
|
83777
|
+
let inlineRecords = records;
|
|
83778
|
+
if (consolidateWorker) {
|
|
83779
|
+
const fileBacked = records.filter((r) => r.engine.paths.castalia !== ":memory:");
|
|
83780
|
+
if (fileBacked.length > 0) {
|
|
83781
|
+
try {
|
|
83782
|
+
const res = await consolidateWorker.run({
|
|
83783
|
+
kind: "percolateAll",
|
|
83784
|
+
sharedDbPath,
|
|
83785
|
+
ts,
|
|
83786
|
+
workspaces: fileBacked.map((r) => ({
|
|
83787
|
+
recordId: r.id,
|
|
83788
|
+
// Repo-keyed when linked: two checkouts of one repo are ONE
|
|
83789
|
+
// project in observedInProjects, not two independent
|
|
83790
|
+
// observations (PJ-local — the Q9 independence gate).
|
|
83791
|
+
workspaceId: r.engine.profile.repoLocator ?? r.engine.profile.id,
|
|
83792
|
+
castaliaPath: r.engine.paths.castalia
|
|
83793
|
+
}))
|
|
83794
|
+
});
|
|
83795
|
+
for (const [id, v] of res) out2.set(id, v);
|
|
83796
|
+
inlineRecords = records.filter((r) => r.engine.paths.castalia === ":memory:");
|
|
83797
|
+
} catch (err2) {
|
|
83798
|
+
console.warn(`[errata] percolate worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83799
|
+
}
|
|
83800
|
+
}
|
|
83801
|
+
}
|
|
83802
|
+
for (const r of inlineRecords) {
|
|
83803
|
+
const done = markPass(`percolate-inline:${r.entry.name}`);
|
|
83804
|
+
try {
|
|
83805
|
+
out2.set(
|
|
83806
|
+
r.id,
|
|
83807
|
+
percolate(r.engine.store, sharedStore, {
|
|
83808
|
+
workspaceId: r.engine.profile.repoLocator ?? r.engine.profile.id,
|
|
83809
|
+
ts
|
|
83810
|
+
})
|
|
83811
|
+
);
|
|
83812
|
+
} catch (err2) {
|
|
83813
|
+
console.warn(
|
|
83814
|
+
`[errata] percolate failed for ${r.entry.name}: ${err2 instanceof Error ? err2.message : err2}`
|
|
83815
|
+
);
|
|
83816
|
+
} finally {
|
|
83817
|
+
done();
|
|
83818
|
+
}
|
|
83819
|
+
}
|
|
83820
|
+
return out2;
|
|
83821
|
+
},
|
|
83822
|
+
async induceL2Abstractions(touched) {
|
|
83823
|
+
const ctxByWs = /* @__PURE__ */ new Map();
|
|
83824
|
+
for (const r of records) ctxByWs.set(r.engine.profile.id, contextKey(r.engine.profile));
|
|
83825
|
+
const ts = Date.now();
|
|
83826
|
+
if (consolidateWorker) {
|
|
83827
|
+
try {
|
|
83828
|
+
return await consolidateWorker.run({
|
|
83829
|
+
kind: "induceAbstractions",
|
|
83830
|
+
sharedDbPath,
|
|
83831
|
+
ts,
|
|
83832
|
+
ctxByWs: Object.fromEntries(ctxByWs),
|
|
83833
|
+
originMachine: machineId(),
|
|
83834
|
+
...touched ? { touched: new Set(touched) } : {}
|
|
83835
|
+
});
|
|
83836
|
+
} catch (err2) {
|
|
83837
|
+
console.warn(`[errata] abstraction worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83838
|
+
}
|
|
83839
|
+
}
|
|
83840
|
+
return induceAbstractions(sharedStore, {
|
|
83841
|
+
contextOf: (ws) => ctxByWs.get(ws) ?? null,
|
|
83842
|
+
ts,
|
|
83843
|
+
// Source-tag minted principles to this machine so they stay attributable
|
|
83844
|
+
// across the user's machines once L3 sync lands (P4 / §8.6).
|
|
83845
|
+
originMachine: machineId(),
|
|
83846
|
+
...touched ? { touched } : {}
|
|
83847
|
+
});
|
|
83848
|
+
},
|
|
83849
|
+
async induceL2Triage() {
|
|
83850
|
+
const ctxByWs = /* @__PURE__ */ new Map();
|
|
83851
|
+
for (const r of records) {
|
|
83852
|
+
const p = r.engine.profile;
|
|
83853
|
+
ctxByWs.set(p.id, {
|
|
83854
|
+
features: { stack: [...p.stack].sort().join(","), domain: p.domains[0] ?? p.languages[0] ?? "" }
|
|
83855
|
+
});
|
|
83856
|
+
}
|
|
83857
|
+
const ts = Date.now();
|
|
83858
|
+
if (consolidateWorker) {
|
|
83859
|
+
try {
|
|
83860
|
+
return await consolidateWorker.run({
|
|
83861
|
+
kind: "induceTriage",
|
|
83862
|
+
sharedDbPath,
|
|
83863
|
+
ts,
|
|
83864
|
+
triageCtxByWs: Object.fromEntries(ctxByWs)
|
|
83865
|
+
});
|
|
83866
|
+
} catch (err2) {
|
|
83867
|
+
console.warn(`[errata] triage-induction worker failed \u2014 running inline: ${err2 instanceof Error ? err2.message : err2}`);
|
|
83868
|
+
}
|
|
83869
|
+
}
|
|
83870
|
+
return induceTriage(sharedStore, {
|
|
83871
|
+
contextOf: (ws) => ctxByWs.get(ws) ?? null,
|
|
83872
|
+
ts
|
|
83873
|
+
});
|
|
83874
|
+
},
|
|
83875
|
+
async revisitStaleTriageRoutes() {
|
|
83876
|
+
if (consolidateWorker) {
|
|
83877
|
+
try {
|
|
83878
|
+
return await consolidateWorker.run({ kind: "revisitTriage", sharedDbPath, ts: Date.now() });
|
|
83879
|
+
} catch {
|
|
83880
|
+
}
|
|
83881
|
+
}
|
|
83882
|
+
return revisitStaleRoutes(sharedStore, Date.now());
|
|
83883
|
+
},
|
|
83884
|
+
async revisitStalePrinciples() {
|
|
83885
|
+
if (consolidateWorker) {
|
|
83886
|
+
try {
|
|
83887
|
+
return await consolidateWorker.run({ kind: "revisitPrinciples", sharedDbPath, ts: Date.now() });
|
|
83888
|
+
} catch {
|
|
83889
|
+
}
|
|
83890
|
+
}
|
|
83891
|
+
return revisitContradictedPrinciples(sharedStore, Date.now());
|
|
83892
|
+
},
|
|
83893
|
+
async syncPrinciplesPublic() {
|
|
83894
|
+
if (!loadConfig().consent.sync) return { uploaded: 0, skipped: "consent-off" };
|
|
83895
|
+
const ignore2 = loadClaimIgnorePatterns(globalDir());
|
|
83896
|
+
const primaryLanguage = machineDominantLanguage();
|
|
83897
|
+
const payload = buildPrincipleIngest(sharedStore, DAEMON_VERSION, ignore2, 2, {
|
|
83898
|
+
...primaryLanguage ? { primaryLanguage } : {}
|
|
83899
|
+
});
|
|
83900
|
+
if (!payload) return { uploaded: 0 };
|
|
83901
|
+
const res = await cloudNow().ingest(payload);
|
|
83902
|
+
return { uploaded: res.accepted };
|
|
83903
|
+
},
|
|
83904
|
+
async syncTriagePublic() {
|
|
83905
|
+
const cfg2 = loadConfig();
|
|
83906
|
+
if (!cfg2.consent.sync) return { uploaded: 0, skipped: "consent-off" };
|
|
83907
|
+
const ignore2 = loadClaimIgnorePatterns(globalDir());
|
|
83908
|
+
const primaryLanguage = machineDominantLanguage();
|
|
83909
|
+
const languages = records.flatMap((r) => r.engine.profile.languages ?? []);
|
|
83910
|
+
const packages = cfg2.consent.contributePackages ? records.flatMap(
|
|
83911
|
+
(r) => r.engine.store.findNodesByLabel("Package").map((p) => ({
|
|
83912
|
+
name: String(p.attrs["name"] ?? ""),
|
|
83913
|
+
purl: String(p.attrs["purl"] ?? p.id)
|
|
83914
|
+
}))
|
|
83915
|
+
) : [];
|
|
83916
|
+
const tokenAnchors = buildBucketTokenAnchors(languages, packages);
|
|
83917
|
+
const payload = buildTriageIngest(sharedStore, DAEMON_VERSION, ignore2, 2, {
|
|
83501
83918
|
...primaryLanguage ? { primaryLanguage } : {},
|
|
83502
83919
|
tokenAnchors,
|
|
83503
83920
|
// Scoped bucket keys carry package names — same consent as the purls.
|
|
@@ -83733,660 +84150,274 @@ async function startMultiDaemon(opts = {}) {
|
|
|
83733
84150
|
// failing every pass. Same total, different bug.
|
|
83734
84151
|
blockedRepeat,
|
|
83735
84152
|
rejectedRepeat
|
|
83736
|
-
});
|
|
83737
|
-
return res.accepted;
|
|
83738
|
-
});
|
|
83739
|
-
}
|
|
83740
|
-
}
|
|
83741
|
-
return { uploaded, ...errors.length > 0 ? { errors } : {} };
|
|
83742
|
-
},
|
|
83743
|
-
async backfillCausalEdges() {
|
|
83744
|
-
if (!loadConfig().consent.sync) return { accepted: 0, rejected: 0, skipped: "consent-off" };
|
|
83745
|
-
const client = cloudNow();
|
|
83746
|
-
let accepted = 0;
|
|
83747
|
-
let rejected = 0;
|
|
83748
|
-
const errors = [];
|
|
83749
|
-
for (const r of records) {
|
|
83750
|
-
const payload = buildCausalEdgeBackfill(r.engine.store, r.engine.profile, DAEMON_VERSION);
|
|
83751
|
-
if (!payload) continue;
|
|
83752
|
-
try {
|
|
83753
|
-
const res = await client.ingest(payload);
|
|
83754
|
-
accepted += res.accepted;
|
|
83755
|
-
rejected += res.rejected;
|
|
83756
|
-
if (res.rejected > 0) errors.push(...res.violations.slice(0, 5));
|
|
83757
|
-
} catch (err2) {
|
|
83758
|
-
errors.push(
|
|
83759
|
-
`[${r.engine.profile.name ?? r.engine.profile.id}] backfill: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
83760
|
-
);
|
|
83761
|
-
}
|
|
83762
|
-
}
|
|
83763
|
-
return { accepted, rejected, ...errors.length > 0 ? { errors } : {} };
|
|
83764
|
-
},
|
|
83765
|
-
async pullTriagePublic() {
|
|
83766
|
-
if (!loadConfig().consent.sync) return { merged: 0, skipped: "consent-off" };
|
|
83767
|
-
const stack = /* @__PURE__ */ new Set();
|
|
83768
|
-
const domains = /* @__PURE__ */ new Set();
|
|
83769
|
-
for (const r of records) {
|
|
83770
|
-
for (const s of r.engine.profile.stack) stack.add(s);
|
|
83771
|
-
for (const d of r.engine.profile.domains) domains.add(d);
|
|
83772
|
-
}
|
|
83773
|
-
const merged = await pullCloudTriage(sharedStore, cloudNow(), {
|
|
83774
|
-
stack: [...stack],
|
|
83775
|
-
domains: [...domains]
|
|
83776
|
-
});
|
|
83777
|
-
return { merged };
|
|
83778
|
-
},
|
|
83779
|
-
async syncPrinciplesPrivate() {
|
|
83780
|
-
if (!loadConfig().consent.sync) return null;
|
|
83781
|
-
return syncPrinciples(sharedStore, cloudNow(), {
|
|
83782
|
-
machine: machineId(),
|
|
83783
|
-
daemonVersion: DAEMON_VERSION,
|
|
83784
|
-
ts: Date.now()
|
|
83785
|
-
});
|
|
83786
|
-
},
|
|
83787
|
-
async syncSkillsAll() {
|
|
83788
|
-
let written = 0;
|
|
83789
|
-
let pruned2 = 0;
|
|
83790
|
-
let stale;
|
|
83791
|
-
for (const r of records) {
|
|
83792
|
-
const s = await r.engine.fetchSkills();
|
|
83793
|
-
written += s.written;
|
|
83794
|
-
pruned2 += s.pruned;
|
|
83795
|
-
if (s.staleDaemon) stale = s.staleDaemon;
|
|
83796
|
-
}
|
|
83797
|
-
const next = pendingFromServerVerdict(
|
|
83798
|
-
stale?.latest,
|
|
83799
|
-
loadConfig().updateChannel
|
|
83800
|
-
);
|
|
83801
|
-
if ((next?.latest ?? null) !== (serverPending?.latest ?? null)) {
|
|
83802
|
-
serverPending = next;
|
|
83803
|
-
for (const rec of records) rec.engine.markContextDirty();
|
|
83804
|
-
}
|
|
83805
|
-
return { written, pruned: pruned2 };
|
|
83806
|
-
},
|
|
83807
|
-
async sessionBoundaryFlush() {
|
|
83808
|
-
if (!settleRunning) {
|
|
83809
|
-
settleRunning = true;
|
|
83810
|
-
try {
|
|
83811
|
-
for (const r of records) {
|
|
83812
|
-
await r.engine.embedSettled();
|
|
83813
|
-
}
|
|
83814
|
-
} finally {
|
|
83815
|
-
settleRunning = false;
|
|
83816
|
-
}
|
|
83817
|
-
}
|
|
83818
|
-
if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
|
|
83819
|
-
beginFlush("instances");
|
|
83820
|
-
try {
|
|
83821
|
-
flushStage("instances");
|
|
83822
|
-
const inst = await daemon.syncInstancesPublic();
|
|
83823
|
-
flushStage("skills");
|
|
83824
|
-
const skills = await daemon.syncSkillsAll();
|
|
83825
|
-
return {
|
|
83826
|
-
uploaded: inst.uploaded,
|
|
83827
|
-
written: skills.written,
|
|
83828
|
-
pruned: skills.pruned,
|
|
83829
|
-
...inst.errors ? { errors: inst.errors } : {}
|
|
83830
|
-
};
|
|
83831
|
-
} finally {
|
|
83832
|
-
endFlush();
|
|
84153
|
+
});
|
|
84154
|
+
return res.accepted;
|
|
84155
|
+
});
|
|
84156
|
+
}
|
|
83833
84157
|
}
|
|
84158
|
+
return { uploaded, ...errors.length > 0 ? { errors } : {} };
|
|
83834
84159
|
},
|
|
83835
|
-
|
|
83836
|
-
|
|
83837
|
-
|
|
83838
|
-
|
|
83839
|
-
|
|
83840
|
-
|
|
83841
|
-
|
|
83842
|
-
|
|
83843
|
-
|
|
83844
|
-
|
|
83845
|
-
|
|
83846
|
-
|
|
83847
|
-
|
|
83848
|
-
|
|
83849
|
-
|
|
83850
|
-
|
|
83851
|
-
|
|
83852
|
-
|
|
83853
|
-
|
|
83854
|
-
try {
|
|
83855
|
-
sharedStore.close();
|
|
83856
|
-
} catch {
|
|
84160
|
+
async backfillCausalEdges() {
|
|
84161
|
+
if (!loadConfig().consent.sync) return { accepted: 0, rejected: 0, skipped: "consent-off" };
|
|
84162
|
+
const client = cloudNow();
|
|
84163
|
+
let accepted = 0;
|
|
84164
|
+
let rejected = 0;
|
|
84165
|
+
const errors = [];
|
|
84166
|
+
for (const r of records) {
|
|
84167
|
+
const payload = buildCausalEdgeBackfill(r.engine.store, r.engine.profile, DAEMON_VERSION);
|
|
84168
|
+
if (!payload) continue;
|
|
84169
|
+
try {
|
|
84170
|
+
const res = await client.ingest(payload);
|
|
84171
|
+
accepted += res.accepted;
|
|
84172
|
+
rejected += res.rejected;
|
|
84173
|
+
if (res.rejected > 0) errors.push(...res.violations.slice(0, 5));
|
|
84174
|
+
} catch (err2) {
|
|
84175
|
+
errors.push(
|
|
84176
|
+
`[${r.engine.profile.name ?? r.engine.profile.id}] backfill: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
84177
|
+
);
|
|
84178
|
+
}
|
|
83857
84179
|
}
|
|
83858
|
-
|
|
83859
|
-
|
|
83860
|
-
|
|
83861
|
-
|
|
83862
|
-
|
|
83863
|
-
|
|
83864
|
-
const
|
|
83865
|
-
|
|
83866
|
-
|
|
83867
|
-
flushStage("instances");
|
|
83868
|
-
const instances = await daemon.syncInstancesPublic();
|
|
83869
|
-
flushStage("edge-backfill");
|
|
83870
|
-
const edgeBackfill = await daemon.backfillCausalEdges();
|
|
83871
|
-
return c.json({ principles, triage, instances, edgeBackfill });
|
|
83872
|
-
} catch (err2) {
|
|
83873
|
-
return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
|
|
83874
|
-
} finally {
|
|
83875
|
-
endFlush();
|
|
83876
|
-
}
|
|
83877
|
-
});
|
|
83878
|
-
app.get("/api/sync", (c) => c.json(flushSnapshot()));
|
|
83879
|
-
app.post("/api/tick", async (c) => {
|
|
83880
|
-
try {
|
|
83881
|
-
return c.json({ reports: Object.fromEntries(await daemon.tickAll()) });
|
|
83882
|
-
} catch (err2) {
|
|
83883
|
-
return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
|
|
83884
|
-
}
|
|
83885
|
-
});
|
|
83886
|
-
return daemon;
|
|
83887
|
-
}
|
|
83888
|
-
|
|
83889
|
-
// src/cli.ts
|
|
83890
|
-
init_src5();
|
|
83891
|
-
init_git_remote();
|
|
83892
|
-
init_config();
|
|
83893
|
-
init_src7();
|
|
83894
|
-
init_cloud_auth();
|
|
83895
|
-
|
|
83896
|
-
// src/agent-switch.ts
|
|
83897
|
-
function parseUseArgs(args2) {
|
|
83898
|
-
const positional = [];
|
|
83899
|
-
let clear = false;
|
|
83900
|
-
for (const arg of args2) {
|
|
83901
|
-
if (arg === "--none" || arg === "--clear") {
|
|
83902
|
-
clear = true;
|
|
83903
|
-
continue;
|
|
83904
|
-
}
|
|
83905
|
-
if (arg.startsWith("--")) continue;
|
|
83906
|
-
positional.push(arg);
|
|
83907
|
-
}
|
|
83908
|
-
if (clear) return { kind: "clear" };
|
|
83909
|
-
const handle2 = positional[0];
|
|
83910
|
-
if (!handle2) return { kind: "show" };
|
|
83911
|
-
return { kind: "set", handle: handle2 };
|
|
83912
|
-
}
|
|
83913
|
-
function normalizeHandle(handle2) {
|
|
83914
|
-
return handle2.trim().replace(/^@/, "").toLowerCase();
|
|
83915
|
-
}
|
|
83916
|
-
function findActable(agents, handle2) {
|
|
83917
|
-
const want = normalizeHandle(handle2);
|
|
83918
|
-
return agents.find((a) => normalizeHandle(a.handle) === want) ?? null;
|
|
83919
|
-
}
|
|
83920
|
-
async function runUse(action, ctx) {
|
|
83921
|
-
switch (action.kind) {
|
|
83922
|
-
case "clear":
|
|
83923
|
-
return runClear(ctx);
|
|
83924
|
-
case "show":
|
|
83925
|
-
return runShow(ctx);
|
|
83926
|
-
case "set":
|
|
83927
|
-
return runSet(action.handle, ctx);
|
|
83928
|
-
}
|
|
83929
|
-
}
|
|
83930
|
-
function runClear(ctx) {
|
|
83931
|
-
const active2 = ctx.getActiveAgent();
|
|
83932
|
-
ctx.clearActiveAgent();
|
|
83933
|
-
if (active2) {
|
|
83934
|
-
ctx.log(`no longer acting as ${active2.handle} \u2014 the daemon acts as your default identity`);
|
|
83935
|
-
} else {
|
|
83936
|
-
ctx.log(`already on the default identity \u2014 nothing to clear`);
|
|
83937
|
-
}
|
|
83938
|
-
return 0;
|
|
83939
|
-
}
|
|
83940
|
-
async function runShow(ctx) {
|
|
83941
|
-
const active2 = ctx.getActiveAgent();
|
|
83942
|
-
ctx.log(active2 ? `active agent: ${active2.handle} (id ${active2.id})` : `active agent: (default identity)`);
|
|
83943
|
-
if (!ctx.loggedIn) {
|
|
83944
|
-
ctx.log(` log in to list the agents you can act as: errata login`);
|
|
83945
|
-
return 0;
|
|
83946
|
-
}
|
|
83947
|
-
let resp;
|
|
83948
|
-
try {
|
|
83949
|
-
resp = await ctx.actableAgents();
|
|
83950
|
-
} catch (err2) {
|
|
83951
|
-
ctx.log(` couldn't list actable agents: ${errText(err2)}`);
|
|
83952
|
-
return 1;
|
|
83953
|
-
}
|
|
83954
|
-
printActable(resp, active2, ctx);
|
|
83955
|
-
return 0;
|
|
83956
|
-
}
|
|
83957
|
-
async function runSet(handle2, ctx) {
|
|
83958
|
-
if (!ctx.loggedIn) {
|
|
83959
|
-
ctx.log(`not logged in \u2014 run \`errata login\` first, then \`errata use ${handle2}\``);
|
|
83960
|
-
return 1;
|
|
83961
|
-
}
|
|
83962
|
-
let resp;
|
|
83963
|
-
try {
|
|
83964
|
-
resp = await ctx.actableAgents();
|
|
83965
|
-
} catch (err2) {
|
|
83966
|
-
ctx.log(`couldn't verify \`${handle2}\`: ${errText(err2)}`);
|
|
83967
|
-
return 1;
|
|
83968
|
-
}
|
|
83969
|
-
const match2 = findActable(resp.agents, handle2);
|
|
83970
|
-
if (!match2) {
|
|
83971
|
-
ctx.log(`unknown agent: ${handle2}`);
|
|
83972
|
-
if (resp.agents.length === 0) {
|
|
83973
|
-
ctx.log(` you can't act as any agents yet.`);
|
|
83974
|
-
} else {
|
|
83975
|
-
ctx.log(` agents you can act as: ${resp.agents.map((a) => a.handle).join(", ")}`);
|
|
83976
|
-
}
|
|
83977
|
-
return 2;
|
|
83978
|
-
}
|
|
83979
|
-
ctx.setActiveAgent({ handle: match2.handle, id: match2.id });
|
|
83980
|
-
ctx.log(`now acting as ${match2.handle} (id ${match2.id}) \u2014 saved; persists across daemon restarts`);
|
|
83981
|
-
return 0;
|
|
83982
|
-
}
|
|
83983
|
-
function printActable(resp, active2, ctx) {
|
|
83984
|
-
if (resp.agents.length === 0) {
|
|
83985
|
-
ctx.log(` (no agents you can act as)`);
|
|
83986
|
-
return;
|
|
83987
|
-
}
|
|
83988
|
-
ctx.log(` agents you can act as:`);
|
|
83989
|
-
for (const a of resp.agents) {
|
|
83990
|
-
const marks = [];
|
|
83991
|
-
if (active2 && a.id === active2.id) marks.push("active");
|
|
83992
|
-
if (a.id === resp.defaultAgentId) marks.push("default");
|
|
83993
|
-
const suffix = marks.length ? ` (${marks.join(", ")})` : "";
|
|
83994
|
-
const scope = a.scope ? ` \xB7 ${a.scope}` : "";
|
|
83995
|
-
ctx.log(` ${a.handle}${scope}${suffix}`);
|
|
83996
|
-
}
|
|
83997
|
-
}
|
|
83998
|
-
function errText(err2) {
|
|
83999
|
-
return err2 instanceof Error ? err2.message : String(err2);
|
|
84000
|
-
}
|
|
84001
|
-
|
|
84002
|
-
// src/cli.ts
|
|
84003
|
-
init_cloud_endpoint_policy();
|
|
84004
|
-
|
|
84005
|
-
// src/oauth-login.ts
|
|
84006
|
-
init_src7();
|
|
84007
|
-
init_cloud_auth();
|
|
84008
|
-
import { createServer } from "node:http";
|
|
84009
|
-
var DEFAULT_OAUTH_SCOPE = "openid profile graph:read graph:write mcp:tools";
|
|
84010
|
-
var strip = (u) => u.replace(/\/+$/, "");
|
|
84011
|
-
function deviceEndpoints(tokenEndpoint) {
|
|
84012
|
-
const token = new URL(tokenEndpoint);
|
|
84013
|
-
const authRoot = token.pathname.replace(/\/(?:oauth2|mcp)\/token\/?$/, "");
|
|
84014
|
-
if (authRoot === token.pathname) throw new Error("authorization server exposes no device endpoint");
|
|
84015
|
-
const prefix = authRoot.replace(/\/api\/auth\/?$/, "");
|
|
84016
|
-
const at = (path2) => new URL(`${path2}`, `${token.origin}${prefix || "/"}`).toString();
|
|
84017
|
-
return {
|
|
84018
|
-
code: at(`${prefix}/api/auth/device/code`),
|
|
84019
|
-
token: at(`${prefix}/api/auth/device/token`),
|
|
84020
|
-
resource: at(`${prefix}/api/gate/session-token`)
|
|
84021
|
-
};
|
|
84022
|
-
}
|
|
84023
|
-
async function loginOAuthDevice(opts) {
|
|
84024
|
-
const fetchFn = opts.fetchFn ?? fetch;
|
|
84025
|
-
const endpoints = await discoverEndpoints(opts.cloudUrl, fetchFn);
|
|
84026
|
-
const device = deviceEndpoints(endpoints.tokenEndpoint);
|
|
84027
|
-
const clientId = oauthClientId();
|
|
84028
|
-
const scope = opts.scope ?? DEFAULT_OAUTH_SCOPE;
|
|
84029
|
-
const codeResponse = await fetchFn(device.code, {
|
|
84030
|
-
method: "POST",
|
|
84031
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84032
|
-
body: JSON.stringify({ client_id: clientId, scope })
|
|
84033
|
-
});
|
|
84034
|
-
if (!codeResponse.ok) {
|
|
84035
|
-
throw new Error(`device authorization failed: HTTP ${codeResponse.status} ${(await codeResponse.text()).slice(0, 200)}`);
|
|
84036
|
-
}
|
|
84037
|
-
const code = await codeResponse.json();
|
|
84038
|
-
if (!code.device_code || !code.user_code || !code.verification_uri || !code.expires_in) {
|
|
84039
|
-
throw new Error("device authorization returned an incomplete response");
|
|
84040
|
-
}
|
|
84041
|
-
opts.onVerification?.({
|
|
84042
|
-
url: code.verification_uri_complete ?? code.verification_uri,
|
|
84043
|
-
userCode: code.user_code,
|
|
84044
|
-
expiresInSec: code.expires_in
|
|
84045
|
-
});
|
|
84046
|
-
const deadline = Date.now() + Math.min(code.expires_in * 1e3, opts.timeoutMs ?? Number.POSITIVE_INFINITY);
|
|
84047
|
-
let intervalMs = Math.max(2, code.interval ?? 5) * 1e3;
|
|
84048
|
-
let deviceSession = null;
|
|
84049
|
-
let approvedInstallation = null;
|
|
84050
|
-
while (Date.now() < deadline) {
|
|
84051
|
-
await (opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))))(intervalMs);
|
|
84052
|
-
const poll = await fetchFn(device.token, {
|
|
84053
|
-
method: "POST",
|
|
84054
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84055
|
-
body: JSON.stringify({
|
|
84056
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
84057
|
-
device_code: code.device_code,
|
|
84058
|
-
client_id: clientId
|
|
84059
|
-
})
|
|
84060
|
-
});
|
|
84061
|
-
const payload = await poll.json().catch(() => null);
|
|
84062
|
-
if (!poll.ok) {
|
|
84063
|
-
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
84064
|
-
if (error48 === "authorization_pending") continue;
|
|
84065
|
-
if (error48 === "slow_down") {
|
|
84066
|
-
intervalMs += 5e3;
|
|
84067
|
-
continue;
|
|
84180
|
+
return { accepted, rejected, ...errors.length > 0 ? { errors } : {} };
|
|
84181
|
+
},
|
|
84182
|
+
async pullTriagePublic() {
|
|
84183
|
+
if (!loadConfig().consent.sync) return { merged: 0, skipped: "consent-off" };
|
|
84184
|
+
const stack = /* @__PURE__ */ new Set();
|
|
84185
|
+
const domains = /* @__PURE__ */ new Set();
|
|
84186
|
+
for (const r of records) {
|
|
84187
|
+
for (const s of r.engine.profile.stack) stack.add(s);
|
|
84188
|
+
for (const d of r.engine.profile.domains) domains.add(d);
|
|
84068
84189
|
}
|
|
84069
|
-
|
|
84070
|
-
|
|
84071
|
-
|
|
84072
|
-
|
|
84073
|
-
|
|
84074
|
-
}
|
|
84075
|
-
if (!deviceSession || !approvedInstallation) throw new Error("device authorization expired");
|
|
84076
|
-
if (approvedInstallation !== opts.installationId) {
|
|
84077
|
-
throw new Error("approved installation does not match the terminal request");
|
|
84078
|
-
}
|
|
84079
|
-
const resourceResponse = await fetchFn(device.resource, {
|
|
84080
|
-
method: "POST",
|
|
84081
|
-
headers: {
|
|
84082
|
-
authorization: `Bearer ${deviceSession}`,
|
|
84083
|
-
"content-type": "application/json",
|
|
84084
|
-
accept: "application/json"
|
|
84190
|
+
const merged = await pullCloudTriage(sharedStore, cloudNow(), {
|
|
84191
|
+
stack: [...stack],
|
|
84192
|
+
domains: [...domains]
|
|
84193
|
+
});
|
|
84194
|
+
return { merged };
|
|
84085
84195
|
},
|
|
84086
|
-
|
|
84087
|
-
|
|
84088
|
-
|
|
84089
|
-
|
|
84090
|
-
|
|
84091
|
-
|
|
84092
|
-
|
|
84093
|
-
|
|
84094
|
-
|
|
84095
|
-
|
|
84096
|
-
|
|
84097
|
-
|
|
84098
|
-
|
|
84099
|
-
|
|
84100
|
-
|
|
84101
|
-
|
|
84102
|
-
|
|
84103
|
-
|
|
84104
|
-
|
|
84105
|
-
|
|
84106
|
-
|
|
84107
|
-
|
|
84108
|
-
|
|
84109
|
-
|
|
84110
|
-
|
|
84111
|
-
|
|
84112
|
-
|
|
84113
|
-
|
|
84114
|
-
|
|
84115
|
-
|
|
84116
|
-
|
|
84117
|
-
|
|
84118
|
-
|
|
84119
|
-
|
|
84120
|
-
|
|
84121
|
-
|
|
84196
|
+
async syncPrinciplesPrivate() {
|
|
84197
|
+
if (!loadConfig().consent.sync) return null;
|
|
84198
|
+
return syncPrinciples(sharedStore, cloudNow(), {
|
|
84199
|
+
machine: machineId(),
|
|
84200
|
+
daemonVersion: DAEMON_VERSION,
|
|
84201
|
+
ts: Date.now()
|
|
84202
|
+
});
|
|
84203
|
+
},
|
|
84204
|
+
async syncSkillsAll() {
|
|
84205
|
+
let written = 0;
|
|
84206
|
+
let pruned2 = 0;
|
|
84207
|
+
let stale;
|
|
84208
|
+
for (const r of records) {
|
|
84209
|
+
const s = await r.engine.fetchSkills();
|
|
84210
|
+
written += s.written;
|
|
84211
|
+
pruned2 += s.pruned;
|
|
84212
|
+
if (s.staleDaemon) stale = s.staleDaemon;
|
|
84213
|
+
}
|
|
84214
|
+
const next = pendingFromServerVerdict(
|
|
84215
|
+
stale?.latest,
|
|
84216
|
+
loadConfig().updateChannel
|
|
84217
|
+
);
|
|
84218
|
+
if ((next?.latest ?? null) !== (serverPending?.latest ?? null)) {
|
|
84219
|
+
serverPending = next;
|
|
84220
|
+
for (const rec of records) rec.engine.markContextDirty();
|
|
84221
|
+
}
|
|
84222
|
+
return { written, pruned: pruned2 };
|
|
84223
|
+
},
|
|
84224
|
+
async sessionBoundaryFlush() {
|
|
84225
|
+
if (!settleRunning) {
|
|
84226
|
+
settleRunning = true;
|
|
84227
|
+
try {
|
|
84228
|
+
for (const r of records) {
|
|
84229
|
+
await r.engine.embedSettled();
|
|
84230
|
+
}
|
|
84231
|
+
} finally {
|
|
84232
|
+
settleRunning = false;
|
|
84233
|
+
}
|
|
84234
|
+
}
|
|
84235
|
+
if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
|
|
84236
|
+
beginFlush("instances");
|
|
84237
|
+
try {
|
|
84238
|
+
flushStage("instances");
|
|
84239
|
+
const inst = await daemon.syncInstancesPublic();
|
|
84240
|
+
flushStage("skills");
|
|
84241
|
+
const skills = await daemon.syncSkillsAll();
|
|
84122
84242
|
return {
|
|
84123
|
-
|
|
84124
|
-
|
|
84125
|
-
|
|
84243
|
+
uploaded: inst.uploaded,
|
|
84244
|
+
written: skills.written,
|
|
84245
|
+
pruned: skills.pruned,
|
|
84246
|
+
...inst.errors ? { errors: inst.errors } : {}
|
|
84126
84247
|
};
|
|
84248
|
+
} finally {
|
|
84249
|
+
endFlush();
|
|
84250
|
+
}
|
|
84251
|
+
},
|
|
84252
|
+
onSessionBoundary(cb) {
|
|
84253
|
+
boundaryListeners.push(cb);
|
|
84254
|
+
},
|
|
84255
|
+
async stop() {
|
|
84256
|
+
clearInterval(idleSweep);
|
|
84257
|
+
clearInterval(codexWake);
|
|
84258
|
+
clearTimeout(codexWakeBoot);
|
|
84259
|
+
try {
|
|
84260
|
+
const cur = readFileSync30(lockPath, "utf8");
|
|
84261
|
+
if (JSON.parse(cur).pid === process.pid) unlinkSync3(lockPath);
|
|
84262
|
+
} catch {
|
|
84263
|
+
}
|
|
84264
|
+
await new Promise(
|
|
84265
|
+
(res) => server.close(res)
|
|
84266
|
+
);
|
|
84267
|
+
updatePoller?.stop();
|
|
84268
|
+
healthSentinel.stop();
|
|
84269
|
+
for (const r of records) await r.engine.stop();
|
|
84270
|
+
if (consolidateWorker) await consolidateWorker.stop();
|
|
84271
|
+
try {
|
|
84272
|
+
sharedStore.close();
|
|
84273
|
+
} catch {
|
|
84127
84274
|
}
|
|
84128
84275
|
}
|
|
84129
|
-
} catch {
|
|
84130
|
-
}
|
|
84131
|
-
const base = strip(consoleUrl);
|
|
84132
|
-
return {
|
|
84133
|
-
authorizationEndpoint: `${base}/api/auth/oauth2/authorize`,
|
|
84134
|
-
tokenEndpoint: `${base}/api/auth/oauth2/token`
|
|
84135
84276
|
};
|
|
84136
|
-
|
|
84137
|
-
|
|
84138
|
-
|
|
84139
|
-
|
|
84140
|
-
|
|
84141
|
-
|
|
84142
|
-
|
|
84143
|
-
|
|
84144
|
-
|
|
84145
|
-
|
|
84146
|
-
|
|
84147
|
-
}
|
|
84148
|
-
|
|
84149
|
-
|
|
84150
|
-
|
|
84151
|
-
|
|
84152
|
-
|
|
84153
|
-
|
|
84154
|
-
|
|
84155
|
-
|
|
84156
|
-
|
|
84157
|
-
|
|
84158
|
-
|
|
84159
|
-
|
|
84160
|
-
res.end();
|
|
84161
|
-
return;
|
|
84162
|
-
}
|
|
84163
|
-
const addr2 = server.address();
|
|
84164
|
-
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
84165
|
-
const err2 = u.searchParams.get("error");
|
|
84166
|
-
const code2 = u.searchParams.get("code");
|
|
84167
|
-
const returnedState = u.searchParams.get("state");
|
|
84168
|
-
if (err2 || !code2 || returnedState !== state) {
|
|
84169
|
-
res.writeHead(400, { "content-type": "text/plain" });
|
|
84170
|
-
res.end(`login failed: ${err2 ?? (returnedState !== state ? "state_mismatch" : "no_code")}`);
|
|
84171
|
-
server.close();
|
|
84172
|
-
reject(new Error(err2 ?? "invalid_callback"));
|
|
84173
|
-
return;
|
|
84174
|
-
}
|
|
84175
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
84176
|
-
res.end("<html><body>inErrata: you're logged in. You can close this tab.</body></html>");
|
|
84177
|
-
server.close();
|
|
84178
|
-
resolve7({ code: code2, redirectUri: `http://127.0.0.1:${port}/callback`, clientId: activeClientId });
|
|
84179
|
-
});
|
|
84180
|
-
let activeClientId = oauthClientId();
|
|
84181
|
-
server.on("error", reject);
|
|
84182
|
-
server.listen(0, "127.0.0.1", () => {
|
|
84183
|
-
void (async () => {
|
|
84184
|
-
try {
|
|
84185
|
-
const addr2 = server.address();
|
|
84186
|
-
const port = typeof addr2 === "object" && addr2 ? addr2.port : 0;
|
|
84187
|
-
const redirectUri2 = `http://127.0.0.1:${port}/callback`;
|
|
84188
|
-
if (endpoints.registrationEndpoint) {
|
|
84189
|
-
const registered = await registerOAuthClient(fetchFn, endpoints.registrationEndpoint, {
|
|
84190
|
-
redirectUri: redirectUri2,
|
|
84191
|
-
scope,
|
|
84192
|
-
clientName: "errata daemon"
|
|
84193
|
-
});
|
|
84194
|
-
activeClientId = registered.clientId;
|
|
84195
|
-
}
|
|
84196
|
-
const url2 = buildAuthorizeUrl(endpoints.authorizationEndpoint, {
|
|
84197
|
-
clientId: activeClientId,
|
|
84198
|
-
redirectUri: redirectUri2,
|
|
84199
|
-
scope,
|
|
84200
|
-
state,
|
|
84201
|
-
challenge: pkce.challenge,
|
|
84202
|
-
method: pkce.method
|
|
84203
|
-
});
|
|
84204
|
-
if (opts.open) opts.open(url2);
|
|
84205
|
-
else console.log(`approve this login in your browser:
|
|
84206
|
-
${url2}`);
|
|
84207
|
-
} catch (err2) {
|
|
84208
|
-
server.close();
|
|
84209
|
-
reject(err2);
|
|
84210
|
-
}
|
|
84211
|
-
})();
|
|
84212
|
-
});
|
|
84213
|
-
if (opts.timeoutMs) {
|
|
84214
|
-
setTimeout(() => {
|
|
84215
|
-
server.close();
|
|
84216
|
-
reject(new Error("login timed out \u2014 run `errata login` again"));
|
|
84217
|
-
}, opts.timeoutMs).unref?.();
|
|
84218
|
-
}
|
|
84277
|
+
app.post("/api/sync", async (c) => {
|
|
84278
|
+
if (boundaryFlushing) return c.json({ skipped: "in-flight", ...flushSnapshot() }, 409);
|
|
84279
|
+
beginFlush("principles");
|
|
84280
|
+
try {
|
|
84281
|
+
const principles = await daemon.syncPrinciplesPublic();
|
|
84282
|
+
flushStage("triage");
|
|
84283
|
+
const triage = await daemon.syncTriagePublic();
|
|
84284
|
+
flushStage("instances");
|
|
84285
|
+
const instances = await daemon.syncInstancesPublic();
|
|
84286
|
+
flushStage("edge-backfill");
|
|
84287
|
+
const edgeBackfill = await daemon.backfillCausalEdges();
|
|
84288
|
+
return c.json({ principles, triage, instances, edgeBackfill });
|
|
84289
|
+
} catch (err2) {
|
|
84290
|
+
return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
|
|
84291
|
+
} finally {
|
|
84292
|
+
endFlush();
|
|
84293
|
+
}
|
|
84294
|
+
});
|
|
84295
|
+
app.get("/api/sync", (c) => c.json(flushSnapshot()));
|
|
84296
|
+
app.post("/api/tick", async (c) => {
|
|
84297
|
+
try {
|
|
84298
|
+
return c.json({ reports: Object.fromEntries(await daemon.tickAll()) });
|
|
84299
|
+
} catch (err2) {
|
|
84300
|
+
return c.json({ error: err2 instanceof Error ? err2.message : String(err2) }, 500);
|
|
84219
84301
|
}
|
|
84220
|
-
);
|
|
84221
|
-
const tokens = await exchangeAuthorizationCode(fetchFn, endpoints.tokenEndpoint, {
|
|
84222
|
-
code,
|
|
84223
|
-
codeVerifier: pkce.verifier,
|
|
84224
|
-
redirectUri,
|
|
84225
|
-
clientId
|
|
84226
84302
|
});
|
|
84227
|
-
return
|
|
84303
|
+
return daemon;
|
|
84228
84304
|
}
|
|
84229
84305
|
|
|
84230
|
-
// src/
|
|
84306
|
+
// src/cli.ts
|
|
84307
|
+
init_src5();
|
|
84308
|
+
init_git_remote();
|
|
84309
|
+
init_config();
|
|
84310
|
+
init_src7();
|
|
84231
84311
|
init_cloud_auth();
|
|
84232
|
-
|
|
84233
|
-
|
|
84234
|
-
|
|
84235
|
-
|
|
84236
|
-
|
|
84237
|
-
|
|
84312
|
+
|
|
84313
|
+
// src/agent-switch.ts
|
|
84314
|
+
function parseUseArgs(args2) {
|
|
84315
|
+
const positional = [];
|
|
84316
|
+
let clear = false;
|
|
84317
|
+
for (const arg of args2) {
|
|
84318
|
+
if (arg === "--none" || arg === "--clear") {
|
|
84319
|
+
clear = true;
|
|
84320
|
+
continue;
|
|
84321
|
+
}
|
|
84322
|
+
if (arg.startsWith("--")) continue;
|
|
84323
|
+
positional.push(arg);
|
|
84324
|
+
}
|
|
84325
|
+
if (clear) return { kind: "clear" };
|
|
84326
|
+
const handle2 = positional[0];
|
|
84327
|
+
if (!handle2) return { kind: "show" };
|
|
84328
|
+
return { kind: "set", handle: handle2 };
|
|
84238
84329
|
}
|
|
84239
|
-
function
|
|
84240
|
-
|
|
84241
|
-
const v = value;
|
|
84242
|
-
return typeof v["projectId"] === "string" && typeof v["locator"] === "string" && (v["visibility"] === "public" || v["visibility"] === "private") && typeof v["consents"] === "object" && v["consents"] !== null;
|
|
84330
|
+
function normalizeHandle(handle2) {
|
|
84331
|
+
return handle2.trim().replace(/^@/, "").toLowerCase();
|
|
84243
84332
|
}
|
|
84244
|
-
|
|
84245
|
-
const
|
|
84246
|
-
|
|
84247
|
-
|
|
84248
|
-
|
|
84249
|
-
|
|
84250
|
-
|
|
84251
|
-
|
|
84252
|
-
|
|
84253
|
-
|
|
84254
|
-
|
|
84255
|
-
|
|
84256
|
-
...typeof opts.facts.commitCount === "number" ? { commitCount: opts.facts.commitCount } : {}
|
|
84257
|
-
};
|
|
84258
|
-
const codeResponse = await fetchFn(device.code, {
|
|
84259
|
-
method: "POST",
|
|
84260
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84261
|
-
body: JSON.stringify({ client_id: clientId, scope: "project:link", link })
|
|
84262
|
-
});
|
|
84263
|
-
if (!codeResponse.ok) {
|
|
84264
|
-
const text = (await codeResponse.text()).slice(0, 200);
|
|
84265
|
-
if (codeResponse.status === 404) {
|
|
84266
|
-
throw new Error("this console does not offer the link flow yet (device authorization is off)");
|
|
84267
|
-
}
|
|
84268
|
-
throw new Error(`link request failed: HTTP ${codeResponse.status} ${text}`);
|
|
84333
|
+
function findActable(agents, handle2) {
|
|
84334
|
+
const want = normalizeHandle(handle2);
|
|
84335
|
+
return agents.find((a) => normalizeHandle(a.handle) === want) ?? null;
|
|
84336
|
+
}
|
|
84337
|
+
async function runUse(action, ctx) {
|
|
84338
|
+
switch (action.kind) {
|
|
84339
|
+
case "clear":
|
|
84340
|
+
return runClear(ctx);
|
|
84341
|
+
case "show":
|
|
84342
|
+
return runShow(ctx);
|
|
84343
|
+
case "set":
|
|
84344
|
+
return runSet(action.handle, ctx);
|
|
84269
84345
|
}
|
|
84270
|
-
|
|
84271
|
-
|
|
84272
|
-
|
|
84346
|
+
}
|
|
84347
|
+
function runClear(ctx) {
|
|
84348
|
+
const active2 = ctx.getActiveAgent();
|
|
84349
|
+
ctx.clearActiveAgent();
|
|
84350
|
+
if (active2) {
|
|
84351
|
+
ctx.log(`no longer acting as ${active2.handle} \u2014 the daemon acts as your default identity`);
|
|
84352
|
+
} else {
|
|
84353
|
+
ctx.log(`already on the default identity \u2014 nothing to clear`);
|
|
84273
84354
|
}
|
|
84274
|
-
|
|
84275
|
-
|
|
84276
|
-
|
|
84277
|
-
|
|
84278
|
-
});
|
|
84279
|
-
|
|
84280
|
-
|
|
84281
|
-
|
|
84282
|
-
while (Date.now() < deadline) {
|
|
84283
|
-
await sleep(intervalMs);
|
|
84284
|
-
const poll = await fetchFn(device.token, {
|
|
84285
|
-
method: "POST",
|
|
84286
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
84287
|
-
body: JSON.stringify({
|
|
84288
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
84289
|
-
device_code: code.device_code,
|
|
84290
|
-
client_id: clientId
|
|
84291
|
-
})
|
|
84292
|
-
});
|
|
84293
|
-
const payload = await poll.json().catch(() => null);
|
|
84294
|
-
if (!poll.ok) {
|
|
84295
|
-
const error48 = typeof payload?.["error"] === "string" ? payload["error"] : "device_poll_failed";
|
|
84296
|
-
if (error48 === "authorization_pending") continue;
|
|
84297
|
-
if (error48 === "slow_down") {
|
|
84298
|
-
intervalMs += 5e3;
|
|
84299
|
-
continue;
|
|
84300
|
-
}
|
|
84301
|
-
if (error48 === "access_denied") return { status: "denied" };
|
|
84302
|
-
if (error48 === "expired_token") return { status: "expired" };
|
|
84303
|
-
throw new Error(`link approval failed: ${error48}`);
|
|
84304
|
-
}
|
|
84305
|
-
const decision = payload?.["link"];
|
|
84306
|
-
if (!isDecision(decision)) {
|
|
84307
|
-
throw new Error("the console approved the device but returned no link decision");
|
|
84308
|
-
}
|
|
84309
|
-
return {
|
|
84310
|
-
status: "approved",
|
|
84311
|
-
decision,
|
|
84312
|
-
installationId: typeof payload?.["installation_id"] === "string" ? payload["installation_id"] : null
|
|
84313
|
-
};
|
|
84355
|
+
return 0;
|
|
84356
|
+
}
|
|
84357
|
+
async function runShow(ctx) {
|
|
84358
|
+
const active2 = ctx.getActiveAgent();
|
|
84359
|
+
ctx.log(active2 ? `active agent: ${active2.handle} (id ${active2.id})` : `active agent: (default identity)`);
|
|
84360
|
+
if (!ctx.loggedIn) {
|
|
84361
|
+
ctx.log(` log in to list the agents you can act as: errata login`);
|
|
84362
|
+
return 0;
|
|
84314
84363
|
}
|
|
84315
|
-
|
|
84364
|
+
let resp;
|
|
84365
|
+
try {
|
|
84366
|
+
resp = await ctx.actableAgents();
|
|
84367
|
+
} catch (err2) {
|
|
84368
|
+
ctx.log(` couldn't list actable agents: ${errText(err2)}`);
|
|
84369
|
+
return 1;
|
|
84370
|
+
}
|
|
84371
|
+
printActable(resp, active2, ctx);
|
|
84372
|
+
return 0;
|
|
84316
84373
|
}
|
|
84317
|
-
function
|
|
84318
|
-
|
|
84319
|
-
|
|
84320
|
-
|
|
84321
|
-
|
|
84322
|
-
|
|
84374
|
+
async function runSet(handle2, ctx) {
|
|
84375
|
+
if (!ctx.loggedIn) {
|
|
84376
|
+
ctx.log(`not logged in \u2014 run \`errata login\` first, then \`errata use ${handle2}\``);
|
|
84377
|
+
return 1;
|
|
84378
|
+
}
|
|
84379
|
+
let resp;
|
|
84380
|
+
try {
|
|
84381
|
+
resp = await ctx.actableAgents();
|
|
84382
|
+
} catch (err2) {
|
|
84383
|
+
ctx.log(`couldn't verify \`${handle2}\`: ${errText(err2)}`);
|
|
84384
|
+
return 1;
|
|
84385
|
+
}
|
|
84386
|
+
const match2 = findActable(resp.agents, handle2);
|
|
84387
|
+
if (!match2) {
|
|
84388
|
+
ctx.log(`unknown agent: ${handle2}`);
|
|
84389
|
+
if (resp.agents.length === 0) {
|
|
84390
|
+
ctx.log(` you can't act as any agents yet.`);
|
|
84391
|
+
} else {
|
|
84392
|
+
ctx.log(` agents you can act as: ${resp.agents.map((a) => a.handle).join(", ")}`);
|
|
84323
84393
|
}
|
|
84324
|
-
|
|
84325
|
-
|
|
84326
|
-
|
|
84327
|
-
|
|
84328
|
-
|
|
84329
|
-
return {
|
|
84330
|
-
...rootCommitSha && /^[0-9a-f]{40,64}$/i.test(rootCommitSha) ? { rootCommitSha } : {},
|
|
84331
|
-
...typeof commitCount === "number" ? { commitCount } : {}
|
|
84332
|
-
};
|
|
84394
|
+
return 2;
|
|
84395
|
+
}
|
|
84396
|
+
ctx.setActiveAgent({ handle: match2.handle, id: match2.id });
|
|
84397
|
+
ctx.log(`now acting as ${match2.handle} (id ${match2.id}) \u2014 saved; persists across daemon restarts`);
|
|
84398
|
+
return 0;
|
|
84333
84399
|
}
|
|
84334
|
-
|
|
84335
|
-
|
|
84336
|
-
|
|
84337
|
-
|
|
84338
|
-
|
|
84339
|
-
|
|
84340
|
-
|
|
84341
|
-
|
|
84342
|
-
.
|
|
84343
|
-
|
|
84344
|
-
|
|
84345
|
-
|
|
84346
|
-
|
|
84347
|
-
credentials*
|
|
84348
|
-
|
|
84349
|
-
# fixtures and captured data
|
|
84350
|
-
fixtures/
|
|
84351
|
-
**/testdata/
|
|
84352
|
-
*.dump
|
|
84353
|
-
*.sql.gz
|
|
84354
|
-
`;
|
|
84355
|
-
function applyLinkDecision(root, profile, config2, decision, now = Date.now) {
|
|
84356
|
-
const c = decision.consents;
|
|
84357
|
-
const nextConfig = {
|
|
84358
|
-
...config2,
|
|
84359
|
-
consent: {
|
|
84360
|
-
...config2.consent,
|
|
84361
|
-
sync: c.sync !== "off",
|
|
84362
|
-
telemetry: c.telemetry,
|
|
84363
|
-
contributePackages: c.packages && c.sync !== "off"
|
|
84364
|
-
},
|
|
84365
|
-
onboardedAt: config2.onboardedAt ?? now()
|
|
84366
|
-
};
|
|
84367
|
-
const nextProfile = {
|
|
84368
|
-
...profile,
|
|
84369
|
-
syncConsent: c.sync === "automatic" ? "automatic" : c.sync === "ask" ? "ask-each-time" : "disabled",
|
|
84370
|
-
projectId: decision.projectId,
|
|
84371
|
-
projectLocator: decision.locator,
|
|
84372
|
-
linkConsents: {
|
|
84373
|
-
structure: c.structure,
|
|
84374
|
-
mineHistory: c.mineHistory,
|
|
84375
|
-
contributeHistory: c.contributeHistory && c.sync !== "off",
|
|
84376
|
-
decidedAt: decision.approvedAt
|
|
84377
|
-
}
|
|
84378
|
-
};
|
|
84379
|
-
delete nextProfile.projectLinkDisabled;
|
|
84380
|
-
let wroteIgnore = false;
|
|
84381
|
-
if (decision.writeIgnoreStarter !== false) {
|
|
84382
|
-
const target = join35(root, ".errataignore");
|
|
84383
|
-
if (!existsSync31(target)) {
|
|
84384
|
-
writeFileSync23(target, IGNORE_STARTER, "utf8");
|
|
84385
|
-
wroteIgnore = true;
|
|
84386
|
-
}
|
|
84400
|
+
function printActable(resp, active2, ctx) {
|
|
84401
|
+
if (resp.agents.length === 0) {
|
|
84402
|
+
ctx.log(` (no agents you can act as)`);
|
|
84403
|
+
return;
|
|
84404
|
+
}
|
|
84405
|
+
ctx.log(` agents you can act as:`);
|
|
84406
|
+
for (const a of resp.agents) {
|
|
84407
|
+
const marks = [];
|
|
84408
|
+
if (active2 && a.id === active2.id) marks.push("active");
|
|
84409
|
+
if (a.id === resp.defaultAgentId) marks.push("default");
|
|
84410
|
+
const suffix = marks.length ? ` (${marks.join(", ")})` : "";
|
|
84411
|
+
const scope = a.scope ? ` \xB7 ${a.scope}` : "";
|
|
84412
|
+
ctx.log(` ${a.handle}${scope}${suffix}`);
|
|
84387
84413
|
}
|
|
84388
|
-
return { profile: nextProfile, config: nextConfig, wroteIgnore };
|
|
84389
84414
|
}
|
|
84415
|
+
function errText(err2) {
|
|
84416
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
84417
|
+
}
|
|
84418
|
+
|
|
84419
|
+
// src/cli.ts
|
|
84420
|
+
init_cloud_endpoint_policy();
|
|
84390
84421
|
|
|
84391
84422
|
// src/device-login.ts
|
|
84392
84423
|
init_src7();
|
|
@@ -86070,6 +86101,13 @@ async function cmdLink(args2) {
|
|
|
86070
86101
|
}
|
|
86071
86102
|
const consents = historyConsents(applied.profile);
|
|
86072
86103
|
const reported = await reportHistoryStage(authedCloudClient(applied.config), applied.profile, (line) => console.warn(line));
|
|
86104
|
+
if (reported?.consents) {
|
|
86105
|
+
const synced = applyConsents(applied.profile, applied.config, reported.consents, (/* @__PURE__ */ new Date()).toISOString());
|
|
86106
|
+
if (synced.changed) {
|
|
86107
|
+
saveProfile(ROOT, synced.profile);
|
|
86108
|
+
saveConfig(synced.config);
|
|
86109
|
+
}
|
|
86110
|
+
}
|
|
86073
86111
|
if (!consents.mineHistory) console.log("history: not mined (consent off)");
|
|
86074
86112
|
else if (reported?.stopRequested) console.log("history: a stop request is standing in the console; the daemon honours it");
|
|
86075
86113
|
else console.log(`history: consents recorded${consents.contributeHistory ? "" : " (mine only)"} \xB7 the daemon walks the repo on its next pass`);
|