wendkeep 0.66.3 → 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 +34 -0
- package/README.en.md +13 -8
- package/README.md +13 -8
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/memory.md +31 -2
- 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/memory.md +32 -2
- 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/hooks/vault-health.mjs +6 -0
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -0
- package/packages/vault/src/memory-store.mjs +5 -0
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/memory.mjs +603 -5
- package/src/rebuild-costs.mjs +220 -34
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
|
}
|