wendkeep 0.66.4 → 0.67.0

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 (40) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.en.md +78 -5
  3. package/README.md +78 -5
  4. package/docs/en/commands/costs-and-observability.md +21 -7
  5. package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
  6. package/docs/en/commands/operating-profiles.md +65 -10
  7. package/docs/en/commands/sessions-and-import.md +22 -1
  8. package/docs/en/commands/verify.md +5 -3
  9. package/docs/pt-BR/commands/costs-and-observability.md +21 -7
  10. package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
  11. package/docs/pt-BR/commands/operating-profiles.md +66 -11
  12. package/docs/pt-BR/commands/sessions-and-import.md +20 -0
  13. package/docs/pt-BR/commands/verify.md +6 -3
  14. package/hooks/change-nag.mjs +8 -0
  15. package/hooks/codex-rollout-meta.mjs +112 -0
  16. package/hooks/codex-subagent-graph.mjs +903 -0
  17. package/hooks/harness-doctor.mjs +82 -1
  18. package/hooks/import-sessions.mjs +185 -50
  19. package/hooks/operating-profile-runtime.mjs +36 -2
  20. package/hooks/operating-profile-task-store.mjs +77 -0
  21. package/hooks/session-identity.mjs +40 -5
  22. package/hooks/session-observability-lifecycle.mjs +129 -0
  23. package/hooks/session-observability-state.mjs +241 -0
  24. package/hooks/session-observability-store.mjs +436 -0
  25. package/hooks/session-observability.mjs +647 -21
  26. package/hooks/session-stop.mjs +339 -11
  27. package/hooks/subagent-stop.mjs +266 -12
  28. package/hooks/subagent-usage.mjs +65 -0
  29. package/hooks/token-usage.mjs +81 -4
  30. package/package.json +3 -3
  31. package/packages/harness/src/operating-profile.mjs +127 -0
  32. package/packages/harness/src/sensors-core.mjs +41 -1
  33. package/packages/integrations/src/prompt-content.mjs +123 -0
  34. package/packages/integrations/src/transcripts.mjs +16 -10
  35. package/src/cost.mjs +40 -6
  36. package/src/doctor.mjs +4 -1
  37. package/src/profile.mjs +95 -17
  38. package/src/rebuild-costs.mjs +220 -34
  39. package/src/skills-seed.mjs +38 -2
  40. package/src/sync-defs.mjs +6 -1
@@ -1,9 +1,20 @@
1
1
  // Single atomic writer for session usage, models, reasoning/effort and subagents.
2
- import { existsSync } from 'node:fs';
3
- import { collectSessionUsage } from './token-usage.mjs';
4
- import { collectSubagentUsage, collectCodexSubagentUsage, sessionDirFromTranscript } from './subagent-usage.mjs';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { collectSessionUsage, collectSessionUsageForRoots } from './token-usage.mjs';
4
+ import { collectClaudeSubagentUsageState, collectCodexSubagentUsage, sessionDirFromTranscript } from './subagent-usage.mjs';
5
5
  import { inspectTranscriptIdentity } from './session-identity.mjs';
6
6
  import { hasSessionFrontmatter, mutateSessionNote } from './session-note-io.mjs';
7
+ import { composeCodexSubagentGraph } from './codex-subagent-graph.mjs';
8
+ import { resolveObservabilityRoots } from './session-observability-lifecycle.mjs';
9
+ import { markObservabilityCheckpoint, readObservabilityStore } from './session-observability-store.mjs';
10
+ import { mutateSessionRegistry } from './obsidian-common.mjs';
11
+ import {
12
+ compareObservabilityFrontiers,
13
+ normalizeObservabilityFrontier,
14
+ parseObservabilityCheckpoint,
15
+ renderObservabilityCheckpoint,
16
+ sanitizeObservabilityDiagnostics,
17
+ } from './session-observability-state.mjs';
7
18
 
8
19
  const HEADING = '## Agentes, tokens e custos';
9
20
  const LEGACY_HEADINGS = ['## Uso de tokens e custos', '## Subagents & Workflows'];
@@ -49,13 +60,14 @@ export function upsertObservabilitySection(content, section) {
49
60
  }
50
61
 
51
62
  function mainLedger(main) {
52
- return (main.summary.modelRows || []).map((row) => ({
63
+ const summaries = main.summary ? [main.summary] : (main.summaries || []);
64
+ return summaries.flatMap((summary) => (summary.modelRows || []).map((row) => ({
53
65
  provider: row.provider || 'unknown', model: row.model || 'unknown', source: 'main',
54
- effort: effort(main.summary.pensamento), calls: row.calls || 0,
66
+ effort: effort(summary.pensamento), calls: row.calls || 0,
55
67
  input: row.usage.input || 0, cacheWrite: row.usage.cacheWrite || 0, cached: row.usage.cached || 0,
56
68
  output: row.usage.output || 0, reasoning: row.usage.reasoning || 0, total: usageTotal(row.usage),
57
69
  cost: round4(row.costs?.model || 0),
58
- }));
70
+ })));
59
71
  }
