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.
- package/CHANGELOG.md +50 -0
- package/README.en.md +78 -5
- package/README.md +78 -5
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/operating-profiles.md +65 -10
- package/docs/en/commands/sessions-and-import.md +22 -1
- package/docs/en/commands/verify.md +5 -3
- package/docs/pt-BR/commands/costs-and-observability.md +21 -7
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
- package/docs/pt-BR/commands/operating-profiles.md +66 -11
- package/docs/pt-BR/commands/sessions-and-import.md +20 -0
- package/docs/pt-BR/commands/verify.md +6 -3
- package/hooks/change-nag.mjs +8 -0
- package/hooks/codex-rollout-meta.mjs +112 -0
- package/hooks/codex-subagent-graph.mjs +903 -0
- package/hooks/harness-doctor.mjs +82 -1
- package/hooks/import-sessions.mjs +185 -50
- package/hooks/operating-profile-runtime.mjs +36 -2
- package/hooks/operating-profile-task-store.mjs +77 -0
- package/hooks/session-identity.mjs +40 -5
- package/hooks/session-observability-lifecycle.mjs +129 -0
- package/hooks/session-observability-state.mjs +241 -0
- package/hooks/session-observability-store.mjs +436 -0
- package/hooks/session-observability.mjs +647 -21
- package/hooks/session-stop.mjs +339 -11
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- package/package.json +3 -3
- package/packages/harness/src/operating-profile.mjs +127 -0
- package/packages/harness/src/sensors-core.mjs +41 -1
- package/packages/integrations/src/prompt-content.mjs +123 -0
- package/packages/integrations/src/transcripts.mjs +16 -10
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/profile.mjs +95 -17
- package/src/rebuild-costs.mjs +220 -34
- package/src/skills-seed.mjs +38 -2
- 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 {
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 ||
|
|
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
|
-
|
|
159
|
-
|
|
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 =
|
|
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({
|
|
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;
|