wendkeep 0.66.4 → 0.66.5

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.
@@ -8,7 +8,88 @@ import { buildEffectiveRequirementPackage, checkSpecsState, evaluateVerdict, tas
8
8
  import { getLocale } from './locale.mjs';
9
9
  import { priceForModel } from './token-usage.mjs';
10
10
  import { countMissing, indexDerivedBySession, listSessionNotes, missingDerivedLinks } from './derived-sections.mjs';
11
- import { readControl } from './obsidian-common.mjs';
11
+ import { readControl, readSessionRegistry } from './obsidian-common.mjs';
12
+ import { parseObservabilityCheckpoint } from './session-observability-state.mjs';
13
+ import { readObservabilityStore } from './session-observability-store.mjs';
14
+ import { assessObservabilityFreshness } from './session-observability-lifecycle.mjs';
15
+
16
+ export function checkSessionObservability(vaultBase, deps = {}) {
17
+ const readRegistry = deps.readRegistry || readSessionRegistry;
18
+ const readStore = deps.readStore || readObservabilityStore;
19
+ const readNote = deps.readNote || readFileSync;
20
+ const statSource = deps.statSource || statSync;
21
+ const registry = readRegistry(vaultBase);
22
+ const result = { ok: true, scanned: 0, healthy: 0, issues: [] };
23
+ const entries = Object.entries(registry?.sessions || {})
24
+ .map(([sessionId, entry]) => ({ sessionId, ...entry }))
25
+ .filter((entry) => entry.session_file)
26
+ .sort((a, b) => a.sessionId.localeCompare(b.sessionId));
27
+
28
+ for (const entry of entries) {
29
+ if (entry.provider && entry.provider !== 'codex') continue;
30
+ const notePath = join(vaultBase, entry.session_file);
31
+ let content;
32
+ try { content = readNote(notePath, 'utf8'); } catch { continue; }
33
+ if (!entry.provider && /^provider:\s*["']?claude/m.test(content)) continue;
34
+ result.scanned += 1;
35
+ const command = `wendkeep cost rebuild --session ${entry.sessionId} --json`;
36
+ const checkpoint = parseObservabilityCheckpoint(content);
37
+ if (!checkpoint) {
38
+ result.issues.push({
39
+ sessionId: entry.sessionId, status: 'legacy', diagnostics: [], command,
40
+ });
41
+ continue;
42
+ }
43
+ if (checkpoint.state === 'degraded') {
44
+ result.issues.push({
45
+ sessionId: entry.sessionId,
46
+ status: 'degraded',
47
+ diagnostics: checkpoint.diagnostics,
48
+ command,
49
+ });
50
+ continue;
51
+ }
52
+
53
+ let runtime;
54
+ try { runtime = readStore(vaultBase, entry.sessionId); } catch {
55
+ runtime = null;
56
+ }
57
+ const assessment = assessObservabilityFreshness({
58
+ checkpoint,
59
+ runtimeState: runtime,
60
+ statSource,
61
+ });
62
+ if (!assessment.fresh) {
63
+ result.issues.push({
64
+ sessionId: entry.sessionId,
65
+ status: assessment.status,
66
+ diagnostics: assessment.diagnostics,
67
+ command,
68
+ });
69
+ continue;
70
+ }
71
+ result.healthy += 1;
72
+ }
73
+ result.ok = result.issues.length === 0;
74
+ return result;
75
+ }
76
+
77
+ export function renderSessionObservabilityLines(result) {
78
+ const issues = result?.issues || [];
79
+ const lines = [
80
+ `[observabilidade] ${result?.healthy || 0} saudável(is) · ${issues.length} reparável(is)`,
81
+ ];
82
+ for (const issue of issues) {
83
+ const diagnostics = issue.diagnostics?.length
84
+ ? ` (${issue.diagnostics.map(({ code, count }) => `${code}:${count}`).join(', ')})`
85
+ : '';
86
+ lines.push(` ✗ ${issue.sessionId}: ${issue.status}${diagnostics}`);
87
+ lines.push(` → ${issue.command}`);
88
+ lines.push(` → ${issue.command} --apply`);
89
+ }
90
+ if (!issues.length) lines.push(' ✓ frontiers, checkpoints e manifests consistentes');
91
+ return lines;
92
+ }
12
93
 
13
94
  export function checkHarness(vaultBase, projectRoot) {
14
95
  const loc = getLocale(vaultBase);
@@ -16,10 +16,20 @@ import {
16
16
  } from './session-stop.mjs';
17
17
  import { buildSessionContent, allocateSessionPath } from './session-start.mjs';
18
18
  import { createLinkedNotes } from './linked-notes.mjs';
19
- import { updateSessionObservability } from './session-observability.mjs';
19
+ import {
20
+ composeSessionObservability,
21
+ publishSessionObservability,
22
+ } from './session-observability.mjs';
20
23
  import { readSessionRegistry, upsertSessionRegistry, removeSessionRegistryEntry, formatLocalIso, formatDate, providerMeta, isBootstrapPrompt } from './obsidian-common.mjs';
21
24
  import { getLocale } from './locale.mjs';
22
25
  import { captureProseDecisions } from './decision-capture.mjs';
26
+ import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
27
+ import {
28
+ parseObservabilityCheckpoint,
29
+ sanitizeObservabilityDiagnostics,
30
+ } from './session-observability-state.mjs';
31
+ import { readObservabilityStore } from './session-observability-store.mjs';
32
+ import { assessObservabilityFreshness } from './session-observability-lifecycle.mjs';
23
33
 
24
34
  // Claude encodes a project's absolute path as its `.claude/projects` dir name by replacing each
25
35
  // path separator and the drive colon with '-'. `C:\GitHub\WendKeep` -> `C--GitHub-WendKeep`.
@@ -97,41 +107,6 @@ function readPrefix(path, bytes = 4096) {
97
107
  }
98
108
  }
99
109
 
100
- // Read the first physical line in full, growing the buffer until a newline is found (capped).
101
- // A fixed prefix truncated any rollout whose session_meta line exceeded the window, silently
102
- // dropping that session from discovery — Codex meta lines can be large (env, git, instructions).
103
- function readFirstLine(path, maxBytes = 4 * 1024 * 1024) {
104
- let fd;
105
- try {
106
- fd = openSync(path, 'r');
107
- const chunk = Buffer.alloc(65536);
108
- let acc = '';
109
- let pos = 0;
110
- while (pos < maxBytes) {
111
- const n = readSync(fd, chunk, 0, chunk.length, pos);
112
- if (n <= 0) break;
113
- acc += chunk.slice(0, n).toString('utf-8');
114
- const nl = acc.indexOf('\n');
115
- if (nl >= 0) return acc.slice(0, nl);
116
- pos += n;
117
- }
118
- return acc; // single-line file, or gave up at the cap
119
- } catch {
120
- return '';
121
- } finally {
122
- if (fd !== undefined) { try { closeSync(fd); } catch { /* already closed */ } }
123
- }
124
- }
125
-
126
- // Pull the session_meta payload (id + cwd). session_meta is line 1 of a rollout.
127
- function readSessionMeta(path) {
128
- const line = readFirstLine(path);
129
- if (!line.trim()) return null;
130
- let e;
131
- try { e = JSON.parse(line); } catch { return null; }
132
- return e.type === 'session_meta' ? (e.payload || {}) : null;
133
- }
134
-
135
110
  // The `session_id` recorded in a note's frontmatter (empty when absent).
136
111
  function noteSessionId(path) {
137
112
  const m = readPrefix(path, 2048).match(/^session_id:\s*["']?([^"'\r\n]+)["']?\s*$/m);
@@ -189,8 +164,9 @@ export function discoverCodexTranscripts(projectPath, fromDir) {
189
164
  if (!dir || !existsSync(dir)) return { dir, transcripts: [] };
190
165
  const transcripts = [];
191
166
  for (const path of walkFiles(dir, /\.jsonl$/i)) {
192
- const meta = readSessionMeta(path);
193
- if (!meta || !meta.id) continue;
167
+ const metaResult = readCodexRolloutMeta(path);
168
+ if (!metaResult.ok || !metaResult.meta.id) continue;
169
+ const meta = metaResult.meta;
194
170
  if (projectPath && !cwdMatchesProject(meta.cwd, projectPath)) continue;
195
171
  // A subagent thread's rollout is a SIBLING file of its parent's — same dir, own id. The
196
172
  // meta says what the file IS; ignoring it turned hierarchy into a duplicate session note.
@@ -248,11 +224,6 @@ export function importSession(vaultBase, txPath, opts = {}) {
248
224
  insertIteration(absPath, block, turn.turnId, tx, vaultBase);
249
225
  }
250
226
 
251
- // Cost + subagent telemetry, exactly like the live Stop hook. Fail-open.
252
- try {
253
- updateSessionObservability({ vaultBase, sessionPath: absPath, transcriptPath: txPath });
254
- } catch { /* observability is best-effort */ }
255
-
256
227
  // Finalize: derived notes + closing section + ended_at from the last turn.
257
228
  const endedAt = formatLocalIso(endDate);
258
229
  const created = mergeCreatedNotes(
@@ -271,6 +242,17 @@ export function importSession(vaultBase, txPath, opts = {}) {
271
242
  imported: true,
272
243
  });
273
244
 
245
+ // Materialize only after the authoritative registry entry exists. This gives import the
246
+ // same causal CAS + runtime manifest as live hooks instead of a note-only legacy snapshot.
247
+ try {
248
+ reconcileImportedObservability({
249
+ vaultBase,
250
+ sessionId,
251
+ sessionPath: absPath,
252
+ transcriptPath: txPath,
253
+ });
254
+ } catch { /* observability is best-effort and reported on a later reconciliation */ }
255
+
274
256
  return { sessionId, relPath, turns: turns.length };
275
257
  }
276
258
 
@@ -333,12 +315,105 @@ export function stampSessionIds(vaultBase) {
333
315
  return report;
334
316
  }
335
317
 
318
+ export function reconcileImportedObservability({
319
+ vaultBase,
320
+ sessionId,
321
+ sessionPath,
322
+ transcriptPath,
323
+ dryRun = false,
324
+ } = {}, dependencies = {}) {
325
+ const readRegistry = dependencies.readRegistry || readSessionRegistry;
326
+ const readStore = dependencies.readStore || readObservabilityStore;
327
+ const compose = dependencies.compose || composeSessionObservability;
328
+ const publish = dependencies.publish || publishSessionObservability;
329
+ if (!vaultBase || !sessionId || !sessionPath || !existsSync(sessionPath)) {
330
+ return {
331
+ status: 'degraded',
332
+ diagnostics: [{ code: 'PARENT_META_INVALID', count: 1 }],
333
+ };
334
+ }
335
+
336
+ let entry;
337
+ let content;
338
+ let runtimeState;
339
+ try {
340
+ entry = readRegistry(vaultBase)?.sessions?.[sessionId];
341
+ content = readFileSync(sessionPath, 'utf8');
342
+ runtimeState = readStore(vaultBase, sessionId);
343
+ } catch {
344
+ return { status: 'degraded', diagnostics: [{ code: 'CACHE_INVALID', count: 1 }] };
345
+ }
346
+ if (!entry) {
347
+ return {
348
+ status: 'degraded',
349
+ diagnostics: [{ code: 'PARENT_META_INVALID', count: 1 }],
350
+ };
351
+ }
352
+ const checkpoint = parseObservabilityCheckpoint(content);
353
+ const assessment = assessObservabilityFreshness({ checkpoint, runtimeState });
354
+ if (assessment.fresh) return { status: 'fresh', diagnostics: [] };
355
+
356
+ let candidate;
357
+ try {
358
+ candidate = compose({
359
+ sessionContent: content,
360
+ sessionEntry: { ...entry, transcript_path: entry.transcript_path || transcriptPath },
361
+ canonicalConversationId: sessionId,
362
+ runtimeState,
363
+ allowNone: true,
364
+ mode: 'offline',
365
+ });
366
+ } catch {
367
+ return {
368
+ status: 'degraded',
369
+ diagnostics: [{ code: 'PARENT_META_INVALID', count: 1 }],
370
+ };
371
+ }
372
+ const diagnostics = sanitizeObservabilityDiagnostics(candidate?.diagnostics || []);
373
+ if (candidate?.state === 'degraded') return { status: 'degraded', diagnostics };
374
+ if (candidate?.state !== 'complete' && candidate?.state !== 'none') {
375
+ return {
376
+ status: 'degraded',
377
+ diagnostics: [{ code: 'PARENT_META_INVALID', count: 1 }],
378
+ };
379
+ }
380
+ if (dryRun) return { status: 'would-reconcile', diagnostics };
381
+
382
+ let outcome;
383
+ try {
384
+ outcome = publish({
385
+ vaultBase,
386
+ sessionPath,
387
+ canonicalConversationId: sessionId,
388
+ candidate,
389
+ caller: 'import-reconcile',
390
+ allowSourceRefresh: true,
391
+ allowDegradedRecovery: true,
392
+ });
393
+ } catch {
394
+ return { status: 'degraded', diagnostics: [{ code: 'CACHE_INVALID', count: 1 }] };
395
+ }
396
+ if (outcome?.status === 'published' || outcome?.status === 'unchanged') {
397
+ return { status: 'published', diagnostics };
398
+ }
399
+ if (outcome?.status === 'stale' || outcome?.status === 'conflict') {
400
+ return {
401
+ status: 'stale',
402
+ diagnostics: [{ code: 'STALE_FRONTIER', count: 1 }],
403
+ };
404
+ }
405
+ return { status: 'degraded', diagnostics };
406
+ }
407
+
336
408
  // Import every not-yet-captured transcript from the requested source(s). Deduped by session_id
337
409
  // against the registry. Options: { projectPath, source ('all'|'claude'|'codex'), from, codexFrom,
338
410
  // since (ISO/date), limit, dryRun }. importSession is provider-agnostic, so both sources share
339
411
  // the same dedup + import loop.
340
412
  export function runImport(vaultBase, opts = {}) {
341
- const { projectPath = process.cwd(), source = 'all', from = '', codexFrom = '', since = '', limit = 0, dryRun = false } = opts;
413
+ const {
414
+ projectPath = process.cwd(), source = 'all', from = '', codexFrom = '', since = '',
415
+ limit = 0, dryRun = false, reconcileObservability = reconcileImportedObservability,
416
+ } = opts;
342
417
  const src = String(source).toLowerCase();
343
418
  const transcripts = [];
344
419
  let claudeDir = '';
@@ -355,7 +430,57 @@ export function runImport(vaultBase, opts = {}) {
355
430
  }
356
431
  const notes = capturedSessionNotes(vaultBase);
357
432
  const sinceMs = since ? Date.parse(since) : 0;
358
- const report = { source: src, claudeDir, codexDir, scanned: transcripts.length, imported: 0, repaired: 0, skipped: 0, subagents: 0, errors: [], sessions: [] };
433
+ const report = {
434
+ source: src, claudeDir, codexDir, scanned: transcripts.length, imported: 0,
435
+ repaired: 0, skipped: 0, subagents: 0, observabilityReconciled: 0,
436
+ observabilityFresh: 0, observabilityDegraded: 0, errors: [], sessions: [],
437
+ };
438
+
439
+ const degradedObservability = () => ({
440
+ status: 'degraded',
441
+ diagnostics: [{ code: 'CACHE_INVALID', count: 1 }],
442
+ });
443
+ const reportableObservabilityStatuses = new Set([
444
+ 'degraded', 'fresh', 'published', 'stale', 'unchanged', 'would-reconcile',
445
+ ]);
446
+ const sanitizeReportedObservability = (result) => {
447
+ try {
448
+ const status = String(result?.status || 'degraded');
449
+ if (!reportableObservabilityStatuses.has(status)) return degradedObservability();
450
+ const projected = (result?.diagnostics || []).map((diagnostic) => ({
451
+ code: diagnostic?.code,
452
+ count: diagnostic?.count,
453
+ }));
454
+ return { status, diagnostics: sanitizeObservabilityDiagnostics(projected) };
455
+ } catch {
456
+ return degradedObservability();
457
+ }
458
+ };
459
+
460
+ const reconcileExistingObservability = (transcript, sessionPath, turns) => {
461
+ if (typeof reconcileObservability !== 'function') return null;
462
+ let result;
463
+ try {
464
+ result = reconcileObservability({
465
+ vaultBase,
466
+ sessionId: transcript.sessionId,
467
+ sessionPath,
468
+ transcriptPath: transcript.path,
469
+ dryRun,
470
+ });
471
+ } catch {
472
+ result = degradedObservability();
473
+ }
474
+ const observability = sanitizeReportedObservability(result);
475
+ if (observability.status === 'fresh' || observability.status === 'unchanged') {
476
+ report.observabilityFresh++;
477
+ } else if (observability.status === 'published' || observability.status === 'would-reconcile') {
478
+ report.observabilityReconciled++;
479
+ } else {
480
+ report.observabilityDegraded++;
481
+ }
482
+ return observability;
483
+ };
359
484
 
360
485
  let done = 0;
361
486
  for (const t of transcripts) {
@@ -390,7 +515,14 @@ export function runImport(vaultBase, opts = {}) {
390
515
  if (existingNote) {
391
516
  const have = noteTurnIds(existingNote);
392
517
  const missing = turns.filter((turn) => !have.has(String(turn.turnId)));
393
- if (!missing.length) { report.skipped++; continue; }
518
+ if (!missing.length) {
519
+ const observability = reconcileExistingObservability(t, existingNote, 0);
520
+ if (observability) report.sessions.push({
521
+ sessionId: t.sessionId, turns: 0, observability,
522
+ });
523
+ report.skipped++;
524
+ continue;
525
+ }
394
526
  if (dryRun) {
395
527
  report.sessions.push({ sessionId: t.sessionId, turns: missing.length, repaired: true, dryRun: true });
396
528
  report.repaired++;
@@ -404,10 +536,13 @@ export function runImport(vaultBase, opts = {}) {
404
536
  turn.turnId, tx, vaultBase,
405
537
  );
406
538
  }
407
- try {
408
- updateSessionObservability({ vaultBase, sessionPath: existingNote, transcriptPath: t.path });
409
- } catch { /* best-effort */ }
410
- report.sessions.push({ sessionId: t.sessionId, turns: missing.length, repaired: true });
539
+ const observability = reconcileExistingObservability(t, existingNote, missing.length);
540
+ report.sessions.push({
541
+ sessionId: t.sessionId,
542
+ turns: missing.length,
543
+ repaired: true,
544
+ ...(observability ? { observability } : {}),
545
+ });
411
546
  report.repaired++;
412
547
  done++;
413
548
  } catch (error) {
@@ -5,14 +5,49 @@ import {
5
5
  inspectTranscriptIdentityContent,
6
6
  resolveSessionIdentitySnapshot,
7
7
  } from '../packages/integrations/src/session-identity.mjs';
8
+ import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
9
+
10
+ function unknownTranscriptIdentity() {
11
+ return {
12
+ transcriptProvider: 'unknown',
13
+ provider: 'unknown',
14
+ canonicalConversationId: '',
15
+ transcriptId: '',
16
+ parentConversationId: '',
17
+ };
18
+ }
19
+
20
+ function inspectCodexMeta(meta, fallbackTranscriptId) {
21
+ return {
22
+ transcriptProvider: 'openai',
23
+ provider: 'codex',
24
+ canonicalConversationId: meta.session_id || meta.id || '',
25
+ transcriptId: meta.id || fallbackTranscriptId,
26
+ parentConversationId: meta.parent_thread_id || meta.forked_from_id || '',
27
+ };
28
+ }
29
+
30
+ function withoutCodexSessionMeta(content) {
31
+ return String(content || '').split('\n').filter((line) => {
32
+ try { return JSON.parse(line)?.type !== 'session_meta'; } catch { return true; }
33
+ }).join('\n');
34
+ }
8
35
 
9
36
  export function inspectTranscriptIdentity(transcriptPath) {
10
- const content = transcriptPath && existsSync(transcriptPath)
11
- ? readFileSync(transcriptPath, 'utf-8')
12
- : '';
13
- return inspectTranscriptIdentityContent(content, {
14
- fallbackTranscriptId: transcriptPath ? basename(transcriptPath, '.jsonl') : '',
37
+ const fallbackTranscriptId = transcriptPath ? basename(transcriptPath, '.jsonl') : '';
38
+ if (!transcriptPath || !existsSync(transcriptPath)) return unknownTranscriptIdentity();
39
+
40
+ const codexMeta = readCodexRolloutMeta(transcriptPath);
41
+ if (codexMeta.ok) return inspectCodexMeta(codexMeta.meta, fallbackTranscriptId);
42
+
43
+ // Claude JSONL has no session_meta header. Preserve its existing full-content inspection,
44
+ // but never reinterpret a later/misplaced Codex meta as the file identity.
45
+ let content;
46
+ try { content = readFileSync(transcriptPath, 'utf-8'); } catch { return unknownTranscriptIdentity(); }
47
+ const inspected = inspectTranscriptIdentityContent(withoutCodexSessionMeta(content), {
48
+ fallbackTranscriptId,
15
49
  });
50
+ return inspected.provider === 'claude' ? inspected : unknownTranscriptIdentity();
16
51
  }
17
52
 
18
53
  export function resolveSessionIdentity(vaultBase, input = {}, provider = detectProvider()) {
@@ -0,0 +1,129 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { statSync } from 'node:fs';
3
+ import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
4
+
5
+ const stableHash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
6
+
7
+ function addTranscriptPath(paths, value) {
8
+ if (typeof value === 'string' && value.trim()) paths.add(value.trim());
9
+ }
10
+
11
+ function collectActivationPaths(paths, activations) {
12
+ if (!activations || typeof activations !== 'object') return;
13
+ const values = Array.isArray(activations) ? activations : Object.values(activations);
14
+ for (const activation of values) {
15
+ if (!activation || typeof activation !== 'object') continue;
16
+ addTranscriptPath(paths, activation.transcript_path);
17
+ for (const path of activation.transcript_paths || []) addTranscriptPath(paths, path);
18
+ }
19
+ }
20
+
21
+ function isCodexDescendant(meta) {
22
+ return Boolean(meta?.source && typeof meta.source === 'object' && meta.source.subagent);
23
+ }
24
+
25
+ // Resolve every transcript explicitly attached to the registry entry. Classification is
26
+ // authoritative only when the rollout's first session_meta line can be read: filename and
27
+ // directory layout are deliberately not used as subagent heuristics.
28
+ export function resolveObservabilityRoots(entry, { readMeta = readCodexRolloutMeta } = {}) {
29
+ if (!entry || typeof entry !== 'object') {
30
+ return {
31
+ state: 'degraded',
32
+ rootPaths: [],
33
+ descendantPaths: [],
34
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
35
+ };
36
+ }
37
+
38
+ const candidates = new Set();
39
+ addTranscriptPath(candidates, entry.transcript_path);
40
+ for (const path of entry.transcript_paths || []) addTranscriptPath(candidates, path);
41
+ collectActivationPaths(candidates, entry.activations);
42
+
43
+ const rootPaths = [];
44
+ const descendantPaths = [];
45
+ let unreadable = 0;
46
+ for (const path of [...candidates].sort((a, b) => a.localeCompare(b))) {
47
+ const result = readMeta(path);
48
+ if (!result?.ok) {
49
+ unreadable += 1;
50
+ continue;
51
+ }
52
+ if (isCodexDescendant(result.meta)) descendantPaths.push(path);
53
+ else rootPaths.push(path);
54
+ }
55
+
56
+ if (unreadable > 0) {
57
+ return {
58
+ state: 'degraded',
59
+ rootPaths,
60
+ descendantPaths,
61
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: unreadable }],
62
+ };
63
+ }
64
+
65
+ return { state: 'complete', rootPaths, descendantPaths, diagnostics: [] };
66
+ }
67
+
68
+ function sameFrontier(left, right) {
69
+ return Boolean(left && right && JSON.stringify(left) === JSON.stringify(right));
70
+ }
71
+
72
+ export function assessObservabilityFreshness({
73
+ checkpoint,
74
+ runtimeState,
75
+ statSource = statSync,
76
+ } = {}) {
77
+ if (!checkpoint) return { fresh: false, status: 'legacy', diagnostics: [] };
78
+ if (checkpoint.state === 'degraded') {
79
+ return {
80
+ fresh: false,
81
+ status: 'degraded',
82
+ diagnostics: checkpoint.diagnostics || [],
83
+ };
84
+ }
85
+ const manifest = runtimeState?.source_manifest;
86
+ if (!Array.isArray(manifest) || manifest.length === 0) {
87
+ return { fresh: false, status: 'manifest-unproven', diagnostics: [] };
88
+ }
89
+
90
+ const hashInput = [];
91
+ for (const source of manifest) {
92
+ if (!source || typeof source.path !== 'string' || typeof source.rolloutId !== 'string') {
93
+ return { fresh: false, status: 'manifest-unproven', diagnostics: [] };
94
+ }
95
+ let stat;
96
+ try { stat = statSource(source.path); } catch {
97
+ return {
98
+ fresh: false,
99
+ status: 'stale',
100
+ diagnostics: [{ code: 'STALE_FRONTIER', count: 1 }],
101
+ };
102
+ }
103
+ if (!stat.isFile() || stat.size !== Number(source.size)
104
+ || stat.mtimeMs !== Number(source.mtimeMs)) {
105
+ return {
106
+ fresh: false,
107
+ status: 'stale',
108
+ diagnostics: [{ code: 'STALE_FRONTIER', count: 1 }],
109
+ };
110
+ }
111
+ hashInput.push({ rolloutId: source.rolloutId, size: stat.size, mtimeMs: stat.mtimeMs });
112
+ }
113
+ hashInput.sort((left, right) =>
114
+ `${left.rolloutId}\u0000${left.size}\u0000${left.mtimeMs}`.localeCompare(
115
+ `${right.rolloutId}\u0000${right.size}\u0000${right.mtimeMs}`,
116
+ ));
117
+ const stale = stableHash(hashInput) !== checkpoint.frontier.source_manifest_hash
118
+ || !sameFrontier(runtimeState?.checkpoint_frontier, checkpoint.frontier)
119
+ || runtimeState?.observability_dirty !== false
120
+ || runtimeState?.observability_checkpoint_sequence !== runtimeState?.observability_signal_sequence
121
+ || runtimeState?.observability_checkpoint_sequence !== checkpoint.frontier.signal_sequence;
122
+ return stale
123
+ ? {
124
+ fresh: false,
125
+ status: 'stale',
126
+ diagnostics: [{ code: 'STALE_FRONTIER', count: 1 }],
127
+ }
128
+ : { fresh: true, status: 'fresh', diagnostics: [] };
129
+ }