60
72
 
61
73
  function subagentLedger(collected) {
@@ -85,7 +97,9 @@ function renderHistory(entries) {
85
97
  }
86
98
 
87
99
  function renderSubagents(collected) {
88
- if (!collected) return '### Subagents e workflows\n\nNenhum subagent registrado.';
100
+ if (!collected || collected.state === 'none') {
101
+ return '### Subagents e workflows\n\nNenhum subagent registrado.';
102
+ }
89
103
  const a = collected.aggregate;
90
104
  const workflows = collected.workflows.length
91
105
  ? collected.workflows.map((w) => `${w.name} (${w.runId}${w.status ? ` · ${w.status}` : ''} · ${w.agents} agentes · ${usd(w.cost)})`).join('; ')
@@ -135,16 +149,206 @@ ${renderHistory(main.entries)}
135
149
  ${renderSubagents(subagents)}`;
136
150
  }
137
151
 
138
- export function buildSessionObservability({ sessionContent, transcriptPath }) {
139
- const main = collectSessionUsage({ sessionContent, transcriptPath });
140
- if (!main) return null;
141
- // Claude layout first (<transcript>/subagents/); when absent, the Codex layout — sibling
142
- // rollouts linked by parent_thread_id. The Codex collector self-gates: a Claude transcript
143
- // has no session_meta line, so it returns null and this stays a strict fallback.
144
- const subagents = collectSubagentUsage(sessionDirFromTranscript(transcriptPath))
145
- || collectCodexSubagentUsage(transcriptPath);
152
+ function quoteFrontmatter(value) {
153
+ if (typeof value === 'number') return String(value);
154
+ return `'${String(value).replaceAll("'", "''")}'`;
155
+ }
156
+
157
+ function applyCheckpoint(content, frontier, state, diagnostics) {
158
+ let next = content;
159
+ for (const [key, value] of Object.entries(renderObservabilityCheckpoint(frontier, { state, diagnostics }))) {
160
+ next = setFrontmatterField(next, key, quoteFrontmatter(value));
161
+ }
162
+ return next;
163
+ }
164
+
165
+ function rootIds(roots = {}) {
166
+ return new Set((roots.rootPaths || []).map((value) => String(value || '')
167
+ .split(/[\\/]/).pop().replace(/\.jsonl?$/i, '').toLowerCase()).filter(Boolean));
168
+ }
169
+
170
+ function subagentIds(collected) {
171
+ return new Set((collected?.subagents || []).map((entry) => String(entry?.id || entry?.rollout_id || '')
172
+ .trim().toLowerCase()).filter(Boolean));
173
+ }
174
+
175
+ function combineDiagnostics(...groups) {
176
+ return sanitizeObservabilityDiagnostics(groups.flatMap((group) => group || []));
177
+ }
178
+
179
+ function zeroSubagentAggregate() {
180
+ return {
181
+ count: 0, calls: 0, tokens: 0, cost: 0, wasted: 0, tools: [],
182
+ usage: { input: 0, cached: 0, cacheWrite: 0, output: 0, reasoning: 0 },
183
+ modelRows: [],
184
+ };
185
+ }
186
+
187
+ function legacySubagentScan(transcriptPath) {
188
+ const claude = collectClaudeSubagentUsageState(sessionDirFromTranscript(transcriptPath));
189
+ if (claude.state === 'complete' || claude.state === 'degraded') return claude;
190
+ const codex = collectCodexSubagentUsage(transcriptPath);
191
+ return codex
192
+ ? { ...codex, state: 'complete', diagnostics: [] }
193
+ : { state: 'none', diagnostics: [], aggregate: zeroSubagentAggregate(), subagents: [], workflows: [] };
194
+ }
195
+
196
+ function graphSubagentScan({
197
+ rootPaths,
198
+ frontier,
199
+ signals,
200
+ cache,
201
+ mode,
202
+ limits,
203
+ deadlineAt,
204
+ now,
205
+ }) {
206
+ if (!rootPaths?.length) return legacySubagentScan('');
207
+ return composeCodexSubagentGraph({
208
+ rootPaths,
209
+ canonicalSessionId: frontier?.canonical_session_id || '',
210
+ signals,
211
+ cache,
212
+ mode,
213
+ limits,
214
+ deadlineAt,
215
+ now,
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Pure, lock-free composition. Callers may inject the graph/main collectors so the same
221
+ * merge logic is shared by live hooks, import and rebuild without coupling their scanners.
222
+ */
223
+ export function composeSessionObservability({
224
+ sessionContent,
225
+ frontier: frontierInput,
226
+ roots = {},
227
+ transcriptPath = '',
228
+ sessionEntry,
229
+ canonicalConversationId = '',
230
+ runtimeState,
231
+ mainResult,
232
+ subagentsResult,
233
+ previousSnapshot = null,
234
+ allowNone = true,
235
+ signals = [],
236
+ cache = null,
237
+ mode = 'live',
238
+ limits = {},
239
+ deadlineAt = Number.POSITIVE_INFINITY,
240
+ now,
241
+ } = {}, {
242
+ collectMain = ({ sessionContent: content, rootPaths, descendantIds, transcriptPath: scanTranscriptPath }) => (
243
+ rootPaths?.length
244
+ ? collectSessionUsageForRoots({ sessionContent: content, rootPaths, descendantIds })
245
+ : collectSessionUsage({ sessionContent: content, transcriptPath: scanTranscriptPath })
246
+ ),
247
+ collectSubagents = ({ rootPaths, transcriptPath: scanTranscriptPath, ...input }) => (rootPaths?.length
248
+ ? graphSubagentScan({ rootPaths, ...input })
249
+ : legacySubagentScan(scanTranscriptPath)),
250
+ } = {}) {
251
+ if (sessionEntry) {
252
+ return composeRegisteredSessionObservability({
253
+ sessionContent,
254
+ sessionEntry,
255
+ canonicalConversationId,
256
+ runtimeState,
257
+ frontier: frontierInput,
258
+ allowNone,
259
+ mode,
260
+ limits,
261
+ deadlineAt,
262
+ now,
263
+ }, { collectMain, collectSubagents });
264
+ }
265
+ const original = String(sessionContent || '');
266
+ if (!hasSessionFrontmatter(original)) {
267
+ const frontier = normalizeObservabilityFrontier(frontierInput);
268
+ return {
269
+ state: 'degraded', frontier,
270
+ diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
271
+ snapshot: previousSnapshot, content: original,
272
+ };
273
+ }
274
+
275
+ const collectedSubagents = subagentsResult ?? collectSubagents({
276
+ roots,
277
+ rootPaths: roots.rootPaths || [],
278
+ descendantIds: roots.descendantIds || [],
279
+ frontier: frontierInput,
280
+ signals,
281
+ cache,
282
+ mode,
283
+ limits,
284
+ deadlineAt,
285
+ now,
286
+ transcriptPath,
287
+ });
288
+ const frontier = normalizeObservabilityFrontier({
289
+ ...frontierInput,
290
+ ...(collectedSubagents?.frontier?.rootsStatHash
291
+ ? { roots_stat_hash: collectedSubagents.frontier.rootsStatHash }
292
+ : {}),
293
+ ...(collectedSubagents?.frontier?.graphCursor
294
+ ? { graph_cursor: collectedSubagents.frontier.graphCursor }
295
+ : {}),
296
+ ...(collectedSubagents?.frontier?.sourceManifestHash
297
+ ? { source_manifest_hash: collectedSubagents.frontier.sourceManifestHash }
298
+ : {}),
299
+ });
300
+ const descendantIds = collectedSubagents?.descendantIds || roots.descendantIds || [];
301
+ const preliminarySubagentState = collectedSubagents?.state || (collectedSubagents ? 'complete' : 'degraded');
302
+ const preliminaryState = preliminarySubagentState === 'none' && allowNone
303
+ ? 'none'
304
+ : (preliminarySubagentState === 'complete' ? 'complete' : 'degraded');
305
+ const preliminaryDiagnostics = combineDiagnostics(
306
+ collectedSubagents?.diagnostics,
307
+ !collectedSubagents ? [{ code: 'SOURCE_CHANGED_DURING_SCAN', count: 1 }] : [],
308
+ preliminarySubagentState === 'none' && !allowNone ? [{ code: 'STALE_FRONTIER', count: 1 }] : [],
309
+ );
310
+ // Seed the checkpoint before the main frontmatter collector canonicalizes managed fields.
311
+ // This makes the first materialization use the same ordering as every subsequent replay.
312
+ const mainInputContent = preliminaryState === 'degraded'
313
+ ? original
314
+ : applyCheckpoint(original, frontier, preliminaryState, preliminaryDiagnostics);
315
+ const main = mainResult ?? collectMain({
316
+ sessionContent: mainInputContent,
317
+ rootPaths: roots.rootPaths || [],
318
+ descendantIds,
319
+ roots,
320
+ frontier,
321
+ transcriptPath,
322
+ });
323
+
324
+ const mainState = main?.state || (main ? 'complete' : 'degraded');
325
+ const subagentsState = preliminarySubagentState;
326
+ const diagnostics = combineDiagnostics(
327
+ main?.diagnostics,
328
+ collectedSubagents?.diagnostics,
329
+ !main ? [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }] : [],
330
+ !collectedSubagents ? [{ code: 'SOURCE_CHANGED_DURING_SCAN', count: 1 }] : [],
331
+ subagentsState === 'none' && !allowNone ? [{ code: 'STALE_FRONTIER', count: 1 }] : [],
332
+ );
333
+ const overlap = [...rootIds(roots)].filter((id) => subagentIds(collectedSubagents).has(id));
334
+ const safeDiagnostics = overlap.length
335
+ ? combineDiagnostics(diagnostics, [{ code: 'ROOT_MISMATCH', count: overlap.length }])
336
+ : diagnostics;
337
+
338
+ if (mainState === 'degraded' || subagentsState === 'degraded'
339
+ || (subagentsState === 'none' && !allowNone) || overlap.length) {
340
+ return {
341
+ state: 'degraded', frontier, diagnostics: safeDiagnostics,
342
+ snapshot: previousSnapshot, content: original,
343
+ };
344
+ }
345
+
346
+ const state = subagentsState === 'none' ? 'none' : 'complete';
347
+ // A proven empty graph still carries its source manifest/cache. Dropping that proof made
348
+ // the next import/doctor run classify a genuinely fresh `none` snapshot as unprovable.
349
+ const subagents = collectedSubagents;
146
350
  const ledger = [...mainLedger(main), ...subagentLedger(subagents)];
147
- const sub = subagents?.aggregate || { count: 0, tokens: 0, cost: 0, wasted: 0, tools: [] };
351
+ const sub = subagents?.aggregate || zeroSubagentAggregate();
148
352
  let content = main.content;
149
353
  content = setFrontmatterField(content, 'subagents_count', sub.count || 0);
150
354
  content = setFrontmatterField(content, 'subagents_tokens_total', sub.tokens || 0);
@@ -153,10 +357,425 @@ export function buildSessionObservability({ sessionContent, transcriptPath }) {
153
357
  content = setFrontmatterField(content, 'subagents_wasted_usd', sub.wasted || 0);
154
358
  content = setFrontmatterField(content, 'tokens_total_incl_subagents', main.aggregate.total + (sub.tokens || 0));
155
359
  content = setFrontmatterField(content, 'custo_total_incl_subagents_usd', round4(main.aggregate.custo + (sub.cost || 0)));
156
- content = setFrontmatterField(content, 'observability_schema', 1);
157
360
  content = setFrontmatterField(content, 'custo_por_modelo_json', `'${JSON.stringify(ledger).replaceAll("'", "''")}'`);
158
- const snapshot = { version: 1, main, subagents, ledger };
159
- return { snapshot, content: upsertObservabilitySection(content, renderSessionObservability(snapshot)) };
361
+ content = applyCheckpoint(content, frontier, state, safeDiagnostics);
362
+ const snapshot = {
363
+ version: 2, state, frontier, diagnostics: safeDiagnostics,
364
+ main, subagents, ledger,
365
+ roots: {
366
+ rootPaths: [...(roots.rootPaths || [])].sort(),
367
+ descendantIds: [...descendantIds].sort(),
368
+ },
369
+ };
370
+ return {
371
+ state, frontier, diagnostics: safeDiagnostics, snapshot,
372
+ content: upsertObservabilitySection(content, renderSessionObservability(snapshot)),
373
+ };
374
+ }
375
+
376
+ function causalFieldsFromEntry(entry = {}, runtimeState = {}, canonicalConversationId = '') {
377
+ const activationId = String(entry.active_activation_id || entry.activation_id || 'offline');
378
+ const activation = entry.activations?.[activationId] || {};
379
+ return {
380
+ canonical_session_id: canonicalConversationId || entry.canonical_session_id || entry.session_id || 'offline-session',
381
+ activation_id: activationId,
382
+ activation_epoch: Number(activation.epoch ?? entry.activation_epoch ?? 0) || 0,
383
+ turn_sequence: Number(entry.last_turn_sequence ?? activation.last_turn_sequence ?? 0) || 0,
384
+ signal_sequence: Number(runtimeState.observability_signal_sequence ?? entry.observability_signal_sequence ?? 0) || 0,
385
+ roots_stat_hash: runtimeState.checkpoint_frontier?.roots_stat_hash || 'pending-roots',
386
+ graph_cursor: runtimeState.checkpoint_frontier?.graph_cursor || 'pending-graph',
387
+ source_manifest_hash: runtimeState.checkpoint_frontier?.source_manifest_hash || 'pending-manifest',
388
+ };
389
+ }
390
+
391
+ /** Pure registered-session composition used by import/rebuild previews. No writer is reachable. */
392
+ export function composeRegisteredSessionObservability({
393
+ sessionContent,
394
+ sessionEntry = {},
395
+ canonicalConversationId = '',
396
+ runtimeState = {},
397
+ frontier: frontierInput,
398
+ allowNone = true,
399
+ mode = 'offline',
400
+ limits = {},
401
+ deadlineAt = Number.POSITIVE_INFINITY,
402
+ now,
403
+ } = {}, dependencies = {}) {
404
+ const provider = String(sessionEntry.provider || '').trim().toLowerCase();
405
+ const transcriptPath = sessionEntry.transcript_path
406
+ || sessionEntry.transcript_paths?.[0]
407
+ || '';
408
+ const resolution = provider === 'claude'
409
+ ? { state: 'complete', rootPaths: [], descendantPaths: [], diagnostics: [] }
410
+ : resolveObservabilityRoots(sessionEntry);
411
+ const roots = {
412
+ rootPaths: resolution.rootPaths || [],
413
+ descendantPaths: resolution.descendantPaths || [],
414
+ descendantIds: sessionEntry.descendant_ids || [],
415
+ };
416
+ const missingCodexRoot = provider !== 'claude' && roots.rootPaths.length === 0;
417
+ const subagentsResult = resolution.state === 'degraded' || missingCodexRoot
418
+ ? {
419
+ state: 'degraded',
420
+ diagnostics: resolution.diagnostics?.length
421
+ ? resolution.diagnostics
422
+ : [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
423
+ }
424
+ : undefined;
425
+ return composeSessionObservability({
426
+ sessionContent,
427
+ transcriptPath,
428
+ frontier: {
429
+ ...causalFieldsFromEntry(sessionEntry, runtimeState, canonicalConversationId),
430
+ ...(frontierInput || {}),
431
+ },
432
+ roots,
433
+ subagentsResult,
434
+ allowNone,
435
+ signals: runtimeState.signals || [],
436
+ cache: runtimeState.graph_cache || null,
437
+ mode,
438
+ limits,
439
+ deadlineAt,
440
+ now,
441
+ }, dependencies);
442
+ }
443
+
444
+ function candidateRelation(current, candidate) {
445
+ if (!current) return 'newer';
446
+ const frontier = current.frontier || current;
447
+ return compareObservabilityFrontiers(frontier, candidate);
448
+ }
449
+
450
+ function sameSortedStrings(left = [], right = []) {
451
+ const a = [...new Set(left.filter(Boolean))].sort();
452
+ const b = [...new Set(right.filter(Boolean))].sort();
453
+ return a.length === b.length && a.every((value, index) => value === b[index]);
454
+ }
455
+
456
+ const CAUSAL_FRONTIER_FIELDS = [
457
+ 'canonical_session_id',
458
+ 'activation_id',
459
+ 'activation_epoch',
460
+ 'turn_sequence',
461
+ 'signal_sequence',
462
+ ];
463
+
464
+ function sameCausalAuthority(leftInput, rightInput) {
465
+ const left = leftInput?.frontier || leftInput;
466
+ const right = rightInput?.frontier || rightInput;
467
+ return Boolean(left && right
468
+ && CAUSAL_FRONTIER_FIELDS.every((field) => left[field] === right[field]));
469
+ }
470
+
471
+ function candidateManifestIsCurrent(snapshot) {
472
+ const manifest = snapshot?.subagents?.sourceManifest;
473
+ if (!Array.isArray(manifest) || manifest.length === 0) return false;
474
+ return manifest.every((source) => {
475
+ try {
476
+ const stat = statSync(source.path);
477
+ return stat.isFile() && stat.size === Number(source.size)
478
+ && stat.mtimeMs === Number(source.mtimeMs);
479
+ } catch {
480
+ return false;
481
+ }
482
+ });
483
+ }
484
+
485
+ function registeredRootsAreCurrent(context, snapshot) {
486
+ const candidateRoots = snapshot?.roots?.rootPaths || [];
487
+ if (!context?.entry || !candidateRoots.length) return false;
488
+ const resolution = resolveObservabilityRoots(context.entry);
489
+ return resolution.state === 'complete'
490
+ && sameSortedStrings(resolution.rootPaths, candidateRoots);
491
+ }
492
+
493
+ function canRefreshSourceFrontier(current, candidate, context, snapshot, enabled) {
494
+ return Boolean(enabled
495
+ && sameCausalAuthority(current, candidate)
496
+ && registeredRootsAreCurrent(context, snapshot)
497
+ && candidateManifestIsCurrent(snapshot));
498
+ }
499
+
500
+ function currentFrontierUnderGuard(candidate, context, snapshot) {
501
+ if (!context) return candidate;
502
+ const entry = context.entry;
503
+ if (!entry) return { ...candidate, canonical_session_id: 'registry-session-missing' };
504
+ const activationId = String(entry.active_activation_id || entry.activation_id || candidate.activation_id);
505
+ const activation = entry.activations?.[activationId] || {};
506
+ const current = {
507
+ ...candidate,
508
+ activation_id: activationId,
509
+ activation_epoch: Number(activation.epoch ?? entry.activation_epoch ?? candidate.activation_epoch) || 0,
510
+ turn_sequence: Number(entry.last_turn_sequence ?? activation.last_turn_sequence ?? candidate.turn_sequence) || 0,
511
+ signal_sequence: Number(
512
+ context.runtimeState?.observability_signal_sequence
513
+ ?? entry.observability_signal_sequence
514
+ ?? candidate.signal_sequence,
515
+ ) || 0,
516
+ };
517
+ const candidateRoots = snapshot?.roots?.rootPaths || [];
518
+ if (candidateRoots.length && String(entry.provider || '').toLowerCase() !== 'claude') {
519
+ const resolution = resolveObservabilityRoots(entry);
520
+ if (resolution.state !== 'complete'
521
+ || !sameSortedStrings(resolution.rootPaths, candidateRoots)) {
522
+ current.roots_stat_hash = `${candidate.roots_stat_hash}-registry-changed`;
523
+ }
524
+ }
525
+ return current;
526
+ }
527
+
528
+ function updateGuardedRegistryCheckpoint(context, sessionId, checkpoint) {
529
+ const registry = context?.registry;
530
+ const current = registry?.sessions?.[sessionId];
531
+ if (!current) return;
532
+ const frontier = checkpoint.frontier;
533
+ const signalSequence = Math.max(
534
+ Number(current.observability_signal_sequence || 0),
535
+ frontier.signal_sequence,
536
+ );
537
+ registry.sessions[sessionId] = {
538
+ ...current,
539
+ observability_schema: 2,
540
+ subagents_observability_state: checkpoint.state,
541
+ observability_signal_sequence: signalSequence,
542
+ observability_checkpoint_sequence: frontier.signal_sequence,
543
+ observability_dirty: signalSequence > frontier.signal_sequence,
544
+ observability_checkpoint_frontier: frontier,
545
+ subagents_diagnostics: checkpoint.diagnostics || [],
546
+ };
547
+ }
548
+
549
+ /**
550
+ * Causal publisher. Composition is deliberately completed before the note writer is entered;
551
+ * the writer only validates the latest checkpoint and swaps complete bytes.
552
+ */
553
+ export function publishSessionObservability({
554
+ sessionPath,
555
+ frontier: frontierInput,
556
+ candidate: providedCandidate,
557
+ compose,
558
+ composeInput,
559
+ readRuntimeFrontier,
560
+ writeRegistryCheckpoint,
561
+ withPublicationGuard,
562
+ canonicalConversationId = '',
563
+ vaultBase = '',
564
+ allowSourceRefresh = false,
565
+ allowDegradedRecovery = false,
566
+ lockTimeoutMs,
567
+ } = {}, {
568
+ mutateNote = mutateSessionNote,
569
+ composeObservability = composeSessionObservability,
570
+ } = {}) {
571
+ const candidate = providedCandidate
572
+ ?? (compose ? compose() : composeObservability(composeInput));
573
+ if (!candidate) return { status: 'missing', state: 'degraded', candidate: null };
574
+ if (candidate.state === 'degraded') {
575
+ return { status: 'degraded', state: 'degraded', candidate };
576
+ }
577
+
578
+ const frontier = normalizeObservabilityFrontier(candidate.frontier || frontierInput);
579
+ const sessionId = canonicalConversationId || frontier.canonical_session_id;
580
+ const guardedByDefault = Boolean(vaultBase && sessionId);
581
+ const effectiveGuard = withPublicationGuard || (guardedByDefault
582
+ ? (_candidateFrontier, publishGuarded) => mutateSessionRegistry(vaultBase, (registry) => {
583
+ const entry = registry.sessions?.[sessionId] || null;
584
+ const runtimeState = readObservabilityStore(vaultBase, sessionId);
585
+ return publishGuarded({ registry, entry, runtimeState });
586
+ }, { ...(lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {}) })
587
+ : null);
588
+ const effectiveReadFrontier = readRuntimeFrontier
589
+ || (guardedByDefault
590
+ ? (candidateFrontier, context) => currentFrontierUnderGuard(
591
+ candidateFrontier,
592
+ context,
593
+ candidate.snapshot,
594
+ )
595
+ : null);
596
+ const effectiveCheckpointWriter = writeRegistryCheckpoint
597
+ || (guardedByDefault
598
+ ? (checkpoint, context) => {
599
+ updateGuardedRegistryCheckpoint(context, sessionId, checkpoint);
600
+ return markObservabilityCheckpoint(vaultBase, sessionId, {
601
+ checkpointSequence: checkpoint.frontier.signal_sequence,
602
+ frontier: checkpoint.frontier,
603
+ sourceManifest: checkpoint.snapshot?.subagents?.sourceManifest,
604
+ graphCache: checkpoint.snapshot?.subagents?.cache,
605
+ diagnostics: checkpoint.diagnostics,
606
+ });
607
+ }
608
+ : null);
609
+ const publishGuarded = (guardContext) => {
610
+ const live = effectiveReadFrontier?.(frontier, guardContext);
611
+ const liveRelation = candidateRelation(live, frontier);
612
+ if ((liveRelation === 'stale' || liveRelation === 'conflict')
613
+ && !canRefreshSourceFrontier(
614
+ live,
615
+ frontier,
616
+ guardContext,
617
+ candidate.snapshot,
618
+ allowSourceRefresh,
619
+ )) {
620
+ return { status: liveRelation, state: candidate.state, candidate };
621
+ }
622
+
623
+ let rejection = '';
624
+ const outcome = mutateNote(sessionPath, (original) => {
625
+ if (!hasSessionFrontmatter(original)) {
626
+ rejection = 'degraded';
627
+ return null;
628
+ }
629
+ const checkpoint = parseObservabilityCheckpoint(original);
630
+ const relation = candidateRelation(checkpoint, frontier);
631
+ if ((relation === 'stale' || relation === 'conflict')
632
+ && !canRefreshSourceFrontier(
633
+ checkpoint,
634
+ frontier,
635
+ guardContext,
636
+ candidate.snapshot,
637
+ allowSourceRefresh,
638
+ )) {
639
+ rejection = relation;
640
+ return null;
641
+ }
642
+ const recoveringDegraded = allowDegradedRecovery
643
+ && checkpoint?.state === 'degraded'
644
+ && candidate.state !== 'degraded';
645
+ if (relation === 'same' && original !== candidate.content && !recoveringDegraded) {
646
+ rejection = 'conflict';
647
+ return null;
648
+ }
649
+ return candidate.content;
650
+ }, { ...(lockTimeoutMs ? { timeoutMs: lockTimeoutMs } : {}), vaultBase });
651
+
652
+ if (rejection) return { status: rejection, state: candidate.state, candidate, outcome };
653
+ if (!outcome?.written && outcome?.reason !== 'unchanged') {
654
+ return { status: outcome?.reason || 'missing', state: candidate.state, candidate, outcome };
655
+ }
656
+
657
+ // A crash here leaves a fully checkpointed note. Retrying observes the same bytes and
658
+ // executes only this registry reconciliation, so publication remains idempotent.
659
+ effectiveCheckpointWriter?.({
660
+ frontier,
661
+ state: candidate.state,
662
+ diagnostics: candidate.diagnostics || [],
663
+ snapshot: candidate.snapshot,
664
+ }, guardContext);
665
+ return {
666
+ status: outcome.written ? 'published' : 'unchanged',
667
+ state: candidate.state,
668
+ candidate,
669
+ snapshot: candidate.snapshot,
670
+ outcome,
671
+ };
672
+ };
673
+ return effectiveGuard
674
+ ? effectiveGuard(frontier, publishGuarded)
675
+ : publishGuarded(undefined);
676
+ }
677
+
678
+ /**
679
+ * Hook-friendly facade: resolve registered roots, scan/compose outside the note lock and
680
+ * causally publish the resulting candidate. Live hooks pass allowNone=false for an isolated
681
+ * SubagentStop signal and true only for a causally eligible SessionStop.
682
+ */
683
+ export function materializeSessionObservability({
684
+ vaultBase = '',
685
+ sessionPath,
686
+ entry = {},
687
+ transcriptPath = entry.transcript_path || '',
688
+ frontier: frontierInput,
689
+ canonicalConversationId = '',
690
+ allowNone = true,
691
+ signals = [],
692
+ cache = null,
693
+ mode = 'live',
694
+ limits = {},
695
+ deadlineAt = Number.POSITIVE_INFINITY,
696
+ now,
697
+ lockTimeoutMs,
698
+ readRuntimeFrontier,
699
+ writeRegistryCheckpoint,
700
+ withPublicationGuard,
701
+ } = {}, {
702
+ readSessionContent = (path) => readFileSync(path, 'utf8'),
703
+ resolveRoots = resolveObservabilityRoots,
704
+ mutateNote = mutateSessionNote,
705
+ composeObservability = composeSessionObservability,
706
+ } = {}) {
707
+ if (!sessionPath || !existsSync(sessionPath)) {
708
+ return { status: 'missing', state: 'degraded', candidate: null };
709
+ }
710
+ const sessionContent = readSessionContent(sessionPath);
711
+ const identity = inspectTranscriptIdentity(transcriptPath);
712
+ const provider = String(entry.provider || '').trim().toLowerCase();
713
+ const rootResolution = provider === 'claude'
714
+ ? { state: 'complete', rootPaths: [], descendantPaths: [], diagnostics: [] }
715
+ : resolveRoots(entry);
716
+ const compatibility = compatibilityFrontier(
717
+ transcriptPath,
718
+ identity,
719
+ canonicalConversationId || entry.canonical_session_id || entry.session_id || '',
720
+ );
721
+ const frontier = { ...compatibility, ...(frontierInput || {}) };
722
+ const roots = {
723
+ rootPaths: rootResolution.rootPaths || [],
724
+ descendantPaths: rootResolution.descendantPaths || [],
725
+ descendantIds: entry.descendant_ids || [],
726
+ };
727
+ const subagentsResult = rootResolution.state === 'degraded'
728
+ ? { state: 'degraded', diagnostics: rootResolution.diagnostics || [] }
729
+ : undefined;
730
+ const candidate = composeObservability({
731
+ sessionContent,
732
+ frontier,
733
+ roots,
734
+ transcriptPath,
735
+ subagentsResult,
736
+ allowNone,
737
+ signals,
738
+ cache,
739
+ mode,
740
+ limits,
741
+ deadlineAt,
742
+ now,
743
+ });
744
+ return publishSessionObservability({
745
+ sessionPath,
746
+ candidate,
747
+ canonicalConversationId: candidate.frontier?.canonical_session_id || frontier.canonical_session_id,
748
+ readRuntimeFrontier,
749
+ writeRegistryCheckpoint,
750
+ withPublicationGuard,
751
+ vaultBase,
752
+ lockTimeoutMs,
753
+ }, { mutateNote, composeObservability });
754
+ }
755
+
756
+ function compatibilityFrontier(transcriptPath, identity, canonicalConversationId = '') {
757
+ const transcriptId = identity.transcriptId || String(transcriptPath || '').split(/[\\/]/).pop().replace(/\.jsonl?$/i, '') || 'legacy';
758
+ return normalizeObservabilityFrontier({
759
+ canonical_session_id: canonicalConversationId || identity.canonicalConversationId || transcriptId,
760
+ activation_id: 'legacy',
761
+ activation_epoch: 0,
762
+ turn_sequence: 0,
763
+ signal_sequence: 0,
764
+ roots_stat_hash: `legacy-${transcriptId}`,
765
+ graph_cursor: 'legacy',
766
+ source_manifest_hash: `legacy-${transcriptId}`,
767
+ });
768
+ }
769
+
770
+ export function buildSessionObservability({ sessionContent, transcriptPath, frontier, canonicalConversationId = '', allowNone = true }) {
771
+ const identity = inspectTranscriptIdentity(transcriptPath);
772
+ const result = composeSessionObservability({
773
+ sessionContent,
774
+ transcriptPath,
775
+ frontier: frontier || compatibilityFrontier(transcriptPath, identity, canonicalConversationId),
776
+ allowNone,
777
+ });
778
+ return result.state === 'degraded' ? null : result;
160
779
  }
161
780
 
162
781
  export function updateSessionObservability({
@@ -176,13 +795,20 @@ export function updateSessionObservability({
176
795
  || (noteProvider === 'claude' && identity.transcriptProvider !== 'anthropic')) {
177
796
  throw new Error(`observability provider mismatch: note=${noteProvider}, transcript=${identity.transcriptProvider}`);
178
797
  }
179
- let annotated = setFrontmatterField(sessionContent, 'observability_caller', `"${caller}"`);
798
+ let annotated = sessionContent;
799
+ if (!/^observability_caller:/m.test(annotated)) {
800
+ annotated = setFrontmatterField(annotated, 'observability_caller', `"${caller}"`);
801
+ }
180
802
  annotated = setFrontmatterField(annotated, 'observability_session_id', `"${canonicalConversationId || identity.canonicalConversationId || ''}"`);
181
803
  annotated = setFrontmatterField(annotated, 'observability_transcript_id', `"${identity.transcriptId || ''}"`);
182
804
  if (!/^observability_updated_at:/m.test(annotated)) {
183
805
  annotated = setFrontmatterField(annotated, 'observability_updated_at', `"${new Date().toISOString()}"`);
184
806
  }
185
- const result = buildSessionObservability({ sessionContent: annotated, transcriptPath });
807
+ const result = buildSessionObservability({
808
+ sessionContent: annotated,
809
+ transcriptPath,
810
+ canonicalConversationId: canonicalConversationId || identity.canonicalConversationId || '',
811
+ });
186
812
  if (!result) return null;
187
813
  snapshot = result.snapshot;
188
814
  return result.content;