@ouro.bot/cli 0.1.0-alpha.661 → 0.1.0-alpha.663

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.
@@ -0,0 +1,752 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.contextLossSentinelPaths = contextLossSentinelPaths;
37
+ exports.deriveContextLossSentinelProviderSignals = deriveContextLossSentinelProviderSignals;
38
+ exports.refreshContextLossSentinel = refreshContextLossSentinel;
39
+ exports.readContextLossSentinelView = readContextLossSentinelView;
40
+ exports.formatContextLossSentinelText = formatContextLossSentinelText;
41
+ exports.formatContextLossSentinelJson = formatContextLossSentinelJson;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const child_process_1 = require("child_process");
45
+ const crypto_1 = require("crypto");
46
+ const flight_recorder_1 = require("../arc/flight-recorder");
47
+ const runtime_1 = require("../nerves/runtime");
48
+ const context_loss_gauntlet_1 = require("./context-loss-gauntlet");
49
+ const provider_visibility_1 = require("./provider-visibility");
50
+ const REQUIRED_LANES = ["outward", "inner"];
51
+ function logicalLocator(...parts) {
52
+ return path.posix.join(...parts);
53
+ }
54
+ function relativeSentinelRoot() {
55
+ return logicalLocator("arc", "flight-recorder", "context-loss-sentinel");
56
+ }
57
+ function latestLocator() {
58
+ return logicalLocator(relativeSentinelRoot(), "latest.json");
59
+ }
60
+ function latestReadyLocator() {
61
+ return logicalLocator(relativeSentinelRoot(), "latest-ready.json");
62
+ }
63
+ function receiptLocator(receiptId) {
64
+ return logicalLocator(relativeSentinelRoot(), "receipts", `${receiptId}.json`);
65
+ }
66
+ function historyDay(generatedAt) {
67
+ return generatedAt.slice(0, 10);
68
+ }
69
+ function historyLocator(generatedAt) {
70
+ return logicalLocator(relativeSentinelRoot(), "history", `${historyDay(generatedAt)}.jsonl`);
71
+ }
72
+ function contextLossSentinelPaths(agentRoot) {
73
+ const rootDir = path.join(agentRoot, relativeSentinelRoot());
74
+ return {
75
+ rootDir,
76
+ latest: path.join(rootDir, "latest.json"),
77
+ latestReady: path.join(rootDir, "latest-ready.json"),
78
+ historyDir: path.join(rootDir, "history"),
79
+ receiptsDir: path.join(rootDir, "receipts"),
80
+ lock: path.join(rootDir, ".write.lock"),
81
+ };
82
+ }
83
+ function atomicWriteJson(filePath, value) {
84
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
85
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${(0, crypto_1.randomUUID)()}.tmp`;
86
+ fs.writeFileSync(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
87
+ fs.renameSync(tmpPath, filePath);
88
+ }
89
+ function sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
+ }
92
+ async function withFileLock(lockPath, timeoutMs, fn) {
93
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
94
+ const startedAt = Date.now();
95
+ while (true) {
96
+ try {
97
+ const fd = fs.openSync(lockPath, "wx");
98
+ try {
99
+ fs.writeFileSync(fd, `${process.pid}\n`, "utf-8");
100
+ return await fn();
101
+ }
102
+ finally {
103
+ fs.closeSync(fd);
104
+ fs.rmSync(lockPath, { force: true });
105
+ }
106
+ }
107
+ catch (error) {
108
+ const code = String(error.code);
109
+ if (code !== "EEXIST") {
110
+ throw error;
111
+ }
112
+ if (Date.now() - startedAt > timeoutMs) {
113
+ throw new Error(`context-loss Sentinel lock timed out: ${lockPath}`);
114
+ }
115
+ await sleep(5);
116
+ }
117
+ }
118
+ }
119
+ function compareReceiptOrder(left, right) {
120
+ const leftTime = Date.parse(left.generatedAt);
121
+ const rightTime = Date.parse(right.generatedAt);
122
+ if (leftTime !== rightTime)
123
+ return leftTime - rightTime;
124
+ return left.id.localeCompare(right.id);
125
+ }
126
+ function shouldReplaceReceipt(existing, candidate) {
127
+ return existing === null || compareReceiptOrder(candidate, existing) >= 0;
128
+ }
129
+ function readJson(filePath) {
130
+ try {
131
+ return { ok: true, value: JSON.parse(fs.readFileSync(filePath, "utf-8")) };
132
+ }
133
+ catch (error) {
134
+ return { ok: false, reason: String(error) };
135
+ }
136
+ }
137
+ function isStringArray(value) {
138
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
139
+ }
140
+ function isValidTimestamp(value) {
141
+ return Number.isFinite(Date.parse(value));
142
+ }
143
+ function isSource(value) {
144
+ if (!value || typeof value !== "object" || Array.isArray(value))
145
+ return false;
146
+ const record = value;
147
+ return typeof record.kind === "string" && typeof record.locator === "string";
148
+ }
149
+ function isRepair(value) {
150
+ if (!value || typeof value !== "object" || Array.isArray(value))
151
+ return false;
152
+ const record = value;
153
+ return (record.actor === "agent-runnable" || record.actor === "human-required" || record.actor === "human-choice")
154
+ && typeof record.kind === "string"
155
+ && (record.command === undefined || typeof record.command === "string")
156
+ && typeof record.detail === "string";
157
+ }
158
+ function isSignal(value) {
159
+ if (!value || typeof value !== "object" || Array.isArray(value))
160
+ return false;
161
+ const record = value;
162
+ return typeof record.id === "string"
163
+ && (record.kind === "gauntlet" || record.kind === "provider_lane" || record.kind === "sense" || record.kind === "daemon" || record.kind === "bundle")
164
+ && (record.status === "pass" || record.status === "warn" || record.status === "fail")
165
+ && (record.severity === "info" || record.severity === "warn" || record.severity === "critical")
166
+ && (record.verdictImpact === "none" || record.verdictImpact === "watch" || record.verdictImpact === "blocked")
167
+ && typeof record.summary === "string"
168
+ && isSource(record.source)
169
+ && (record.repair === undefined || isRepair(record.repair));
170
+ }
171
+ function isRecoveryAnchor(value) {
172
+ if (!value || typeof value !== "object" || Array.isArray(value))
173
+ return false;
174
+ const record = value;
175
+ return (record.kind === "flight-recorder" || record.kind === "latest-ready")
176
+ && (record.currentAsk === null || typeof record.currentAsk === "string")
177
+ && (record.nextSafeAction === null || typeof record.nextSafeAction === "string")
178
+ && typeof record.flightRecorderLatestLocator === "string"
179
+ && isStringArray(record.sourceEventIds)
180
+ && (record.recordedAt === null || typeof record.recordedAt === "string");
181
+ }
182
+ function isGauntletSummary(value) {
183
+ if (!value || typeof value !== "object" || Array.isArray(value))
184
+ return false;
185
+ const record = value;
186
+ return (record.verdict === "ready" || record.verdict === "watch" || record.verdict === "blocked")
187
+ && typeof record.scorePercentage === "number"
188
+ && isStringArray(record.failedChecks)
189
+ && isStringArray(record.warnedChecks)
190
+ && typeof record.sourceLocator === "string";
191
+ }
192
+ function isResumeSnapshot(value) {
193
+ return (0, flight_recorder_1.isFlightRecorderResume)(value);
194
+ }
195
+ function isReceipt(value) {
196
+ if (!value || typeof value !== "object" || Array.isArray(value))
197
+ return false;
198
+ const record = value;
199
+ return record.schemaVersion === 1
200
+ && typeof record.id === "string"
201
+ && typeof record.agent === "string"
202
+ && (record.trigger === "post_turn" || record.trigger === "provider_failover" || record.trigger === "daemon_startup" || record.trigger === "daemon_health" || record.trigger === "session_start" || record.trigger === "manual_cli")
203
+ && typeof record.generatedAt === "string"
204
+ && isValidTimestamp(record.generatedAt)
205
+ && (record.verdict === "ready" || record.verdict === "watch" || record.verdict === "blocked")
206
+ && typeof record.summary === "string"
207
+ && typeof record.receiptLocator === "string"
208
+ && (record.latestReadyLocator === null || typeof record.latestReadyLocator === "string")
209
+ && isRecoveryAnchor(record.recoveryAnchor)
210
+ && isGauntletSummary(record.gauntlet)
211
+ && Array.isArray(record.signals)
212
+ && record.signals.every(isSignal)
213
+ && isStringArray(record.sourceLocators)
214
+ && isResumeSnapshot(record.resumeSnapshot);
215
+ }
216
+ function readReceiptFile(filePath, label, issues) {
217
+ if (!fs.existsSync(filePath))
218
+ return null;
219
+ const parsed = readJson(filePath);
220
+ if (!parsed.ok) {
221
+ issues.push(`${label} unreadable: ${parsed.reason}`);
222
+ return null;
223
+ }
224
+ if (!isReceipt(parsed.value)) {
225
+ issues.push(`${label} malformed`);
226
+ return null;
227
+ }
228
+ return parsed.value;
229
+ }
230
+ function isNonEmptyText(value) {
231
+ return typeof value === "string" && value.trim().length > 0;
232
+ }
233
+ function hasSafeResumeSnapshot(receipt) {
234
+ const resume = receipt.resumeSnapshot;
235
+ return resume.canContinue
236
+ && resume.hasCompleteState
237
+ && resume.recorderHealth.status === "ok"
238
+ && resume.blockedBecause.length === 0
239
+ && isNonEmptyText(resume.currentAsk.value)
240
+ && isNonEmptyText(resume.nextSafeAction.value);
241
+ }
242
+ function readLatestReadyFile(filePath, label, issues) {
243
+ const receipt = readReceiptFile(filePath, label, issues);
244
+ if (receipt && receipt.verdict !== "ready") {
245
+ issues.push(`${label} is not a ready receipt`);
246
+ return null;
247
+ }
248
+ if (receipt && (receipt.gauntlet.verdict !== "ready"
249
+ || receipt.signals.some((signal) => signal.verdictImpact !== "none")
250
+ || !hasSafeResumeSnapshot(receipt))) {
251
+ issues.push(`${label} is not a semantically ready receipt`);
252
+ return null;
253
+ }
254
+ return receipt;
255
+ }
256
+ function readLatestReady(agentRoot) {
257
+ const receipt = readLatestReadyFile(contextLossSentinelPaths(agentRoot).latestReady, "latest-ready.json", []);
258
+ return receipt?.verdict === "ready" ? receipt : null;
259
+ }
260
+ function signalStatusForImpact(impact) {
261
+ if (impact === "blocked")
262
+ return { status: "fail", severity: "critical" };
263
+ if (impact === "watch")
264
+ return { status: "warn", severity: "warn" };
265
+ return { status: "pass", severity: "info" };
266
+ }
267
+ function providerCheckCommand(agentName, lane) {
268
+ return `ouro provider check --agent ${agentName} --lane ${lane}`;
269
+ }
270
+ function sourceForLane(lane) {
271
+ return {
272
+ kind: "provider-visibility",
273
+ locator: `agent.json#providers.${lane}`,
274
+ };
275
+ }
276
+ function repair(actor, kind, detail, command) {
277
+ return {
278
+ actor,
279
+ kind,
280
+ detail,
281
+ command,
282
+ };
283
+ }
284
+ function providerSignal(visibility, lane, impact, summary, repairValue, meta = {}) {
285
+ return {
286
+ id: `provider:${lane}`,
287
+ kind: "provider_lane",
288
+ ...signalStatusForImpact(impact),
289
+ verdictImpact: impact,
290
+ summary,
291
+ source: sourceForLane(lane),
292
+ ...(repairValue ? { repair: repairValue } : {}),
293
+ meta: {
294
+ agentName: visibility.agentName,
295
+ lane,
296
+ ...meta,
297
+ },
298
+ };
299
+ }
300
+ function configuredProviderSignal(visibility, lane) {
301
+ if (lane.credential.status === "missing") {
302
+ return providerSignal(visibility, lane.lane, "blocked", `${lane.lane} credentials missing for ${lane.provider}`, repair("human-required", "provider-credential", `${lane.provider} credentials must be added to the agent vault.`, lane.credential.repairCommand), { provider: lane.provider, model: lane.model, credentialStatus: lane.credential.status });
303
+ }
304
+ if (lane.credential.status === "invalid-pool") {
305
+ return providerSignal(visibility, lane.lane, "blocked", `${lane.lane} credential vault unavailable for ${lane.provider}`, repair("human-required", "vault-unavailable", "The agent credential vault must be unlocked or repaired.", lane.credential.repairCommand), { provider: lane.provider, model: lane.model, credentialStatus: lane.credential.status });
306
+ }
307
+ if (lane.credential.status === "not-loaded") {
308
+ return providerSignal(visibility, lane.lane, "watch", `${lane.lane} credentials not loaded for ${lane.provider}`, repair("agent-runnable", "provider-credential-cache", "Refresh the in-process provider credential cache.", lane.credential.repairCommand ?? `ouro provider refresh --agent ${visibility.agentName}`), { provider: lane.provider, model: lane.model, credentialStatus: lane.credential.status });
309
+ }
310
+ if (lane.readiness.status === "failed") {
311
+ return providerSignal(visibility, lane.lane, "blocked", `${lane.lane} live check failed for ${lane.provider}${lane.readiness.error ? `: ${lane.readiness.error}` : ""}`, repair("agent-runnable", "provider-live-check", "Run a fresh provider capability check and inspect the failure.", providerCheckCommand(visibility.agentName, lane.lane)), { provider: lane.provider, model: lane.model, readinessStatus: lane.readiness.status, checkedAt: lane.readiness.checkedAt ?? null, attempts: lane.readiness.attempts ?? null });
312
+ }
313
+ if (lane.readiness.status === "stale" && (lane.readiness.checkedAt || lane.readiness.reason || lane.readiness.error)) {
314
+ return providerSignal(visibility, lane.lane, "watch", `${lane.lane} readiness stale for ${lane.provider}${lane.readiness.reason ? `: ${lane.readiness.reason}` : ""}`, repair("agent-runnable", "provider-live-check", "Run a fresh provider capability check before relying on this lane.", providerCheckCommand(visibility.agentName, lane.lane)), { provider: lane.provider, model: lane.model, readinessStatus: lane.readiness.status, checkedAt: lane.readiness.checkedAt ?? null });
315
+ }
316
+ if (lane.readiness.status === "unknown" || lane.readiness.status === "stale") {
317
+ return providerSignal(visibility, lane.lane, "watch", `${lane.lane} readiness unknown for ${lane.provider}${lane.readiness.reason ? `: ${lane.readiness.reason}` : ""}`, repair("agent-runnable", "provider-live-check", "Run a provider capability check; Sentinel will not invent stale readiness without evidence.", providerCheckCommand(visibility.agentName, lane.lane)), { provider: lane.provider, model: lane.model, readinessStatus: "unknown" });
318
+ }
319
+ return providerSignal(visibility, lane.lane, "none", `${lane.lane} provider ready: ${lane.provider} / ${lane.model}`, undefined, { provider: lane.provider, model: lane.model, readinessStatus: lane.readiness.status, checkedAt: lane.readiness.checkedAt ?? null });
320
+ }
321
+ function missingProviderLaneSignal(visibility, lane) {
322
+ return providerSignal(visibility, lane, "blocked", `${lane} provider visibility missing from deterministic provider report`, repair("agent-runnable", "provider-visibility", "Refresh Sentinel from a complete provider visibility source.", `ouro work sentinel refresh --agent ${visibility.agentName}`), { laneStatus: "missing-from-report" });
323
+ }
324
+ function deriveContextLossSentinelProviderSignals(visibility) {
325
+ return REQUIRED_LANES.map((laneName) => {
326
+ const lane = visibility.lanes.find((entry) => entry.lane === laneName);
327
+ if (!lane) {
328
+ return missingProviderLaneSignal(visibility, laneName);
329
+ }
330
+ if (lane.status === "unconfigured") {
331
+ return providerSignal(visibility, lane.lane, "blocked", `${lane.lane} provider unconfigured: ${lane.reason}`, repair("human-choice", "provider-selection", "Choose a provider and model for this lane.", lane.repairCommand), { laneStatus: lane.status, reason: lane.reason });
332
+ }
333
+ return configuredProviderSignal(visibility, lane);
334
+ });
335
+ }
336
+ function gauntletSignal(report) {
337
+ const impact = report.verdict === "blocked"
338
+ ? "blocked"
339
+ : report.verdict === "watch"
340
+ ? "watch"
341
+ : "none";
342
+ return {
343
+ id: "gauntlet:context-loss",
344
+ kind: "gauntlet",
345
+ ...signalStatusForImpact(impact),
346
+ verdictImpact: impact,
347
+ summary: report.summary,
348
+ source: {
349
+ kind: "context-loss-gauntlet",
350
+ locator: "arc/flight-recorder/latest.json",
351
+ },
352
+ meta: {
353
+ scorePercentage: report.score.percentage,
354
+ failedChecks: report.checks.filter((check) => check.status === "fail").map((check) => check.id),
355
+ warnedChecks: report.checks.filter((check) => check.status === "warn").map((check) => check.id),
356
+ },
357
+ };
358
+ }
359
+ function healthSignalKind(result) {
360
+ return result.name.startsWith("sense-probe:") ? "sense" : "daemon";
361
+ }
362
+ function healthSignals(results) {
363
+ return results
364
+ .filter((result) => result.name.startsWith("sense-probe:") || result.status !== "ok")
365
+ .map((result) => {
366
+ const impact = result.status === "critical"
367
+ ? "blocked"
368
+ : result.status === "warn"
369
+ ? "watch"
370
+ : "none";
371
+ const kind = healthSignalKind(result);
372
+ return {
373
+ id: `${kind}:${result.name}`,
374
+ kind,
375
+ ...signalStatusForImpact(impact),
376
+ verdictImpact: impact,
377
+ summary: result.message,
378
+ source: {
379
+ kind: "daemon-health",
380
+ locator: `daemon.health:${result.name}`,
381
+ },
382
+ meta: { healthStatus: result.status },
383
+ };
384
+ });
385
+ }
386
+ function defaultGitStatus(agentRoot) {
387
+ try {
388
+ const porcelain = (0, child_process_1.execFileSync)("git", ["status", "--porcelain"], {
389
+ cwd: agentRoot,
390
+ encoding: "utf-8",
391
+ stdio: ["ignore", "pipe", "pipe"],
392
+ timeout: 1_000,
393
+ });
394
+ return { ok: true, porcelain };
395
+ }
396
+ catch (error) {
397
+ return { ok: false, error: String(error) };
398
+ }
399
+ }
400
+ function bundleSignal(status) {
401
+ if (!status.ok) {
402
+ return {
403
+ id: "bundle:git",
404
+ kind: "bundle",
405
+ status: "warn",
406
+ severity: "warn",
407
+ verdictImpact: "watch",
408
+ summary: `bundle git status unavailable: ${status.error}`,
409
+ source: { kind: "git", locator: "git status --porcelain" },
410
+ repair: repair("agent-runnable", "bundle-cleanup", "Inspect bundle git state before assuming the local state is clean.", "git status --porcelain"),
411
+ };
412
+ }
413
+ const dirtyEntries = status.porcelain.split(/\r?\n/).filter((line) => line.trim().length > 0);
414
+ if (dirtyEntries.length > 0) {
415
+ return {
416
+ id: "bundle:git",
417
+ kind: "bundle",
418
+ status: "warn",
419
+ severity: "warn",
420
+ verdictImpact: "watch",
421
+ summary: `bundle has ${dirtyEntries.length} uncommitted git status entr${dirtyEntries.length === 1 ? "y" : "ies"}`,
422
+ source: { kind: "git", locator: "git status --porcelain" },
423
+ repair: repair("agent-runnable", "bundle-cleanup", "Resolve or intentionally preserve local bundle changes before handoff.", "git status --porcelain"),
424
+ meta: { dirtyEntries },
425
+ };
426
+ }
427
+ return {
428
+ id: "bundle:git",
429
+ kind: "bundle",
430
+ status: "pass",
431
+ severity: "info",
432
+ verdictImpact: "none",
433
+ summary: "bundle git status clean",
434
+ source: { kind: "git", locator: "git status --porcelain" },
435
+ };
436
+ }
437
+ function sentinelVerdict(signals) {
438
+ if (signals.some((entry) => entry.verdictImpact === "blocked"))
439
+ return "blocked";
440
+ if (signals.some((entry) => entry.verdictImpact === "watch"))
441
+ return "watch";
442
+ return "ready";
443
+ }
444
+ function summaryForVerdict(verdict) {
445
+ if (verdict === "ready")
446
+ return "ready: deterministic recovery state is current and last-known-good is safe";
447
+ if (verdict === "watch")
448
+ return "watch: deterministic recovery can continue, but one or more signals need attention";
449
+ return "blocked: deterministic recovery failed and must use latest-ready or repair before continuing";
450
+ }
451
+ function gauntletSummary(report) {
452
+ return {
453
+ verdict: report.verdict,
454
+ scorePercentage: report.score.percentage,
455
+ failedChecks: report.checks.filter((check) => check.status === "fail").map((check) => check.id),
456
+ warnedChecks: report.checks.filter((check) => check.status === "warn").map((check) => check.id),
457
+ sourceLocator: "arc/flight-recorder/latest.json",
458
+ };
459
+ }
460
+ function anchorFromResume(kind, resume) {
461
+ return {
462
+ kind,
463
+ currentAsk: resume.currentAsk.value,
464
+ nextSafeAction: resume.nextSafeAction.value,
465
+ flightRecorderLatestLocator: "arc/flight-recorder/latest.json",
466
+ sourceEventIds: [...resume.currentAsk.sourceEventIds, ...resume.nextSafeAction.sourceEventIds],
467
+ recordedAt: resume.lastSafeCheckpoint.recordedAt,
468
+ };
469
+ }
470
+ function readFlightRecorderEventsByIds(agentRoot, eventIds) {
471
+ if (eventIds.length === 0)
472
+ return [];
473
+ const wanted = new Set(eventIds);
474
+ const eventsRoot = path.join(agentRoot, "arc", "flight-recorder", "events");
475
+ if (!fs.existsSync(eventsRoot))
476
+ return [];
477
+ return fs.readdirSync(eventsRoot)
478
+ .filter((entry) => entry.endsWith(".jsonl"))
479
+ .flatMap((entry) => fs.readFileSync(path.join(eventsRoot, entry), "utf-8").split(/\r?\n/))
480
+ .filter((line) => line.trim().length > 0)
481
+ .flatMap((line) => {
482
+ try {
483
+ const parsed = JSON.parse(line);
484
+ return wanted.has(parsed.id) ? [parsed] : [];
485
+ }
486
+ catch {
487
+ return [];
488
+ }
489
+ });
490
+ }
491
+ function isSentinelAuthoredBlockerEvent(event) {
492
+ return event.kind === "blocker_detected"
493
+ && (event.meta?.source === "context-loss-sentinel"
494
+ || Boolean(event.producedRefs?.some((ref) => ref.kind === "arc" && ref.locator.startsWith(relativeSentinelRoot()))));
495
+ }
496
+ function hasSentinelAuthoredBlocker(agentRoot, resume) {
497
+ if (resume.blockedBecause.length === 0)
498
+ return false;
499
+ return readFlightRecorderEventsByIds(agentRoot, resume.lastSafeCheckpoint.sourceEventIds)
500
+ .some(isSentinelAuthoredBlockerEvent);
501
+ }
502
+ function selectGauntletResume(agentRoot) {
503
+ const resume = (0, flight_recorder_1.readFlightRecorderResume)(agentRoot);
504
+ const latestReady = readLatestReady(agentRoot);
505
+ if (hasSentinelAuthoredBlocker(agentRoot, resume) && latestReady) {
506
+ return { resume: latestReady.resumeSnapshot, anchorKind: "latest-ready" };
507
+ }
508
+ return { resume, anchorKind: "flight-recorder" };
509
+ }
510
+ function resolveProviderVisibility(agentName, agentRoot, options) {
511
+ return options.providerVisibility ?? (0, provider_visibility_1.buildAgentProviderVisibility)({
512
+ agentName,
513
+ agentRoot,
514
+ homeDir: options.homeDir,
515
+ });
516
+ }
517
+ function shouldUseLatestReadyRecovery(anchorKind, verdict, signals) {
518
+ if (anchorKind === "latest-ready")
519
+ return true;
520
+ if (verdict === "ready")
521
+ return false;
522
+ const hasNonGauntletRisk = signals.some((signal) => signal.kind !== "gauntlet" && signal.verdictImpact !== "none");
523
+ const hasGauntletBlocker = signals.some((signal) => signal.kind === "gauntlet" && signal.verdictImpact === "blocked");
524
+ return hasNonGauntletRisk && !hasGauntletBlocker;
525
+ }
526
+ function makeReceipt(agentName, agentRoot, options, generatedAt) {
527
+ const receiptId = options.createReceiptId?.() ?? `sentinel-${(0, crypto_1.randomUUID)()}`;
528
+ const selectedResume = selectGauntletResume(agentRoot);
529
+ const report = (0, context_loss_gauntlet_1.runContextLossGauntlet)(agentName, agentRoot, {
530
+ now: options.now,
531
+ homeDir: options.homeDir,
532
+ flightRecorderResume: selectedResume.resume,
533
+ });
534
+ const providerVisibility = resolveProviderVisibility(agentName, agentRoot, options);
535
+ const signals = [
536
+ gauntletSignal(report),
537
+ ...deriveContextLossSentinelProviderSignals(providerVisibility),
538
+ ...healthSignals(options.daemonHealthResults ?? []),
539
+ bundleSignal((options.gitStatus ?? (() => defaultGitStatus(agentRoot)))()),
540
+ ];
541
+ const verdict = sentinelVerdict(signals);
542
+ const latestReady = readLatestReady(agentRoot);
543
+ const recoverySource = latestReady && shouldUseLatestReadyRecovery(selectedResume.anchorKind, verdict, signals)
544
+ ? { resume: latestReady.resumeSnapshot, anchorKind: "latest-ready" }
545
+ : selectedResume;
546
+ const readyLocator = verdict === "ready" || latestReady ? latestReadyLocator() : null;
547
+ return {
548
+ schemaVersion: 1,
549
+ id: receiptId,
550
+ agent: agentName,
551
+ trigger: options.trigger,
552
+ generatedAt,
553
+ verdict,
554
+ summary: summaryForVerdict(verdict),
555
+ receiptLocator: receiptLocator(receiptId),
556
+ latestReadyLocator: readyLocator,
557
+ recoveryAnchor: anchorFromResume(recoverySource.anchorKind, recoverySource.resume),
558
+ gauntlet: gauntletSummary(report),
559
+ signals,
560
+ sourceLocators: [
561
+ "arc/flight-recorder/latest.json",
562
+ latestLocator(),
563
+ historyLocator(generatedAt),
564
+ receiptLocator(receiptId),
565
+ ...(readyLocator ? [readyLocator] : []),
566
+ ],
567
+ resumeSnapshot: recoverySource.resume,
568
+ };
569
+ }
570
+ function ensureSentinelDirs(paths) {
571
+ fs.mkdirSync(paths.rootDir, { recursive: true });
572
+ fs.mkdirSync(paths.historyDir, { recursive: true });
573
+ fs.mkdirSync(paths.receiptsDir, { recursive: true });
574
+ }
575
+ function appendHistory(paths, receipt) {
576
+ fs.mkdirSync(paths.historyDir, { recursive: true });
577
+ fs.appendFileSync(path.join(paths.historyDir, `${historyDay(receipt.generatedAt)}.jsonl`), `${JSON.stringify(receipt)}\n`, "utf-8");
578
+ }
579
+ function syncLatestReadyLocator(receipt, hasLatestReady) {
580
+ receipt.latestReadyLocator = hasLatestReady ? latestReadyLocator() : null;
581
+ const locator = latestReadyLocator();
582
+ receipt.sourceLocators = hasLatestReady
583
+ ? Array.from(new Set([...receipt.sourceLocators, locator]))
584
+ : receipt.sourceLocators.filter((entry) => entry !== locator);
585
+ return receipt;
586
+ }
587
+ function syncLatestReadyState(receipt, latestReady) {
588
+ syncLatestReadyLocator(receipt, latestReady !== null);
589
+ if (latestReady && shouldUseLatestReadyRecovery(receipt.recoveryAnchor.kind, receipt.verdict, receipt.signals)) {
590
+ receipt.recoveryAnchor = anchorFromResume("latest-ready", latestReady.resumeSnapshot);
591
+ receipt.resumeSnapshot = latestReady.resumeSnapshot;
592
+ }
593
+ return receipt;
594
+ }
595
+ function blockedSignalSummaries(receipt) {
596
+ return receipt.signals
597
+ .filter((signal) => signal.verdictImpact === "blocked")
598
+ .map((signal) => `${signal.id}: ${signal.summary}`);
599
+ }
600
+ function recordBlockedReceiptEvent(agentRoot, receipt) {
601
+ if (receipt.verdict !== "blocked")
602
+ return;
603
+ (0, flight_recorder_1.recordFlightRecorderEvent)(agentRoot, {
604
+ id: `fr-${receipt.id}`,
605
+ kind: "blocker_detected",
606
+ recordedAt: receipt.generatedAt,
607
+ summary: "context-loss Sentinel blocked recovery",
608
+ blockedBecause: blockedSignalSummaries(receipt).map((summary) => `context-loss Sentinel blocked: ${summary}`),
609
+ producedRefs: [{
610
+ kind: "arc",
611
+ locator: receipt.receiptLocator,
612
+ }],
613
+ meta: {
614
+ source: "context-loss-sentinel",
615
+ receiptId: receipt.id,
616
+ trigger: receipt.trigger,
617
+ },
618
+ });
619
+ }
620
+ async function persistReceipt(agentRoot, receipt, lockTimeoutMs) {
621
+ const paths = contextLossSentinelPaths(agentRoot);
622
+ await withFileLock(paths.lock, lockTimeoutMs, async () => {
623
+ ensureSentinelDirs(paths);
624
+ const existingLatest = readReceiptFile(paths.latest, "latest.json", []);
625
+ const existingReady = readLatestReady(agentRoot);
626
+ const nextReady = receipt.verdict === "ready" && shouldReplaceReceipt(existingReady, receipt)
627
+ ? receipt
628
+ : existingReady;
629
+ syncLatestReadyState(receipt, nextReady);
630
+ atomicWriteJson(path.join(paths.receiptsDir, `${receipt.id}.json`), receipt);
631
+ appendHistory(paths, receipt);
632
+ if (shouldReplaceReceipt(existingLatest, receipt)) {
633
+ atomicWriteJson(paths.latest, receipt);
634
+ recordBlockedReceiptEvent(agentRoot, receipt);
635
+ }
636
+ else if (existingLatest && receipt.verdict === "ready") {
637
+ atomicWriteJson(paths.latest, syncLatestReadyState(existingLatest, nextReady));
638
+ }
639
+ if (receipt.verdict === "ready" && nextReady === receipt) {
640
+ atomicWriteJson(paths.latestReady, receipt);
641
+ }
642
+ });
643
+ }
644
+ async function refreshContextLossSentinel(agentName, agentRoot, options) {
645
+ const generatedAt = (options.now ?? (() => new Date()))().toISOString();
646
+ const receipt = makeReceipt(agentName, agentRoot, options, generatedAt);
647
+ if (options.delayBeforeWriteMs && options.delayBeforeWriteMs > 0) {
648
+ await sleep(options.delayBeforeWriteMs);
649
+ }
650
+ await persistReceipt(agentRoot, receipt, options.lockTimeoutMs ?? 5_000);
651
+ (0, runtime_1.emitNervesEvent)({
652
+ component: "engine",
653
+ event: "engine.context_loss_sentinel_refreshed",
654
+ message: "context-loss Sentinel refreshed deterministic recovery state",
655
+ meta: {
656
+ agentName,
657
+ trigger: options.trigger,
658
+ verdict: receipt.verdict,
659
+ receiptId: receipt.id,
660
+ blockedSignals: receipt.signals.filter((entry) => entry.verdictImpact === "blocked").map((entry) => entry.id),
661
+ watchSignals: receipt.signals.filter((entry) => entry.verdictImpact === "watch").map((entry) => entry.id),
662
+ },
663
+ });
664
+ return receipt;
665
+ }
666
+ function readHistory(paths, limit, issues) {
667
+ if (!fs.existsSync(paths.historyDir))
668
+ return [];
669
+ const files = fs.readdirSync(paths.historyDir)
670
+ .filter((entry) => entry.endsWith(".jsonl"))
671
+ .sort();
672
+ const receipts = [];
673
+ for (const fileName of files) {
674
+ const filePath = path.join(paths.historyDir, fileName);
675
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
676
+ lines.forEach((line, index) => {
677
+ if (line.trim().length === 0)
678
+ return;
679
+ try {
680
+ const parsed = JSON.parse(line);
681
+ if (isReceipt(parsed)) {
682
+ receipts.push(parsed);
683
+ }
684
+ else {
685
+ issues.push(`history/${fileName} line ${index + 1} malformed`);
686
+ }
687
+ }
688
+ catch (error) {
689
+ issues.push(`history/${fileName} line ${index + 1} unreadable: ${String(error)}`);
690
+ }
691
+ });
692
+ }
693
+ return receipts.slice(Math.max(0, receipts.length - limit));
694
+ }
695
+ function readContextLossSentinelView(agentRoot, options = {}) {
696
+ const paths = contextLossSentinelPaths(agentRoot);
697
+ const issues = [];
698
+ const limit = Math.max(0, options.limit ?? 20);
699
+ return {
700
+ schemaVersion: 1,
701
+ latest: readReceiptFile(paths.latest, "latest.json", issues),
702
+ latestReady: readLatestReadyFile(paths.latestReady, "latest-ready.json", issues),
703
+ history: readHistory(paths, limit, issues),
704
+ degraded: { issues },
705
+ };
706
+ }
707
+ function renderSignal(signalEntry) {
708
+ const repairText = signalEntry.repair?.command ? ` repair: ${signalEntry.repair.command}` : "";
709
+ return ` - ${signalEntry.status.toUpperCase()} ${signalEntry.id}: ${signalEntry.summary}${repairText}`;
710
+ }
711
+ function formatReceipt(receipt) {
712
+ return [
713
+ `Recovery Sentinel - ${receipt.agent}`,
714
+ `generated: ${receipt.generatedAt}`,
715
+ `receipt: ${receipt.receiptLocator}`,
716
+ `trigger: ${receipt.trigger}`,
717
+ `verdict: ${receipt.verdict}`,
718
+ `latest-ready: ${receipt.latestReadyLocator ?? "unavailable"}`,
719
+ `summary: ${receipt.summary}`,
720
+ "",
721
+ "Recovery anchor",
722
+ ` kind: ${receipt.recoveryAnchor.kind}`,
723
+ ` current ask: ${receipt.recoveryAnchor.currentAsk ?? "unavailable"}`,
724
+ ` next action: ${receipt.recoveryAnchor.nextSafeAction ?? "unavailable"}`,
725
+ "",
726
+ "Signals",
727
+ ...receipt.signals.map(renderSignal),
728
+ ];
729
+ }
730
+ function formatContextLossSentinelText(input) {
731
+ if (input === null)
732
+ return "Recovery Sentinel - unavailable";
733
+ if ("latest" in input) {
734
+ const displayReceipt = input.latest ?? input.latestReady;
735
+ if (!displayReceipt) {
736
+ return [
737
+ "Recovery Sentinel - unavailable",
738
+ ...input.degraded.issues.map((issue) => `degraded: ${issue}`),
739
+ ].join("\n").trim();
740
+ }
741
+ return [
742
+ ...formatReceipt(displayReceipt),
743
+ "",
744
+ `history: ${input.history.length} receipt${input.history.length === 1 ? "" : "s"}`,
745
+ ...(input.degraded.issues.length > 0 ? ["", ...input.degraded.issues.map((issue) => `degraded: ${issue}`)] : []),
746
+ ].join("\n").trim();
747
+ }
748
+ return formatReceipt(input).join("\n").trim();
749
+ }
750
+ function formatContextLossSentinelJson(input) {
751
+ return `${JSON.stringify(input, null, 2)}\n`;
752
+ }