@evoclock/pi-agentic-driver 0.9.1 → 0.9.2

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.
Files changed (38) hide show
  1. package/.agentic-driver/router.defaults.json +64 -0
  2. package/LICENSE +11 -8
  3. package/README.md +92 -32
  4. package/extensions/attended-authority-guard.ts +1 -1
  5. package/extensions/code-phage.js +1 -1
  6. package/extensions/herdr-communication.ts +1 -1
  7. package/extensions/herdr-dispatch.ts +1 -1
  8. package/extensions/herdr-lifecycle.ts +1 -1
  9. package/extensions/linux-microvm.ts +1 -1
  10. package/extensions/pulse.ts +33 -2
  11. package/extensions/task-board.ts +1 -1
  12. package/lib/adapters/diff-scope.mjs +1 -1
  13. package/lib/adapters/evidence.mjs +1 -1
  14. package/lib/adapters/narrative.mjs +1 -1
  15. package/lib/adapters/review-feedback.mjs +1 -1
  16. package/lib/adapters/visualization.mjs +1 -1
  17. package/lib/code-phage-core.mjs +1 -1
  18. package/lib/python_ast_metrics.py +1 -1
  19. package/lib/typescript_ast_metrics.mjs +1 -1
  20. package/package.json +12 -2
  21. package/scripts/aidr_writing_review.js +1 -1
  22. package/scripts/enforcement/attended_authority_guard.js +1 -1
  23. package/scripts/enforcement/guest_containment_taxonomy.v1.json +1 -1
  24. package/scripts/enforcement/herdr_async_dispatch_pi.js +1 -1
  25. package/scripts/enforcement/herdr_communication_pi.js +13 -8
  26. package/scripts/enforcement/herdr_lifecycle_pi.js +3 -3
  27. package/scripts/enforcement/linux_microvm_cutover_pi.js +1 -1
  28. package/scripts/enforcement/native_tui_context.js +1 -1
  29. package/scripts/enforcement/pulse_core_pi.js +38 -14
  30. package/scripts/enforcement/pulse_scheduler_pi.js +188 -25
  31. package/scripts/enforcement/router_engine_pi.js +483 -0
  32. package/scripts/enforcement/router_ranking_pi.js +146 -0
  33. package/scripts/enforcement/router_replacement_pi.js +98 -0
  34. package/scripts/enforcement/router_runtime_pi.js +299 -0
  35. package/scripts/enforcement/router_schemas_pi.js +604 -0
  36. package/scripts/enforcement/router_store_pi.js +438 -0
  37. package/scripts/enforcement/task_board_core_pi.js +255 -44
  38. package/skills/pulse/SKILL.md +1 -1
