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.
- package/CHANGELOG.md +22 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/sessions-and-import.md +15 -0
- 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/sessions-and-import.md +15 -1
- 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/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 +218 -5
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- package/package.json +1 -1
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/rebuild-costs.mjs +220 -34
package/hooks/token-usage.mjs
CHANGED
|
@@ -510,16 +510,19 @@ function detectTranscriptFormat(lines) {
|
|
|
510
510
|
return 'codex';
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
export function
|
|
513
|
+
export function parseTokenUsageFromContent(content, { transcriptPath = '' } = {}) {
|
|
514
514
|
const result = emptyParseResult(transcriptPath);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
|
|
515
|
+
const lines = String(content || '').split('\n').filter(Boolean);
|
|
518
516
|
return detectTranscriptFormat(lines) === 'claude'
|
|
519
517
|
? parseClaudeLines(lines, result)
|
|
520
518
|
: parseCodexLines(lines, result);
|
|
521
519
|
}
|
|
522
520
|
|
|
521
|
+
export function parseTokenUsageFromTranscript(transcriptPath) {
|
|
522
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return emptyParseResult(transcriptPath);
|
|
523
|
+
return parseTokenUsageFromContent(readFileSync(transcriptPath, 'utf-8'), { transcriptPath });
|
|
524
|
+
}
|
|
525
|
+
|
|
523
526
|
function modelCost(usage, model) {
|
|
524
527
|
const normalized = normalizeModelName(model);
|
|
525
528
|
const price = PRICE_REFERENCE[normalized];
|
|
@@ -935,6 +938,80 @@ export function collectSessionUsage({ sessionContent, transcriptPath }) {
|
|
|
935
938
|
};
|
|
936
939
|
}
|
|
937
940
|
|
|
941
|
+
function normalizedTranscriptKey(value) {
|
|
942
|
+
return String(value || '').replace(/\.jsonl?$/i, '').trim().toLowerCase();
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Rebuild the main bucket from every validated top-level rollout. Existing history is used
|
|
946
|
+
// only to preserve stable timestamps; an entry that is neither a known root nor a proven
|
|
947
|
+
// descendant makes the reconstruction fail closed instead of silently deleting a legitimate
|
|
948
|
+
// reopening.
|
|
949
|
+
export function collectSessionUsageForRoots({
|
|
950
|
+
sessionContent,
|
|
951
|
+
rootPaths = [],
|
|
952
|
+
descendantIds = [],
|
|
953
|
+
} = {}) {
|
|
954
|
+
const fmMatch = String(sessionContent || '').match(/^---\n([\s\S]*?)\n---/);
|
|
955
|
+
if (!fmMatch) return null;
|
|
956
|
+
|
|
957
|
+
const roots = [...new Set(rootPaths.filter(Boolean))].sort((a, b) => String(a).localeCompare(String(b)));
|
|
958
|
+
const rootKeys = new Set(roots.map((path) => normalizedTranscriptKey(transcriptIdFromPath(path))));
|
|
959
|
+
const descendantKeys = new Set(descendantIds.map(normalizedTranscriptKey).filter(Boolean));
|
|
960
|
+
const existingEntries = parseUsageHistory(fmMatch[1]);
|
|
961
|
+
const unresolved = existingEntries.filter((entry) => {
|
|
962
|
+
const key = normalizedTranscriptKey(entry.transcript_id);
|
|
963
|
+
return key && !rootKeys.has(key) && !descendantKeys.has(key);
|
|
964
|
+
});
|
|
965
|
+
if (unresolved.length) {
|
|
966
|
+
return {
|
|
967
|
+
state: 'degraded',
|
|
968
|
+
diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: unresolved.length }],
|
|
969
|
+
entries: existingEntries,
|
|
970
|
+
content: sessionContent,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const entries = [];
|
|
975
|
+
const summaries = [];
|
|
976
|
+
for (const transcriptPath of roots) {
|
|
977
|
+
if (!existsSync(transcriptPath)) {
|
|
978
|
+
return {
|
|
979
|
+
state: 'degraded',
|
|
980
|
+
diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
|
|
981
|
+
entries: existingEntries,
|
|
982
|
+
content: sessionContent,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(transcriptPath));
|
|
986
|
+
if (!summary.calls) {
|
|
987
|
+
return {
|
|
988
|
+
state: 'degraded',
|
|
989
|
+
diagnostics: [{ code: 'MAIN_TRANSCRIPT_UNRESOLVED', count: 1 }],
|
|
990
|
+
entries: existingEntries,
|
|
991
|
+
content: sessionContent,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
const transcriptId = transcriptIdFromPath(transcriptPath);
|
|
995
|
+
const current = entryFromSummary(summary, transcriptId);
|
|
996
|
+
const previous = existingEntries.find((entry) => normalizedTranscriptKey(entry.transcript_id) === normalizedTranscriptKey(transcriptId));
|
|
997
|
+
if (previous && sameUsageData(previous, current)) current.atualizado_em = previous.atualizado_em;
|
|
998
|
+
entries.push(current);
|
|
999
|
+
summaries.push(summary);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const agg = aggregateEntries(entries);
|
|
1003
|
+
const content = upsertSessionFrontmatter(sessionContent, agg, entries);
|
|
1004
|
+
if (content === null) return null;
|
|
1005
|
+
return {
|
|
1006
|
+
state: 'complete',
|
|
1007
|
+
diagnostics: [],
|
|
1008
|
+
summaries,
|
|
1009
|
+
aggregate: agg,
|
|
1010
|
+
entries,
|
|
1011
|
+
content,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
938
1015
|
export function updateSessionUsage({ vaultBase, sessionRel, sessionPath, transcriptPath, lockTimeoutMs }) {
|
|
939
1016
|
if (!sessionPath || !existsSync(sessionPath)) return null;
|
|
940
1017
|
let result = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.5",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
package/src/cost.mjs
CHANGED
|
@@ -196,19 +196,53 @@ function opt(argv, name) {
|
|
|
196
196
|
return eq ? eq.slice(name.length + 1) : undefined;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
const REBUILD_LIMIT_FLAGS = [
|
|
200
|
+
['--max-graph-nodes', 'maxGraphNodes'],
|
|
201
|
+
['--max-fallback-days', 'maxFallbackDays'],
|
|
202
|
+
['--max-fallback-candidates', 'maxFallbackCandidates'],
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
export function parseRebuildOptions(argv = []) {
|
|
206
|
+
const session = opt(argv, '--session') || '';
|
|
207
|
+
const overrides = {};
|
|
208
|
+
for (const [flag, key] of REBUILD_LIMIT_FLAGS) {
|
|
209
|
+
const present = argv.includes(flag) || argv.some((arg) => arg.startsWith(`${flag}=`));
|
|
210
|
+
if (!present) continue;
|
|
211
|
+
const value = Number(opt(argv, flag));
|
|
212
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
213
|
+
return { ok: false, exitCode: 2, code: 'INVALID_LIMIT_OVERRIDE' };
|
|
214
|
+
}
|
|
215
|
+
overrides[key] = value;
|
|
216
|
+
}
|
|
217
|
+
if (Object.keys(overrides).length > 0 && !session) {
|
|
218
|
+
return { ok: false, exitCode: 2, code: 'TARGET_REQUIRED_FOR_LIMIT_OVERRIDE' };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
options: {
|
|
223
|
+
apply: argv.includes('--apply'),
|
|
224
|
+
session,
|
|
225
|
+
limit: Number(opt(argv, '--limit')) || 0,
|
|
226
|
+
limits: { ...overrides },
|
|
227
|
+
overrides: { ...overrides },
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
199
232
|
export function runCost(argv) {
|
|
200
233
|
const vaultRaw = opt(argv, '--vault') || process.env.OBSIDIAN_VAULT_PATH;
|
|
201
234
|
if (!vaultRaw) { process.stderr.write('wendkeep cost: no vault (--vault or OBSIDIAN_VAULT_PATH).\n'); process.exit(2); }
|
|
202
235
|
const vaultBase = isAbsolute(vaultRaw) ? vaultRaw : resolve(process.cwd(), vaultRaw);
|
|
203
236
|
if (!existsSync(vaultBase)) { process.stderr.write(`wendkeep cost: vault not found: ${vaultBase}\n`); process.exit(2); }
|
|
204
237
|
if (argv[0] === 'rebuild') {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
238
|
+
const parsed = parseRebuildOptions(argv);
|
|
239
|
+
if (!parsed.ok) {
|
|
240
|
+
process.stderr.write(`wendkeep cost rebuild: ${parsed.code}\n`);
|
|
241
|
+
process.exit(parsed.exitCode);
|
|
242
|
+
}
|
|
243
|
+
const report = rebuildSessionCosts(vaultBase, parsed.options);
|
|
210
244
|
if (argv.includes('--json')) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
211
|
-
else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.
|
|
245
|
+
else process.stdout.write(`cost rebuild (${report.mode}): ${report.scanned} lidas · ${report.changed} alteradas · ${report.unchanged} iguais · ${report.degraded} degradadas · ${report.stale} stale · ${report.missing} sem fonte · ${report.errors} erros\n${report.mode === 'apply' ? 'Relatório: .brain/COST_REBUILD.json\n' : 'Nenhum arquivo foi alterado; use --apply para gravar.\n'}`);
|
|
212
246
|
process.exit(report.ok ? 0 : 1);
|
|
213
247
|
}
|
|
214
248
|
const agg = collectVaultCost(vaultBase, { since: opt(argv, '--since') });
|
package/src/doctor.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines } from '../hooks/harness-doctor.mjs';
|
|
7
|
+
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines, checkSessionObservability, renderSessionObservabilityLines } from '../hooks/harness-doctor.mjs';
|
|
8
8
|
import { checkSyncDefs } from './sync-defs.mjs';
|
|
9
9
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
10
10
|
|
|
@@ -79,6 +79,9 @@ export function runDoctor(argv) {
|
|
|
79
79
|
// 3d. Seções derivadas do corpo que ficaram para trás do Encerramento (notas pré-0.53.0).
|
|
80
80
|
process.stdout.write(`\n${renderStaleDerivedSectionLines(checkStaleDerivedSections(vaultBase)).join('\n')}\n`);
|
|
81
81
|
|
|
82
|
+
// 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
|
|
83
|
+
process.stdout.write(`\n${renderSessionObservabilityLines(checkSessionObservability(vaultBase)).join('\n')}\n`);
|
|
84
|
+
|
|
82
85
|
// 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
|
|
83
86
|
const act = checkSessionActivity(vaultBase);
|
|
84
87
|
if (act.lastSession) {
|
package/src/rebuild-costs.mjs
CHANGED
|
@@ -1,45 +1,231 @@
|
|
|
1
|
-
// Deterministic
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
// Deterministic, causal reconstruction for historical session observability.
|
|
2
|
+
// Dry-run is a pure composition pass; apply delegates all note mutation to the CAS publisher.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
5
6
|
import { readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
6
|
-
import
|
|
7
|
+
import * as sessionObservability from '../hooks/session-observability.mjs';
|
|
8
|
+
import {
|
|
9
|
+
mutateObservabilityStore,
|
|
10
|
+
readObservabilityStore,
|
|
11
|
+
} from '../hooks/session-observability-store.mjs';
|
|
12
|
+
import { sanitizeObservabilityDiagnostics } from '../hooks/session-observability-state.mjs';
|
|
7
13
|
import { assertVaultPathSafe } from '../hooks/vault-path-safety.mjs';
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.filter((
|
|
14
|
-
.
|
|
15
|
-
|
|
15
|
+
function sortedEntries(registry, target) {
|
|
16
|
+
return Object.entries(registry?.sessions || {})
|
|
17
|
+
.map(([sessionId, value]) => ({ sessionId, ...value }))
|
|
18
|
+
.filter((entry) => entry.session_file)
|
|
19
|
+
.filter((entry) => !target || entry.sessionId === target || entry.session_file === target)
|
|
20
|
+
.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function transcriptCandidates(entry) {
|
|
24
|
+
const paths = new Set();
|
|
25
|
+
if (entry.transcript_path) paths.add(entry.transcript_path);
|
|
26
|
+
for (const path of entry.transcript_paths || []) if (path) paths.add(path);
|
|
27
|
+
const activations = Array.isArray(entry.activations)
|
|
28
|
+
? entry.activations
|
|
29
|
+
: Object.values(entry.activations || {});
|
|
30
|
+
for (const activation of activations) {
|
|
31
|
+
if (activation?.transcript_path) paths.add(activation.transcript_path);
|
|
32
|
+
for (const path of activation?.transcript_paths || []) if (path) paths.add(path);
|
|
33
|
+
}
|
|
34
|
+
return [...paths];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function candidateContent(candidate, fallback) {
|
|
38
|
+
return typeof candidate?.content === 'string' ? candidate.content : fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function candidateHash(candidate, fallback) {
|
|
42
|
+
return createHash('sha256').update(candidateContent(candidate, fallback)).digest('hex');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeDiagnostics(input, fallback = []) {
|
|
46
|
+
try {
|
|
47
|
+
return sanitizeObservabilityDiagnostics(input || fallback);
|
|
48
|
+
} catch {
|
|
49
|
+
return sanitizeObservabilityDiagnostics(fallback);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function semanticRebuildReport(report) {
|
|
54
|
+
const {
|
|
55
|
+
generatedAt: _generatedAt,
|
|
56
|
+
changed = 0,
|
|
57
|
+
unchanged = 0,
|
|
58
|
+
sessions = [],
|
|
59
|
+
...semantic
|
|
60
|
+
} = report || {};
|
|
61
|
+
return {
|
|
62
|
+
...semantic,
|
|
63
|
+
converged: Number(changed || 0) + Number(unchanged || 0),
|
|
64
|
+
sessions: sessions.map((entry) => ({
|
|
65
|
+
...entry,
|
|
66
|
+
status: entry.status === 'published' || entry.status === 'unchanged'
|
|
67
|
+
? 'converged'
|
|
68
|
+
: entry.status,
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function writeRebuildReportIfChanged(reportPath, report) {
|
|
74
|
+
if (existsSync(reportPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const previous = JSON.parse(readFileSync(reportPath, 'utf8'));
|
|
77
|
+
if (JSON.stringify(semanticRebuildReport(previous))
|
|
78
|
+
=== JSON.stringify(semanticRebuildReport(report))) return false;
|
|
79
|
+
} catch {
|
|
80
|
+
// Invalid prior reports are replaced by the sanitized current schema.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
mkdirSync(dirname(reportPath), { recursive: true });
|
|
84
|
+
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function markDirtyDefault(vaultBase, sessionId, diagnostics) {
|
|
89
|
+
mutateObservabilityStore(vaultBase, sessionId, (state) => ({
|
|
90
|
+
...state,
|
|
91
|
+
observability_dirty: true,
|
|
92
|
+
diagnostics: safeDiagnostics(diagnostics, [{ code: 'STALE_FRONTIER', count: 1 }]),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function rebuildSessionCosts(
|
|
97
|
+
vaultBase,
|
|
98
|
+
{
|
|
99
|
+
apply = false,
|
|
100
|
+
session = '',
|
|
101
|
+
limit = 0,
|
|
102
|
+
limits = {},
|
|
103
|
+
overrides = {},
|
|
104
|
+
} = {},
|
|
105
|
+
effects = {},
|
|
106
|
+
) {
|
|
107
|
+
const readRegistry = effects.readRegistry || readSessionRegistry;
|
|
108
|
+
const compose = effects.compose || sessionObservability.composeSessionObservability;
|
|
109
|
+
const publish = effects.publish || sessionObservability.publishSessionObservability;
|
|
110
|
+
const readStore = effects.readStore || readObservabilityStore;
|
|
111
|
+
const markDirty = effects.markDirty || markDirtyDefault;
|
|
112
|
+
const writeReport = effects.writeReport || writeRebuildReportIfChanged;
|
|
113
|
+
const now = effects.now || (() => new Date().toISOString());
|
|
114
|
+
if (typeof compose !== 'function') throw new TypeError('composeSessionObservability indisponível');
|
|
115
|
+
if (apply && typeof publish !== 'function') throw new TypeError('publishSessionObservability indisponível');
|
|
116
|
+
|
|
117
|
+
const registry = readRegistry(vaultBase);
|
|
118
|
+
const report = {
|
|
119
|
+
version: 2,
|
|
120
|
+
generatedAt: now(),
|
|
121
|
+
mode: apply ? 'apply' : 'dry-run',
|
|
122
|
+
targeted: Boolean(session),
|
|
123
|
+
overrides: { ...overrides },
|
|
124
|
+
scanned: 0,
|
|
125
|
+
changed: 0,
|
|
126
|
+
unchanged: 0,
|
|
127
|
+
degraded: 0,
|
|
128
|
+
stale: 0,
|
|
129
|
+
missing: 0,
|
|
130
|
+
errors: 0,
|
|
131
|
+
ok: true,
|
|
132
|
+
sessions: [],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const entry of sortedEntries(registry, session)) {
|
|
16
136
|
if (limit && report.scanned >= limit) break;
|
|
17
137
|
report.scanned += 1;
|
|
18
|
-
|
|
19
|
-
expectedType: 'file', label: 'nota de sessão do rebuild de custos',
|
|
20
|
-
});
|
|
21
|
-
const note = checkedNote.target;
|
|
22
|
-
if (!entry.transcript_path || !checkedNote.exists || !existsSync(entry.transcript_path)) {
|
|
23
|
-
report.missing.push({ sessionId: entry.sessionId, session: entry.session_file, note: checkedNote.exists, transcript: !!entry.transcript_path && existsSync(entry.transcript_path), transcriptPath: entry.transcript_path || '' });
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
const before = readFileSync(note, 'utf8');
|
|
138
|
+
let note;
|
|
27
139
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
caller: 'cost-rebuild', canonicalConversationId: entry.sessionId,
|
|
140
|
+
const checked = assertVaultPathSafe(vaultBase, join(vaultBase, entry.session_file), {
|
|
141
|
+
expectedType: 'file', label: 'nota de sessão do rebuild de custos',
|
|
31
142
|
});
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
143
|
+
const hasTranscript = transcriptCandidates(entry).some((path) => existsSync(path));
|
|
144
|
+
if (!checked.exists || !hasTranscript) {
|
|
145
|
+
report.missing += 1;
|
|
146
|
+
report.sessions.push({
|
|
147
|
+
sessionId: entry.sessionId, status: 'missing', diagnostics: [],
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
note = checked.target;
|
|
152
|
+
const before = readFileSync(note, 'utf8');
|
|
153
|
+
const runtimeState = readStore(vaultBase, entry.sessionId);
|
|
154
|
+
const candidate = compose({
|
|
155
|
+
vaultBase,
|
|
156
|
+
sessionContent: before,
|
|
157
|
+
sessionEntry: entry,
|
|
158
|
+
canonicalConversationId: entry.sessionId,
|
|
159
|
+
caller: 'cost-rebuild',
|
|
160
|
+
mode: 'offline',
|
|
161
|
+
limits,
|
|
162
|
+
runtimeState,
|
|
163
|
+
});
|
|
164
|
+
const diagnostics = safeDiagnostics(candidate?.diagnostics);
|
|
165
|
+
const contentHash = candidateHash(candidate, before);
|
|
166
|
+
if (candidate?.state === 'degraded') {
|
|
167
|
+
report.degraded += 1;
|
|
168
|
+
report.sessions.push({ sessionId: entry.sessionId, status: 'degraded', diagnostics });
|
|
169
|
+
if (apply) markDirty(vaultBase, entry.sessionId, diagnostics);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (candidate?.state !== 'complete' && candidate?.state !== 'none') {
|
|
173
|
+
report.degraded += 1;
|
|
174
|
+
const invalidDiagnostics = [{ code: 'PARENT_META_INVALID', count: 1 }];
|
|
175
|
+
report.sessions.push({
|
|
176
|
+
sessionId: entry.sessionId, status: 'degraded', diagnostics: invalidDiagnostics,
|
|
177
|
+
});
|
|
178
|
+
if (apply) markDirty(vaultBase, entry.sessionId, invalidDiagnostics);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!apply) {
|
|
183
|
+
const changed = candidateContent(candidate, before) !== before;
|
|
184
|
+
if (changed) report.changed += 1;
|
|
185
|
+
else report.unchanged += 1;
|
|
186
|
+
report.sessions.push({
|
|
187
|
+
sessionId: entry.sessionId,
|
|
188
|
+
status: changed ? 'would-change' : 'unchanged',
|
|
189
|
+
candidateHash: contentHash,
|
|
190
|
+
diagnostics,
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const outcome = publish({
|
|
196
|
+
vaultBase,
|
|
197
|
+
sessionPath: note,
|
|
198
|
+
canonicalConversationId: entry.sessionId,
|
|
199
|
+
candidate,
|
|
200
|
+
caller: 'cost-rebuild',
|
|
201
|
+
mode: 'offline',
|
|
202
|
+
allowSourceRefresh: true,
|
|
203
|
+
}) || { status: 'degraded' };
|
|
204
|
+
if (outcome.status === 'published') report.changed += 1;
|
|
205
|
+
else if (outcome.status === 'unchanged') report.unchanged += 1;
|
|
206
|
+
else if (outcome.status === 'stale' || outcome.status === 'conflict') {
|
|
207
|
+
report.stale += 1;
|
|
208
|
+
markDirty(vaultBase, entry.sessionId, [{ code: 'STALE_FRONTIER', count: 1 }]);
|
|
209
|
+
} else {
|
|
210
|
+
report.degraded += 1;
|
|
211
|
+
markDirty(vaultBase, entry.sessionId, [{ code: 'PARENT_META_INVALID', count: 1 }]);
|
|
212
|
+
}
|
|
213
|
+
report.sessions.push({
|
|
214
|
+
sessionId: entry.sessionId,
|
|
215
|
+
status: outcome.status || 'degraded',
|
|
216
|
+
candidateHash: contentHash,
|
|
217
|
+
diagnostics: safeDiagnostics(outcome.diagnostics, diagnostics),
|
|
218
|
+
});
|
|
219
|
+
} catch {
|
|
220
|
+
report.errors += 1;
|
|
221
|
+
const diagnostics = [{ code: 'PARENT_META_INVALID', count: 1 }];
|
|
222
|
+
report.sessions.push({ sessionId: entry.sessionId, status: 'degraded', diagnostics });
|
|
223
|
+
if (apply) markDirty(vaultBase, entry.sessionId, diagnostics);
|
|
40
224
|
}
|
|
41
225
|
}
|
|
42
|
-
|
|
43
|
-
|
|
226
|
+
|
|
227
|
+
report.ok = report.degraded === 0 && report.stale === 0
|
|
228
|
+
&& report.missing === 0 && report.errors === 0;
|
|
229
|
+
if (apply) writeReport(join(vaultBase, '.brain', 'COST_REBUILD.json'), report);
|
|
44
230
|
return report;
|
|
45
231
|
}
|