omp-conductor 0.17.1 → 0.18.1
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/README.md +34 -0
- package/REFERENCE.md +71 -17
- package/agents/to-spec.md +90 -0
- package/package.json +2 -1
- package/schema/config.schema.json +53 -1
- package/src/admission.ts +308 -76
- package/src/ask.ts +307 -10
- package/src/backups.ts +2 -2
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +43 -14
- package/src/briefs/to-spec.md +84 -0
- package/src/briefs/worker.md +37 -19
- package/src/cli.ts +2 -0
- package/src/command-help.ts +19 -1
- package/src/command-manifest.ts +27 -2
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +110 -3
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +57 -0
- package/src/config.ts +102 -2
- package/src/daemon.ts +1220 -1517
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +279 -16
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +425 -1
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +52 -0
- package/src/escalate.ts +9 -3
- package/src/failure-class.ts +43 -4
- package/src/fleet.ts +166 -24
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +55 -8
- package/src/graph.ts +379 -69
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +567 -2
- package/src/lifecycle.ts +158 -6
- package/src/omp.ts +269 -20
- package/src/orchestrator-tick.ts +1489 -26
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/routing.ts +11 -3
- package/src/session-host.ts +115 -5
- package/src/settlement.ts +1780 -0
- package/src/setup-host.ts +1205 -6
- package/src/setup-install.ts +119 -30
- package/src/setup-wizard.ts +88 -2
- package/src/setup.ts +119 -13
- package/src/shell.ts +15 -0
- package/src/status-render.ts +100 -11
- package/src/store.ts +519 -45
- package/src/to-spec.ts +387 -0
- package/src/tracker/github.ts +150 -14
- package/src/types.ts +470 -16
- package/src/upgrade-verify.ts +209 -2
- package/src/upgrade.ts +175 -1
- package/src/verbs/protocol.ts +39 -0
- package/src/verbs/server.ts +770 -40
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +239 -9
- package/src/worktree.ts +142 -18
package/src/failure-class.ts
CHANGED
|
@@ -17,9 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
import type { Caps, FailureClass, RecoveryAction, RunRecord } from "./types.ts";
|
|
19
19
|
|
|
20
|
-
/** Facts the caller fetched, each only for the rows that need it.
|
|
20
|
+
/** Facts the caller fetched, each only for the rows that need it.
|
|
21
|
+
*
|
|
22
|
+
* `pr` can also be `"missing"`: the tracker has proven the claimed PR
|
|
23
|
+
* definitively does not exist (#779). It is a fact, the same way `"closed"`
|
|
24
|
+
* is — a REST 404 the adapter corroborated with a same-repository pulls-list
|
|
25
|
+
* read — never a transient "could not tell", which stays `undefined`. */
|
|
21
26
|
export interface ClassifyFacts {
|
|
22
|
-
pr?: "open" | "merged" | "closed";
|
|
27
|
+
pr?: "open" | "merged" | "closed" | "missing";
|
|
23
28
|
mergeable?: "conflicting" | "clean" | "unknown";
|
|
24
29
|
checks?: { name: string; state: string; link?: string }[];
|
|
25
30
|
/** Full session error recovered from the transcript. Kept as a fact so a
|
|
@@ -142,6 +147,12 @@ const START_FAILURE_SIGNATURES = [
|
|
|
142
147
|
"invalid api key",
|
|
143
148
|
"authentication failed",
|
|
144
149
|
"could not load its peer dependency",
|
|
150
|
+
// The pre-launch identity gate (#798/#828): the account is missing, or the
|
|
151
|
+
// harness binding a worker resolves through is not established. The daemon
|
|
152
|
+
// refuses before it spawns anything, so no attempt was spent — and the
|
|
153
|
+
// symptom this replaced (the child dying on the peer import) already read as
|
|
154
|
+
// a start failure, so charging one here would be a regression dressed as a fix.
|
|
155
|
+
"worker identity unavailable",
|
|
145
156
|
] as const;
|
|
146
157
|
|
|
147
158
|
/**
|
|
@@ -188,10 +199,23 @@ export function providerCreditRefusal(error: {
|
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
/** Provider text that names a per-request stream fault. Deliberately narrow:
|
|
191
|
-
* a 429 is rate limiting and a 402 is credit — different remedies (#220).
|
|
202
|
+
* a 429 is rate limiting and a 402 is credit — different remedies (#220).
|
|
203
|
+
*
|
|
204
|
+
* The structural `kind` that `readSessionError` marks outranks this list: a
|
|
205
|
+
* transcript record the harness itself attributed to a provider abort is a
|
|
206
|
+
* per-request stream fault whatever the provider called it, so no vendor
|
|
207
|
+
* prose needs enumerating here (#743). The list remains for transcripts that
|
|
208
|
+
* only ever carried prose, like the original "stream stalled" stall. */
|
|
192
209
|
const TRANSIENT_FAULT_SIGNATURES = ["stream stalled"] as const;
|
|
193
210
|
|
|
194
|
-
export function providerTransientFault(error: {
|
|
211
|
+
export function providerTransientFault(error: {
|
|
212
|
+
status?: number;
|
|
213
|
+
message: string;
|
|
214
|
+
kind?: "provider-stream";
|
|
215
|
+
}): string | undefined {
|
|
216
|
+
if (error.kind === "provider-stream") {
|
|
217
|
+
return error.message.split("\n")[0]?.trim() ?? error.message;
|
|
218
|
+
}
|
|
195
219
|
const text = error.message.toLowerCase();
|
|
196
220
|
if (!TRANSIENT_FAULT_SIGNATURES.some((s) => text.includes(s))) return undefined;
|
|
197
221
|
return error.message.split("\n")[0]?.trim() ?? error.message;
|
|
@@ -499,6 +523,21 @@ export function classifyRun(
|
|
|
499
523
|
if (run.state === "failed" && facts.pr === "open") {
|
|
500
524
|
const checks = facts.checks ?? [];
|
|
501
525
|
const unresolved = checks.filter((c) => !SUCCESS_CHECK_STATES[normalise(c.state)] === true);
|
|
526
|
+
if (checks.length > 0 && unresolved.length === 0) {
|
|
527
|
+
// The PR is open and every check is green: the strongest mechanical
|
|
528
|
+
// success evidence there is short of merge. The row's `failed` state is
|
|
529
|
+
// stale — whatever the worker recorded, its work is passing — so this is
|
|
530
|
+
// not a failure of any class. `none` recovery restores it to
|
|
531
|
+
// `pushed-green` in classifyAndRecover instead of escalating an
|
|
532
|
+
// `[unknown]` that charges an attempt (#766). An empty check list is
|
|
533
|
+
// "could not tell", not all-green, and still falls through to `unknown`
|
|
534
|
+
// below.
|
|
535
|
+
return {
|
|
536
|
+
cls: "unknown",
|
|
537
|
+
recovery: "none",
|
|
538
|
+
evidence: `${run.prUrl ?? "the PR"} is open and every check is green`,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
502
541
|
if (checks.length > 0 && unresolved.length > 0) {
|
|
503
542
|
// A failed check whose *log* smells like infrastructure — a registry 429,
|
|
504
543
|
// a runner shutdown, a DNS failure (#177). The check has a verdict, so
|
package/src/fleet.ts
CHANGED
|
@@ -50,8 +50,24 @@ import { inspectBriefLayout } from "./brief-upgrade.ts";
|
|
|
50
50
|
import { dbPath, openStore } from "./store.ts";
|
|
51
51
|
import { renderBriefForProject } from "./setup.ts";
|
|
52
52
|
import { DEFAULT_ARM_PROOF, type ArmProof, type DaemonStop, type ProjectConfig, type Store } from "./types.ts";
|
|
53
|
-
import { probeCodeGraph, type CodeGraphHealth } from "./graph-health.ts";
|
|
53
|
+
import { DEFAULT_DEPS, probeCodeGraph, type CodeGraphHealth } from "./graph-health.ts";
|
|
54
54
|
import { isPaused, setPaused, statusSnapshot, type StatusSnapshot } from "./daemon.ts";
|
|
55
|
+
// The drain surface belongs beside hold/releaseHold on the operator surface:
|
|
56
|
+
// the record is daemon state (implemented next to the pause sentinel), but the
|
|
57
|
+
// later CLI and release #484 children import from here, exactly where they find
|
|
58
|
+
// every other fleet control.
|
|
59
|
+
export {
|
|
60
|
+
cancelDrain,
|
|
61
|
+
consumeDrain,
|
|
62
|
+
createDrain,
|
|
63
|
+
drainPath,
|
|
64
|
+
readDrain,
|
|
65
|
+
type CreateDrainOptions,
|
|
66
|
+
type DrainProblem,
|
|
67
|
+
type DrainRecord,
|
|
68
|
+
type DrainStatus,
|
|
69
|
+
type DrainVerdict,
|
|
70
|
+
} from "./daemon.ts";
|
|
55
71
|
import {
|
|
56
72
|
healthCheck,
|
|
57
73
|
isAlive,
|
|
@@ -59,6 +75,7 @@ import {
|
|
|
59
75
|
probeUnit,
|
|
60
76
|
runSystemctl,
|
|
61
77
|
stopDaemon,
|
|
78
|
+
type HealthCheckResult,
|
|
62
79
|
type StopResult,
|
|
63
80
|
} from "./lifecycle.ts";
|
|
64
81
|
import type { WorkerPausePhase } from "./worker.ts";
|
|
@@ -1053,21 +1070,15 @@ export function parseHerdrAgentList(rawOutput: string): HerdrAgent[] {
|
|
|
1053
1070
|
return out;
|
|
1054
1071
|
}
|
|
1055
1072
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
});
|
|
1066
|
-
if (res.error) throw res.error;
|
|
1067
|
-
if (res.status !== 0) {
|
|
1068
|
-
throw new Error((res.stderr ?? res.stdout ?? `process-info exit ${String(res.status)}`).trim());
|
|
1069
|
-
}
|
|
1070
|
-
const raw = (res.stdout ?? "").trim();
|
|
1073
|
+
/**
|
|
1074
|
+
* The `process_info` document out of `herdr pane process-info` output
|
|
1075
|
+
* (#832): every caller that maps the fleet pane's live omp processes starts
|
|
1076
|
+
* from this schema, whether the output carries herdr's CLI `result` envelope
|
|
1077
|
+
* or the bare document. Throws when the output cannot be read; the caller
|
|
1078
|
+
* decides what an unreadable answer means.
|
|
1079
|
+
*/
|
|
1080
|
+
export function parseHerdrProcessInfo(stdout: string, paneId: string): ProcessInfo {
|
|
1081
|
+
const raw = stdout.trim();
|
|
1071
1082
|
if (raw.length === 0) {
|
|
1072
1083
|
throw new Error(`herdr pane process-info printed nothing for ${paneId}`);
|
|
1073
1084
|
}
|
|
@@ -1083,10 +1094,27 @@ async function herdrOmpForegroundPids(paneId: string, deps: PaneStopDeps): Promi
|
|
|
1083
1094
|
if (info === undefined) {
|
|
1084
1095
|
throw new Error(`herdr pane process-info has no process_info for ${paneId} — unrecognized schema`);
|
|
1085
1096
|
}
|
|
1086
|
-
return
|
|
1097
|
+
return info;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
async function herdrOmpForegroundPids(paneId: string, deps: PaneStopDeps): Promise<number[]> {
|
|
1101
|
+
const bin = deps.herdrBin ?? "herdr";
|
|
1102
|
+
const session =
|
|
1103
|
+
deps.herdrSession ??
|
|
1104
|
+
resolveHerdrSessionWithBridge({ env: process.env, herdrBin: bin });
|
|
1105
|
+
const res = spawnSync(bin, ["--session", session, "pane", "process-info", "--pane", paneId], {
|
|
1106
|
+
encoding: "utf8",
|
|
1107
|
+
timeout: 8_000,
|
|
1108
|
+
env: process.env,
|
|
1109
|
+
});
|
|
1110
|
+
if (res.error) throw res.error;
|
|
1111
|
+
if (res.status !== 0) {
|
|
1112
|
+
throw new Error((res.stderr ?? res.stdout ?? `process-info exit ${String(res.status)}`).trim());
|
|
1113
|
+
}
|
|
1114
|
+
return ompPidsFromProcessInfo(parseHerdrProcessInfo(res.stdout ?? "", paneId));
|
|
1087
1115
|
}
|
|
1088
1116
|
|
|
1089
|
-
interface ProcessInfo {
|
|
1117
|
+
export interface ProcessInfo {
|
|
1090
1118
|
shell_pid?: number;
|
|
1091
1119
|
foreground_processes?: ForegroundProc[];
|
|
1092
1120
|
}
|
|
@@ -1121,6 +1149,85 @@ function isOmpProcess(proc: ForegroundProc): boolean {
|
|
|
1121
1149
|
return false;
|
|
1122
1150
|
}
|
|
1123
1151
|
|
|
1152
|
+
/**
|
|
1153
|
+
* The omp processes the fleet pane claims, resolved to their start times
|
|
1154
|
+
* (#832), or why they could not be read. Structurally identical to
|
|
1155
|
+
* upgrade-verify's `PaneOmpProbe` — the upgrade engine and the post-restart
|
|
1156
|
+
* verifier pass this straight into the pure verdicts, and the type is
|
|
1157
|
+
* repeated here because the verifier leaf must not import this module (fleet
|
|
1158
|
+
* reaches the daemon, which reaches the verifier).
|
|
1159
|
+
*/
|
|
1160
|
+
export type PaneProbeResult = { starts: readonly number[] } | { problem: string };
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Every omp process the fleet pane currently claims by herdr, resolved to
|
|
1164
|
+
* its start time (#832): the pane is where an *external* orchestrator
|
|
1165
|
+
* session lives, and the only fact that proves it reloaded is the live
|
|
1166
|
+
* process's own start — a pane process still running from before the install
|
|
1167
|
+
* began loaded the pre-upgrade extension. Every unreadable answer is a
|
|
1168
|
+
* `problem`: absence of evidence is never a reload.
|
|
1169
|
+
*
|
|
1170
|
+
* `run` is the injected command runner (the upgrade's scripted seam, the
|
|
1171
|
+
* daemon's `runCommand`), and `startTime` resolves one pid to its start —
|
|
1172
|
+
* the /proc read lives with the verifier's other live-process facts.
|
|
1173
|
+
*/
|
|
1174
|
+
export async function herdrPaneOmpStarts(
|
|
1175
|
+
run: (
|
|
1176
|
+
command: string,
|
|
1177
|
+
args: readonly string[],
|
|
1178
|
+
) => Promise<{ code: number; stdout: string; stderr: string }>,
|
|
1179
|
+
session: string,
|
|
1180
|
+
startTime: (pid: number) => number | undefined,
|
|
1181
|
+
): Promise<PaneProbeResult> {
|
|
1182
|
+
const agents = await run("herdr", ["--session", session, "agent", "list"]);
|
|
1183
|
+
if (agents.code !== 0) {
|
|
1184
|
+
return {
|
|
1185
|
+
problem:
|
|
1186
|
+
`herdr agent list failed: ${agents.stderr.trim() || agents.stdout.trim() || `exit ${agents.code}`}`,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
let parsed: HerdrAgent[];
|
|
1190
|
+
try {
|
|
1191
|
+
parsed = parseHerdrAgentList(agents.stdout);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
return { problem: err instanceof Error ? err.message : String(err) };
|
|
1194
|
+
}
|
|
1195
|
+
const starts: number[] = [];
|
|
1196
|
+
for (const agent of parsed) {
|
|
1197
|
+
// A name without a live omp claim is a leftover label, not a pane process.
|
|
1198
|
+
if (agent.agent === undefined) continue;
|
|
1199
|
+
const info = await run("herdr", [
|
|
1200
|
+
"--session",
|
|
1201
|
+
session,
|
|
1202
|
+
"pane",
|
|
1203
|
+
"process-info",
|
|
1204
|
+
"--pane",
|
|
1205
|
+
agent.paneId,
|
|
1206
|
+
]);
|
|
1207
|
+
if (info.code !== 0) {
|
|
1208
|
+
return {
|
|
1209
|
+
problem:
|
|
1210
|
+
`herdr pane process-info for ${agent.paneId} failed: ` +
|
|
1211
|
+
`${info.stderr.trim() || info.stdout.trim() || `exit ${info.code}`}`,
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
let pids: number[];
|
|
1215
|
+
try {
|
|
1216
|
+
pids = ompPidsFromProcessInfo(parseHerdrProcessInfo(info.stdout, agent.paneId));
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
return { problem: err instanceof Error ? err.message : String(err) };
|
|
1219
|
+
}
|
|
1220
|
+
for (const pid of pids) {
|
|
1221
|
+
const startedAt = startTime(pid);
|
|
1222
|
+
if (startedAt === undefined) {
|
|
1223
|
+
return { problem: `cannot read the start time of pane process ${pid} — reload unproven` };
|
|
1224
|
+
}
|
|
1225
|
+
starts.push(startedAt);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return { starts };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1124
1231
|
/**
|
|
1125
1232
|
* The project a bare read means, when the config leaves no doubt.
|
|
1126
1233
|
*
|
|
@@ -1143,8 +1250,26 @@ function bareReadProject(): string | undefined {
|
|
|
1143
1250
|
|
|
1144
1251
|
export function fleetLayers(projectName?: string): FleetLayers {
|
|
1145
1252
|
const rec = livingDaemon();
|
|
1253
|
+
// systemd is the authoritative liveness witness for a unit-owned daemon: a
|
|
1254
|
+
// pidfile that is missing, stale, or skewed against the unit's MainPID must
|
|
1255
|
+
// not declare the daemon dead (#716). Consulted only when the pidfile says
|
|
1256
|
+
// dead — a healthy record needs no shell-out, and a host without systemctl
|
|
1257
|
+
// answers `inactive`, so those hosts behave exactly as before. `failed`,
|
|
1258
|
+
// `unknown`, and a mid-restart unit (MainPID 0, process gone) all answer
|
|
1259
|
+
// undefined: a genuinely stopped daemon never reports running.
|
|
1260
|
+
let unitPid: number | undefined;
|
|
1261
|
+
if (rec === undefined) {
|
|
1262
|
+
const ownership = probeUnit();
|
|
1263
|
+
if (ownership.kind === "active" && isAlive(ownership.pid)) unitPid = ownership.pid;
|
|
1264
|
+
}
|
|
1265
|
+
const daemon: FleetLayers["daemon"] =
|
|
1266
|
+
rec !== undefined
|
|
1267
|
+
? { running: true, pid: rec.pid, port: rec.port }
|
|
1268
|
+
: unitPid !== undefined
|
|
1269
|
+
? { running: true, pid: unitPid }
|
|
1270
|
+
: { running: false };
|
|
1146
1271
|
const paused = isPaused(projectName ?? bareReadProject());
|
|
1147
|
-
const dispatch: DispatchLayer =
|
|
1272
|
+
const dispatch: DispatchLayer = daemon.running ? (paused ? "paused" : "running") : "stopped";
|
|
1148
1273
|
|
|
1149
1274
|
const tick = resolveTickConfig(projectName);
|
|
1150
1275
|
let ticks: TicksLayer;
|
|
@@ -1224,7 +1349,7 @@ export function fleetLayers(projectName?: string): FleetLayers {
|
|
|
1224
1349
|
...(tickConfigPath === undefined ? {} : { tickConfigPath }),
|
|
1225
1350
|
...(haltPath === undefined ? {} : { paneHaltPath: haltPath }),
|
|
1226
1351
|
paused,
|
|
1227
|
-
daemon
|
|
1352
|
+
daemon,
|
|
1228
1353
|
};
|
|
1229
1354
|
}
|
|
1230
1355
|
|
|
@@ -1337,7 +1462,7 @@ export function workerPhasesFromHealthz(
|
|
|
1337
1462
|
*/
|
|
1338
1463
|
export function classifyDaemonProjectHealth(
|
|
1339
1464
|
record: { project?: string } | undefined,
|
|
1340
|
-
health:
|
|
1465
|
+
health: HealthCheckResult | undefined,
|
|
1341
1466
|
project: string,
|
|
1342
1467
|
): DaemonProjectHealth {
|
|
1343
1468
|
if (record === undefined) return { kind: "stopped" };
|
|
@@ -1347,7 +1472,13 @@ export function classifyDaemonProjectHealth(
|
|
|
1347
1472
|
if (record.project !== undefined && record.project !== project) {
|
|
1348
1473
|
return { kind: "other-project", serves: record.project };
|
|
1349
1474
|
}
|
|
1350
|
-
|
|
1475
|
+
// #685: a timed-out probe is a probe outcome, never a death verdict — the
|
|
1476
|
+
// pid is up but wedged, so it must not read as `not running`. Only a
|
|
1477
|
+
// refusal or other failure (nothing listening, a torn answer) stays
|
|
1478
|
+
// `unreachable`.
|
|
1479
|
+
if (health?.ok !== true) {
|
|
1480
|
+
return health?.failure === "timeout" ? { kind: "unresponsive" } : { kind: "unreachable" };
|
|
1481
|
+
}
|
|
1351
1482
|
try {
|
|
1352
1483
|
const payload = JSON.parse(health.body ?? "null") as unknown;
|
|
1353
1484
|
if (payload === null || typeof payload !== "object") {
|
|
@@ -1437,7 +1568,12 @@ export async function collectFleetStatus(projectName?: string): Promise<FleetSta
|
|
|
1437
1568
|
};
|
|
1438
1569
|
const healthBody = projectHealth.kind === "ok" ? rawHealth?.body : undefined;
|
|
1439
1570
|
const cached = codeGraphFromHealthz(healthBody, project.name);
|
|
1440
|
-
|
|
1571
|
+
// The runtime half of the code-graph finding (#726): the probe reads the
|
|
1572
|
+
// store-backed per-run observations, so "observed" is always grounded in
|
|
1573
|
+
// dispatched runs, never in the daemon's own process or in mcp.json. The
|
|
1574
|
+
// store stays scoped to the same try/finally as the other reads so a
|
|
1575
|
+
// throwing probe cannot leak its handle.
|
|
1576
|
+
const store = openStore(dbPath());
|
|
1441
1577
|
const workerPhases = [...workerPhasesFromHealthz(healthBody, project.name)].map(
|
|
1442
1578
|
([issue, phase]) => ({ issue, phase }),
|
|
1443
1579
|
);
|
|
@@ -1446,10 +1582,16 @@ export async function collectFleetStatus(projectName?: string): Promise<FleetSta
|
|
|
1446
1582
|
// and rendered identically from either project: the daemon_stops table is
|
|
1447
1583
|
// deliberately not partitioned by project, because the daemon serves every
|
|
1448
1584
|
// project and the uninvolved one must see who stopped it too.
|
|
1449
|
-
|
|
1585
|
+
let codeGraph: CodeGraphHealth;
|
|
1450
1586
|
let lastStop: DaemonStop | undefined;
|
|
1451
1587
|
let siblings: { project: string; live: number }[] = [];
|
|
1452
1588
|
try {
|
|
1589
|
+
codeGraph =
|
|
1590
|
+
cached ??
|
|
1591
|
+
(await probeCodeGraph(project, {
|
|
1592
|
+
...DEFAULT_DEPS,
|
|
1593
|
+
graphToolsObservations: () => store.graphToolsObservationCounts(project.name),
|
|
1594
|
+
}));
|
|
1453
1595
|
lastStop = store.latestDaemonStop();
|
|
1454
1596
|
// Shared-daemon visibility (#545): every configured project other than the
|
|
1455
1597
|
// one being viewed, with its live-run count.
|