@@ -0,0 +1,98 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ // Replacement and lineage (ROUTER_DESIGN_TASK68 §7): the same-seat vs
5
+ // different-seat replacement rules and the dead-route rule.
6
+ //
7
+ // - Same-seat replacement (worker died, same route healthy): reuses the
8
+ // envelope; pinned route.
9
+ // - Different-seat replacement (route dead, quota exhausted): requires a NEW
10
+ // route-decision record AND a new claim/envelope — an envelope
11
+ // authenticated for the old seat is never reused for a new seat. The new
12
+ // record carries parentDecisionId lineage.
13
+ // - Dead-route rule: a seat whose collector reports failure or whose health
14
+ // probe fails can never be used for replacement.
15
+ //
16
+ // Pure decision logic; the caller performs the actual replaceAttempt /
17
+ // claim transitions.
18
+
19
+ import { sha256Hex } from "./router_schemas_pi.js";
20
+
21
+ function isPlainObject(value) {
22
+ return value !== null && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+
25
+ // §4 freshness at read time, duplicated here for the health/quota checks the
26
+ // replacement decision needs (single source of truth lives in the schemas
27
+ // module; this re-derivation keeps this module dependency-light).
28
+ function fresh(record, now) {
29
+ if (!isPlainObject(record)) return false;
30
+ if (record.sourceStatus !== "ok") return false;
31
+ const at = typeof now === "string" ? Date.parse(now) : now;
32
+ const expiry = Date.parse(record?.expiresAt);
33
+ return Number.isFinite(at) && Number.isFinite(expiry) && at < expiry;
34
+ }
35
+
36
+ // The dead-route rule (§7.2): a seat is DEAD for replacement when its
37
+ // collector reports failure (sourceStatus not ok, or stale observation) or
38
+ // its health probe fails (no fresh ok health observation for local seats).
39
+ // A dead seat can never be used for replacement — not even the same seat.
40
+ export function isDeadRoute({ seat, healthObservations = {}, quotaObservations = [], now }) {
41
+ if (!isPlainObject(seat)) return { dead: true, reason: "seat record missing" };
42
+ if (seat.enabled !== true) return { dead: true, reason: "seat is disabled in config" };
43
+ if (seat.kind === "local") {
44
+ const health = isPlainObject(healthObservations) ? healthObservations[seat.endpointRef] : undefined;
45
+ if (!fresh(health, now)) {
46
+ return { dead: true, reason: `endpoint "${seat.endpointRef}" has no fresh ok health observation` };
47
+ }
48
+ return { dead: false, reason: null };
49
+ }
50
+ // Subscription seats: collector failure is fatal for replacement (§10:
51
+ // stale/malformed/unknown excludes the route; fail closed).
52
+ const observations = (Array.isArray(quotaObservations) ? quotaObservations : [])
53
+ .filter((obs) => obs?.seatId === seat.seatId);
54
+ if (seat.quotaCollector !== null) {
55
+ const anyOk = observations.some((obs) => obs.sourceStatus === "ok");
56
+ if (!anyOk) {
57
+ return { dead: true, reason: `collector for seat "${seat.seatId}" reports failure (fail closed)` };
58
+ }
59
+ }
60
+ return { dead: false, reason: null };
61
+ }
62
+
63
+ // The replacement decision (§7.2). Inputs: the authenticated envelope's
64
+ // route identity, the candidate replacement seat (from a NEW route decision
65
+ // when the seat differs), and current health/quota observations.
66
+ // Returns one of:
67
+ // { action: "reuse-envelope" } — same seat, alive
68
+ // { action: "new-route-decision" } — different seat required
69
+ // { action: "refused", reason } — dead route / invalid
70
+ export function replacementAction({ envelope, replacementSeatId = null, seatsById, healthObservations = {}, quotaObservations = [], now }) {
71
+ const currentSeatId = typeof envelope?.seatId === "string" ? envelope.seatId : null;
72
+ if (currentSeatId === null) {
73
+ return { action: "refused", reason: "the envelope carries no seat identity (fails closed)" };
74
+ }
75
+ const targetSeatId = replacementSeatId ?? currentSeatId;
76
+ const seat = seatsById?.get?.(targetSeatId) ?? null;
77
+
78
+ // Dead-route rule first: the target seat (even the same seat) must be alive.
79
+ const dead = isDeadRoute({ seat, healthObservations, quotaObservations, now });
80
+ if (dead.dead) {
81
+ return { action: "refused", reason: `dead route: ${dead.reason} (a dead route can never be used for replacement)` };
82
+ }
83
+
84
+ if (targetSeatId === currentSeatId) {
85
+ // Same-seat replacement: the envelope is reused; the route is pinned.
86
+ return { action: "reuse-envelope", seatId: currentSeatId };
87
+ }
88
+ // Different-seat replacement: a NEW route-decision record AND a new
89
+ // claim/envelope are required (the old envelope is never re-authenticated
90
+ // for another seat). The new decision carries parentDecisionId lineage.
91
+ return { action: "new-route-decision", fromSeatId: currentSeatId, toSeatId: targetSeatId };
92
+ }
93
+
94
+ // Lineage digest for the replacement chain: binds parent decision, old
95
+ // envelope, and attempt index into the new decision's audit trail.
96
+ export function lineageDigest({ parentDecisionId, oldEnvelopeId, attemptIndex }) {
97
+ return sha256Hex(JSON.stringify({ parentDecisionId, oldEnvelopeId, attemptIndex }));
98
+ }
@@ -0,0 +1,299 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ // Production router runtime loader. Configuration precedence is the closed
5
+ // user profile over repository defaults. SQLite remains operational state;
6
+ // authenticated claims remain authority and are used only to repair it.
7
+ //
8
+ // Two run modes:
9
+ // - write mode (default): a dispatch path. Expires stale reserved rows,
10
+ // reconciles claimed reservations from the authenticated claims file,
11
+ // settles terminal claims, heartbeats live claims, and ingests
12
+ // observations/receipts.
13
+ // - read-only mode (W4): the `check` observation path. It reads the
14
+ // collector cache and the store without a single write, and never creates
15
+ // a store that does not exist. `persisted: false` is then true.
16
+ //
17
+ // The localHealth probe (W2) produces endpoint-keyed, read-time-fresh health
18
+ // observations for `kind: "local"` seats. A failed probe is never "ok".
19
+
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import { join, resolve } from "node:path";
22
+ import { homedir } from "node:os";
23
+ import { mergeRouterConfig, validateRouterConfig, computeConfigDigest, readCollectorCache } from "./router_schemas_pi.js";
24
+ import {
25
+ openRouterStore, closeRouterStore, activeReservations, consumptionSamples,
26
+ expireStaleReservations, insertQuotaObservation, insertConsumptionSample,
27
+ reconcileReservationFromClaim, followClaimConsumption, renewReservationLease,
28
+ } from "./router_store_pi.js";
29
+ import { readClaimsState } from "./task_board_core_pi.js";
30
+
31
+ export const ROUTER_DEFAULTS_RELATIVE_PATH = ".agentic-driver/router.defaults.json";
32
+ export const ROUTER_PROFILE_DEFAULT_PATH = join(homedir(), ".config", "agentic-driver", "router", "profile.json");
33
+ export const ROUTER_CACHE_ROOT_DEFAULT_PATH = join(homedir(), ".local", "share", "agentic-driver", "quota");
34
+ export const HEALTH_OBSERVATION_SCHEMA = "agentic-driver.health-observation.v1";
35
+ export const LOCAL_HEALTH_PATH = "/health";
36
+
37
+ function readJson(path, { optional = false } = {}) {
38
+ if (!existsSync(path)) {
39
+ if (optional) return null;
40
+ throw new Error(`router configuration is missing: ${path}`);
41
+ }
42
+ return JSON.parse(readFileSync(path, "utf8"));
43
+ }
44
+
45
+ function expandHome(path) {
46
+ return path === "~" ? homedir() : path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
47
+ }
48
+
49
+ export function loadRouterConfig({ repoRoot = process.cwd(), defaultsPath = null, profilePath = null } = {}) {
50
+ const resolvedDefaults = resolve(defaultsPath ?? join(repoRoot, ROUTER_DEFAULTS_RELATIVE_PATH));
51
+ const resolvedProfile = resolve(profilePath ?? process.env.AGENTIC_DRIVER_ROUTER_PROFILE ?? ROUTER_PROFILE_DEFAULT_PATH);
52
+ const defaults = readJson(resolvedDefaults);
53
+ const profile = readJson(resolvedProfile, { optional: true });
54
+ const config = mergeRouterConfig(defaults, profile);
55
+ if (config === null) throw new Error("router defaults/profile merge is invalid (fails closed)");
56
+ const validation = validateRouterConfig(config);
57
+ if (!validation.ok) throw new Error(`router configuration is invalid: ${validation.errors.join("; ")}`);
58
+ return { config, configDigest: computeConfigDigest(config), configRevision: config.revision,
59
+ defaultsPath: resolvedDefaults, profilePath: resolvedProfile, profileLoaded: profile !== null };
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // localHealth probe adapter (W2, design §8.12)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function healthBase({ endpointRef, at, expiresAt }) {
67
+ return {
68
+ schema: HEALTH_OBSERVATION_SCHEMA,
69
+ endpointRef,
70
+ capturedAt: at,
71
+ expiresAt,
72
+ sourceStatus: "unknown",
73
+ reason: null,
74
+ warm: null,
75
+ memoryHeadroomBytes: null,
76
+ concurrency: null,
77
+ };
78
+ }
79
+
80
+ // Probe ONE endpoint. The probe fails closed: a missing endpoint, a missing
81
+ // transport, a non-2xx response, a malformed body, a body status other than
82
+ // "ok", an exhausted memory headroom, or a full concurrency limit all yield a
83
+ // non-"ok" sourceStatus. Only a verified successful probe is ever "ok".
84
+ async function probeEndpoint({ endpointRef, endpoint, at, expiresAt, fetchFn, timeoutMs, warmStateTracking }) {
85
+ const base = healthBase({ endpointRef, at, expiresAt });
86
+ if (endpoint === null || typeof endpoint !== "object" || typeof endpoint.url !== "string" || endpoint.url === "") {
87
+ return { ...base, sourceStatus: "unknown", reason: `endpoint "${endpointRef}" does not resolve in endpoints{}` };
88
+ }
89
+ if (typeof fetchFn !== "function") {
90
+ return { ...base, sourceStatus: "unknown", reason: "no health probe transport is available (fails closed)" };
91
+ }
92
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
93
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
94
+ let response;
95
+ try {
96
+ response = await fetchFn(`${endpoint.url.replace(/\/$/, "")}${LOCAL_HEALTH_PATH}`, { method: "GET", signal: controller?.signal });
97
+ } catch (error) {
98
+ return { ...base, sourceStatus: "unreachable", reason: `health probe failed: ${String(error?.message || error).slice(0, 128)}` };
99
+ } finally {
100
+ if (timer) clearTimeout(timer);
101
+ }
102
+ if (response === null || typeof response !== "object" || response.ok !== true) {
103
+ return { ...base, sourceStatus: "unreachable", reason: `health endpoint returned status ${response?.status ?? "no response"}` };
104
+ }
105
+ let body = null;
106
+ try { body = typeof response.json === "function" ? await response.json() : null; } catch (error) {
107
+ return { ...base, sourceStatus: "parse-error", reason: `health body is not JSON: ${String(error?.message || error).slice(0, 128)}` };
108
+ }
109
+ if (body === null || typeof body !== "object" || Array.isArray(body) || body.status !== "ok") {
110
+ return { ...base, sourceStatus: "unreachable", reason: `health status "${body?.status ?? "absent"}" is not ok` };
111
+ }
112
+ let memoryHeadroomBytes = null;
113
+ const memory = body.memory ?? null;
114
+ if (memory !== null && typeof memory === "object" && Number.isFinite(memory.free)) {
115
+ memoryHeadroomBytes = memory.free;
116
+ if (memory.free <= 0) return { ...base, sourceStatus: "unreachable", reason: "no memory headroom on the endpoint" };
117
+ }
118
+ let concurrency = null;
119
+ const slots = body.concurrency ?? null;
120
+ if (slots !== null && typeof slots === "object" && Number.isFinite(slots.active) && Number.isFinite(slots.limit)) {
121
+ concurrency = { active: slots.active, limit: slots.limit };
122
+ if (slots.limit <= 0 || slots.active >= slots.limit) {
123
+ return { ...base, sourceStatus: "unreachable", reason: `endpoint concurrency is full (${slots.active}/${slots.limit})` };
124
+ }
125
+ }
126
+ const warm = typeof body.warm === "boolean" ? body.warm
127
+ : warmStateTracking && typeof body.model_loaded === "boolean" ? body.model_loaded : null;
128
+ return { ...base, sourceStatus: "ok", reason: null, warm, memoryHeadroomBytes, concurrency };
129
+ }
130
+
131
+ // Probe every endpoint a local seat needs. For a spanning cluster seat the
132
+ // seat's endpointRef observation is "ok" only when BOTH nodes are ready; a
133
+ // single failed node fails the seat closed (design §8.12).
134
+ export async function probeLocalHealth({ config, now = null, fetchFn = null, timeoutMs = 3000 } = {}) {
135
+ const at = now ?? new Date().toISOString();
136
+ const probeSeconds = Number.isInteger(config?.localHealth?.probeSeconds) && config.localHealth.probeSeconds > 0
137
+ ? config.localHealth.probeSeconds : 60;
138
+ const expiresAt = new Date(Date.parse(at) + probeSeconds * 1000).toISOString();
139
+ const warmStateTracking = config?.localHealth?.warmStateTracking === true;
140
+ const doFetch = fetchFn ?? globalThis.fetch?.bind(globalThis) ?? null;
141
+ const localSeats = (config?.seats ?? []).filter((seat) => seat?.kind === "local" && seat.enabled === true);
142
+ const requiredBySeat = new Map();
143
+ const endpointsToProbe = new Set();
144
+ for (const seat of localSeats) {
145
+ const refs = new Set([seat.endpointRef]);
146
+ if (seat.clusterMembership?.spansNodes === true) {
147
+ for (const node of Array.isArray(seat.clusterMembership.nodes) ? seat.clusterMembership.nodes : []) refs.add(node);
148
+ }
149
+ requiredBySeat.set(seat.seatId, [...refs]);
150
+ for (const ref of refs) endpointsToProbe.add(ref);
151
+ }
152
+ const probes = {};
153
+ for (const ref of endpointsToProbe) {
154
+ probes[ref] = await probeEndpoint({ endpointRef: ref, endpoint: config?.endpoints?.[ref], at, expiresAt, fetchFn: doFetch, timeoutMs, warmStateTracking });
155
+ }
156
+ const observations = { ...probes };
157
+ for (const [seatId, refs] of requiredBySeat) {
158
+ if (refs.length <= 1) continue;
159
+ const seat = localSeats.find((candidate) => candidate.seatId === seatId);
160
+ const failed = refs.filter((ref) => probes[ref]?.sourceStatus !== "ok");
161
+ if (failed.length > 0) {
162
+ observations[seat.endpointRef] = {
163
+ ...(probes[seat.endpointRef] ?? healthBase({ endpointRef: seat.endpointRef, at, expiresAt })),
164
+ sourceStatus: "unreachable",
165
+ reason: `spanning cluster seat "${seatId}" is not ready: endpoint(s) ${failed.join(", ")} did not report ok`,
166
+ };
167
+ }
168
+ }
169
+ return { observations, probes, probedAt: at, expiresAt };
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Store acquisition (W5): cache the write store per db path so a tick loop
174
+ // reuses one handle instead of leaking a DatabaseSync per invocation. A
175
+ // read-only store is opened only for its call and closed immediately.
176
+ // ---------------------------------------------------------------------------
177
+
178
+ const writeStoreCache = new Map();
179
+
180
+ function acquireWriteStore(dbPath) {
181
+ const key = dbPath ?? "__default__";
182
+ const cached = writeStoreCache.get(key);
183
+ if (cached !== undefined && cached.closed !== true && cached.db !== null) return cached;
184
+ const store = openRouterStore({ dbPath });
185
+ writeStoreCache.set(key, store);
186
+ return store;
187
+ }
188
+
189
+ export function closeCachedRouterStores() {
190
+ for (const store of writeStoreCache.values()) closeRouterStore(store);
191
+ writeStoreCache.clear();
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Terminal-claim settlement + heartbeat (L1). The authenticated claims file
196
+ // is authority: a reservation bound to a consumed claim settles to
197
+ // consumed/released, a reservation bound to a still-active claim is
198
+ // heartbeated, and a reservation bound to no claim is released. SQLite is
199
+ // repaired FROM the claims file, never the reverse.
200
+ // ---------------------------------------------------------------------------
201
+
202
+ export function settleTerminalReservations(store, { claimsState, now }) {
203
+ const activeEnvelopeIds = new Set((claimsState?.claims ?? []).map((claim) => claim.envelopeId));
204
+ const consumedByEnvelopeId = new Map((claimsState?.consumedClaims ?? []).map((entry) => [entry.envelopeId, entry.reason]));
205
+ const reservations = activeReservations(store, { now });
206
+ const settled = [];
207
+ const seen = new Set();
208
+ for (const reservation of reservations) {
209
+ if (reservation.state !== "claimed" || typeof reservation.envelopeId !== "string" || reservation.envelopeId === "") continue;
210
+ if (seen.has(reservation.envelopeId)) continue;
211
+ seen.add(reservation.envelopeId);
212
+ if (activeEnvelopeIds.has(reservation.envelopeId)) {
213
+ renewReservationLease(store, { reservationId: reservation.reservationId, now });
214
+ continue;
215
+ }
216
+ const reason = consumedByEnvelopeId.get(reservation.envelopeId) ?? "reclaimed";
217
+ const followed = followClaimConsumption(store, { envelopeId: reservation.envelopeId, reason, now });
218
+ settled.push(...followed.followed);
219
+ }
220
+ return settled;
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Runtime assembly
225
+ // ---------------------------------------------------------------------------
226
+
227
+ export function prepareRouterRuntime({ repoRoot = process.cwd(), boardPath = null, defaultsPath = null,
228
+ profilePath = null, dbPath = null, cacheRoot = null, now = null, consumptionReceipts = [], readOnly = false } = {}) {
229
+ const loaded = loadRouterConfig({ repoRoot, defaultsPath, profilePath });
230
+ const at = now ?? new Date().toISOString();
231
+ const store = readOnly
232
+ ? openRouterStore({ dbPath, readOnly: true })
233
+ : acquireWriteStore(dbPath);
234
+
235
+ let claimsState = { claims: [], consumedClaims: [] };
236
+ // Terminal settlement and heartbeat repair SQLite only from an
237
+ // authoritative claims file. Without a board path there is no authority to
238
+ // repair against, so reservations are left untouched (fail closed).
239
+ const hasClaimsAuthority = boardPath && existsSync(boardPath);
240
+ if (hasClaimsAuthority) {
241
+ const read = readClaimsState(boardPath);
242
+ if (!read.ok) throw Object.assign(new Error(read.reason), { code: "claims-corrupt" });
243
+ claimsState = read.state;
244
+ if (!readOnly) {
245
+ for (const claim of claimsState.claims) reconcileReservationFromClaim(store, { claim, now: at });
246
+ }
247
+ }
248
+
249
+ const trustedRoot = resolve(cacheRoot ?? process.env.AGENTIC_DRIVER_ROUTER_CACHE_ROOT ?? ROUTER_CACHE_ROOT_DEFAULT_PATH);
250
+ const quotaObservations = [];
251
+ for (const [collectorId, collector] of Object.entries(loaded.config.collectors)) {
252
+ const cachePath = expandHome(collector.cachePath);
253
+ const read = readCollectorCache({ cachePath, trustedRoot });
254
+ if (!read.ok || read.observation.collector !== collectorId) continue;
255
+ if (!readOnly) insertQuotaObservation(store, read.observation);
256
+ quotaObservations.push(read.observation);
257
+ }
258
+
259
+ try {
260
+ if (!readOnly) {
261
+ expireStaleReservations(store, { now: at });
262
+ // L1: settle reservations whose claim is terminal (consumed/gone) and
263
+ // heartbeat reservations whose claim is still active, but only from an
264
+ // authoritative claims file.
265
+ if (hasClaimsAuthority) settleTerminalReservations(store, { claimsState, now: at });
266
+ for (const receipt of Array.isArray(consumptionReceipts) ? consumptionReceipts : []) {
267
+ if (typeof receipt?.seatId !== "string" || typeof receipt?.unit !== "string"
268
+ || !Number.isFinite(receipt?.quantity) || receipt.quantity < 0) continue;
269
+ insertConsumptionSample(store, { seatId: receipt.seatId, unit: receipt.unit,
270
+ quantity: receipt.quantity, claimId: receipt.claimId ?? null, capturedAt: receipt.capturedAt ?? at });
271
+ }
272
+ }
273
+ const snapshot = {
274
+ quotaObservations,
275
+ consumptionSamples: store.missing === true ? [] : consumptionSamples(store),
276
+ existingReservations: store.missing === true ? [] : activeReservations(store, { now: at }),
277
+ };
278
+ return {
279
+ ...loaded, routerConfig: loaded.config, routerStore: readOnly ? null : store, routerNow: at,
280
+ routerSnapshot: snapshot, cacheRoot: trustedRoot,
281
+ routerReadOnly: readOnly, routerStorePath: store.path,
282
+ };
283
+ } finally {
284
+ // Read-only runtimes never outlive the call; the handle is closed here so
285
+ // the pure `check` path cannot leak a store.
286
+ if (readOnly) closeRouterStore(store);
287
+ }
288
+ }
289
+
290
+ // The production entry: assemble the runtime and attach endpoint-keyed
291
+ // localHealth observations to the snapshot (W2). In read-only mode this is
292
+ // still pure: probes are network reads and the store is opened read-only.
293
+ export async function prepareRouterRuntimeWithHealth({ fetchFn = null, healthTimeoutMs = 3000, ...options } = {}) {
294
+ const runtime = prepareRouterRuntime(options);
295
+ const health = await probeLocalHealth({ config: runtime.routerConfig, now: runtime.routerNow, fetchFn, timeoutMs: healthTimeoutMs });
296
+ runtime.routerSnapshot.healthObservations = health.observations;
297
+ runtime.healthProbes = health.probes;
298
+ return runtime;
299
+ }