wendkeep 0.85.0 → 0.86.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 +40 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/docs/en/commands/evidence-embeddings.md +243 -0
- package/docs/en/commands/mcp.md +67 -7
- package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
- package/docs/pt-BR/commands/mcp.md +66 -7
- package/hooks/evidence-context.mjs +41 -7
- package/hooks/evidence-recall.mjs +10 -0
- package/package.json +1 -1
- package/packages/mcp/src/effects.mjs +3 -2
- package/packages/mcp/src/evidence-recall.mjs +130 -0
- package/packages/mcp/src/executor.mjs +4 -0
- package/packages/mcp/src/server.mjs +31 -1
- package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
- package/packages/vault/src/evidence-index-store.mjs +360 -0
- package/packages/vault/src/evidence-recall-page.mjs +381 -0
- package/packages/vault/src/evidence-search-index.mjs +917 -0
- package/packages/vault/src/index.mjs +12 -1
- package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
- package/packages/vault/src/memory-ledger-view.mjs +41 -0
- package/packages/vault/src/memory-rotation-store.mjs +967 -0
- package/packages/vault/src/memory-segment-store.mjs +820 -0
- package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
- package/packages/vault/src/memory-store-base.mjs +1161 -0
- package/packages/vault/src/memory-store-core.mjs +2 -0
- package/packages/vault/src/memory-store.mjs +46 -1161
- package/src/doctor.mjs +41 -5
- package/src/evidence-search-health.mjs +221 -0
- package/src/memory-scale-health.mjs +210 -0
- package/src/observer-snapshot.mjs +87 -1
package/src/doctor.mjs
CHANGED
|
@@ -4,6 +4,11 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontmatter, renderStackedFrontmatterLines, checkUnpricedModels, renderUnpricedModelLines, checkStaleDerivedSections, renderStaleDerivedSectionLines, checkSessionObservability, renderSessionObservabilityLines } from '../hooks/harness-doctor.mjs';
|
|
5
5
|
import { diagnoseManagedWorktrees } from './worktree.mjs';
|
|
6
6
|
import { runVaultHealth } from '../hooks/vault-health.mjs';
|
|
7
|
+
import {
|
|
8
|
+
inspectEvidenceSearchHealth,
|
|
9
|
+
renderEvidenceSearchHealthLines,
|
|
10
|
+
} from './evidence-search-health.mjs';
|
|
11
|
+
import { augmentVaultHealthWithMemoryScale } from './memory-scale-health.mjs';
|
|
7
12
|
import { checkSyncDefs } from './sync-defs.mjs';
|
|
8
13
|
import { resolveProjectVault } from './project-vault.mjs';
|
|
9
14
|
import { inspectObserverSqlOutbox } from './observer-sql-publish.mjs';
|
|
@@ -19,6 +24,12 @@ const healthStatusLabel = (status) => ({
|
|
|
19
24
|
healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
|
|
20
25
|
}[status] || status || 'desconhecido');
|
|
21
26
|
|
|
27
|
+
const artifactStatusLabel = (status) => ({
|
|
28
|
+
ok: 'saudável', healthy: 'saudável', warning: 'atenção', degraded: 'degradado',
|
|
29
|
+
missing: 'ausente', empty: 'vazio', invalid: 'inválido', corrupt: 'corrompido',
|
|
30
|
+
blocked: 'bloqueado', unknown: 'desconhecido',
|
|
31
|
+
}[status] || status || 'desconhecido');
|
|
32
|
+
|
|
22
33
|
const metricValue = (value) => value === null || value === undefined || value === '' ? 'n/a' : value;
|
|
23
34
|
|
|
24
35
|
export function renderVaultHealthLines(result) {
|
|
@@ -49,6 +60,19 @@ export function renderVaultHealthLines(result) {
|
|
|
49
60
|
);
|
|
50
61
|
const repairableHandoffs = Number(memory.repairableHandoffs || 0);
|
|
51
62
|
lines.push(` ledger: ${metricValue(memory.ledgerEvents)} evento(s) · outbox: ${metricValue(memory.pendingOutbox)} · candidates: ${metricValue(memory.candidates)} · conflitos: ${metricValue(memory.activeConflicts)}${repairableHandoffs ? ` · handoffs reparáveis: ${repairableHandoffs}` : ''}`);
|
|
63
|
+
if (memory.scaleSchemaVersion === 1) {
|
|
64
|
+
lines.push(
|
|
65
|
+
` replay: snapshot ${artifactStatusLabel(memory.snapshotStatus)} · cobertos: ${metricValue(memory.snapshotEvents)} evento(s) · tail: ${metricValue(memory.snapshotTailEvents)} evento(s)/${metricValue(memory.snapshotTailBytes)} bytes · ledger no snapshot: ${metricValue(memory.snapshotLedgerBytes)} bytes`,
|
|
66
|
+
);
|
|
67
|
+
if (memory.snapshotReason) lines.push(` ↳ snapshot: ${memory.snapshotReason}`);
|
|
68
|
+
lines.push(
|
|
69
|
+
` segmentos: ${artifactStatusLabel(memory.segmentStatus)} · ${metricValue(memory.segmentCount)} segmento(s) · cobertos: ${metricValue(memory.segmentCoveredEvents)} evento(s)/${metricValue(memory.segmentCoveredBytes)} bytes · pendentes: ${metricValue(memory.segmentPendingEvents)}`,
|
|
70
|
+
);
|
|
71
|
+
lines.push(
|
|
72
|
+
` rotação: geração ${artifactStatusLabel(memory.generationStatus)} #${metricValue(memory.generation)} · origem: ${metricValue(memory.generationSourceEvents)} · tail ativo: ${metricValue(memory.generationActiveTailEvents)} · journal: ${artifactStatusLabel(memory.rotationJournal)} · receipts: ${metricValue(memory.rotationReceipts)} (${artifactStatusLabel(memory.rotationReceiptCheckpoint)})`,
|
|
73
|
+
);
|
|
74
|
+
if (memory.scaleErrorCode) lines.push(` ↳ escala: ${memory.scaleErrorCode}`);
|
|
75
|
+
}
|
|
52
76
|
const semanticKeys = memory.semanticActiveKeys || [];
|
|
53
77
|
const semanticProjected = memory.semanticProjectedKeys || [];
|
|
54
78
|
const semanticMissing = memory.semanticMissingKeys || [];
|
|
@@ -105,7 +129,10 @@ export function runDoctor(argv) {
|
|
|
105
129
|
// 1. Session/vault integrity. The standalone hook remains JSON; doctor renders it for humans.
|
|
106
130
|
let health;
|
|
107
131
|
try {
|
|
108
|
-
health =
|
|
132
|
+
health = augmentVaultHealthWithMemoryScale(
|
|
133
|
+
runVaultHealth({ vaultBase, session }),
|
|
134
|
+
vaultBase,
|
|
135
|
+
);
|
|
109
136
|
} catch (error) {
|
|
110
137
|
health = {
|
|
111
138
|
ok: false,
|
|
@@ -116,16 +143,24 @@ export function runDoctor(argv) {
|
|
|
116
143
|
memoryStatus: 'blocked',
|
|
117
144
|
};
|
|
118
145
|
}
|
|
119
|
-
|
|
146
|
+
const recall = scope === 'runtime'
|
|
147
|
+
? { status: 'skipped' }
|
|
148
|
+
: inspectEvidenceSearchHealth(vaultBase);
|
|
149
|
+
if (scope !== 'runtime') {
|
|
150
|
+
process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
|
|
151
|
+
process.stdout.write(`${renderEvidenceSearchHealthLines(recall).join('\n')}\n`);
|
|
152
|
+
}
|
|
120
153
|
const healthStatus = health.ok ? 0 : 1;
|
|
154
|
+
const recallStatus = recall.status === 'blocked' ? 1 : 0;
|
|
121
155
|
|
|
122
156
|
if (scope === 'core') {
|
|
123
157
|
const strictDebt = strict && (
|
|
124
158
|
(health.warnings || []).length > 0
|
|
125
159
|
|| !['healthy'].includes(health.memoryStatus)
|
|
160
|
+
|| !['healthy', 'missing'].includes(recall.status)
|
|
126
161
|
);
|
|
127
|
-
process.stdout.write(`\n[core] ${healthStatus ? 'erro estrutural' : health.memoryStatus === 'degraded' ? 'saudável com memória degradada' : 'saudável'}\n`);
|
|
128
|
-
return healthStatus || strictDebt ? 1 : 0;
|
|
162
|
+
process.stdout.write(`\n[core] ${healthStatus || recallStatus ? 'erro estrutural' : health.memoryStatus === 'degraded' ? 'saudável com memória degradada' : 'saudável'}\n`);
|
|
163
|
+
return healthStatus || recallStatus || strictDebt ? 1 : 0;
|
|
129
164
|
}
|
|
130
165
|
|
|
131
166
|
// 2. Harness integrity (Wave B).
|
|
@@ -217,6 +252,7 @@ export function runDoctor(argv) {
|
|
|
217
252
|
const strictDebt = strict && (
|
|
218
253
|
(scope !== 'runtime' && (health.warnings || []).length)
|
|
219
254
|
|| (scope !== 'runtime' && health.memoryStatus !== 'healthy')
|
|
255
|
+
|| (scope !== 'runtime' && !['healthy', 'missing'].includes(recall.status))
|
|
220
256
|
|| attention.length
|
|
221
257
|
|| repairable.length
|
|
222
258
|
|| warnings.length
|
|
@@ -233,5 +269,5 @@ export function runDoctor(argv) {
|
|
|
233
269
|
|| (staleDerived.notes || staleDerived.items || []).length
|
|
234
270
|
|| !observability.ok
|
|
235
271
|
);
|
|
236
|
-
return (scope !== 'runtime' && healthStatus !== 0) || errors.length || strictDebt ? 1 : 0;
|
|
272
|
+
return (scope !== 'runtime' && (healthStatus !== 0 || recallStatus !== 0)) || errors.length || strictDebt ? 1 : 0;
|
|
237
273
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
EVIDENCE_INDEX_FILE,
|
|
6
|
+
} from '../packages/vault/src/evidence-recall.mjs';
|
|
7
|
+
import {
|
|
8
|
+
EVIDENCE_INDEX_STATE_FILE,
|
|
9
|
+
loadEvidenceIndexState,
|
|
10
|
+
} from '../packages/vault/src/evidence-index-store.mjs';
|
|
11
|
+
import {
|
|
12
|
+
EVIDENCE_SEARCH_STATE_FILE,
|
|
13
|
+
evidenceSearchSqliteAvailable,
|
|
14
|
+
loadEvidenceSearchState,
|
|
15
|
+
} from '../packages/vault/src/evidence-search-index.mjs';
|
|
16
|
+
import { assertVaultPathSafe } from '../packages/vault/src/vault-path-safety.mjs';
|
|
17
|
+
|
|
18
|
+
export const EVIDENCE_SEARCH_HEALTH_SCHEMA_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
function brainDir(vaultBase) {
|
|
21
|
+
return join(vaultBase, '.brain');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nsText(value, fallbackMs = 0) {
|
|
25
|
+
if (typeof value === 'bigint') return value.toString();
|
|
26
|
+
return BigInt(Math.max(0, Math.trunc(Number(fallbackMs || 0) * 1_000_000))).toString();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fileInfo(vaultBase, path, label) {
|
|
30
|
+
let checked = assertVaultPathSafe(vaultBase, path, {
|
|
31
|
+
expectedType: 'file',
|
|
32
|
+
label,
|
|
33
|
+
});
|
|
34
|
+
if (!checked.exists) return { exists: false, bytes: 0, fingerprint: null };
|
|
35
|
+
checked = assertVaultPathSafe(vaultBase, checked.target, {
|
|
36
|
+
allowMissing: false,
|
|
37
|
+
expectedType: 'file',
|
|
38
|
+
label,
|
|
39
|
+
});
|
|
40
|
+
const stat = statSync(checked.target, { bigint: true });
|
|
41
|
+
return {
|
|
42
|
+
exists: true,
|
|
43
|
+
bytes: Number(stat.size),
|
|
44
|
+
fingerprint: {
|
|
45
|
+
size: stat.size.toString(),
|
|
46
|
+
mtime_ns: nsText(stat.mtimeNs, stat.mtimeMs),
|
|
47
|
+
ctime_ns: nsText(stat.ctimeNs, stat.ctimeMs),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameFingerprint(left, right) {
|
|
53
|
+
if (left === null || right === null) return left === right;
|
|
54
|
+
return Boolean(left && right)
|
|
55
|
+
&& String(left.size) === String(right.size)
|
|
56
|
+
&& String(left.mtime_ns) === String(right.mtime_ns)
|
|
57
|
+
&& String(left.ctime_ns) === String(right.ctime_ns);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeCode(value, fallback = '') {
|
|
61
|
+
const text = String(value || fallback).trim();
|
|
62
|
+
return text
|
|
63
|
+
? text.replace(/[^A-Za-z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 120)
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function artifactInfo(vaultBase, artifact, kind) {
|
|
68
|
+
if (!artifact) {
|
|
69
|
+
return { status: 'not-built', bytes: 0, current: false };
|
|
70
|
+
}
|
|
71
|
+
const path = join(brainDir(vaultBase), ...String(artifact.path || '').split('/'));
|
|
72
|
+
const file = fileInfo(vaultBase, path, `artefato ${kind} da busca de evidências`);
|
|
73
|
+
if (!file.exists) return { status: 'missing', bytes: 0, current: false };
|
|
74
|
+
const current = sameFingerprint(artifact.fingerprint, file.fingerprint);
|
|
75
|
+
return {
|
|
76
|
+
status: current ? 'current' : 'stale',
|
|
77
|
+
bytes: file.bytes,
|
|
78
|
+
current,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function emptyEvidenceSearchHealth() {
|
|
83
|
+
return {
|
|
84
|
+
schemaVersion: EVIDENCE_SEARCH_HEALTH_SCHEMA_VERSION,
|
|
85
|
+
status: 'unknown',
|
|
86
|
+
errorCode: null,
|
|
87
|
+
authorityStatus: 'unknown',
|
|
88
|
+
authorityBytes: 0,
|
|
89
|
+
incrementalStateStatus: 'unknown',
|
|
90
|
+
incrementalStateBytes: 0,
|
|
91
|
+
documentCount: 0,
|
|
92
|
+
searchStateStatus: 'unknown',
|
|
93
|
+
searchStateBytes: 0,
|
|
94
|
+
rowCount: 0,
|
|
95
|
+
sourceIndexCurrent: false,
|
|
96
|
+
sourceStateCurrent: false,
|
|
97
|
+
lexicalStatus: 'unknown',
|
|
98
|
+
lexicalBytes: 0,
|
|
99
|
+
sqliteStatus: 'unknown',
|
|
100
|
+
sqliteBytes: 0,
|
|
101
|
+
sqliteCapability: false,
|
|
102
|
+
backend: 'unavailable',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function deriveStatus(metrics) {
|
|
107
|
+
if (metrics.authorityStatus === 'missing') return 'missing';
|
|
108
|
+
if (metrics.incrementalStateStatus === 'invalid'
|
|
109
|
+
|| metrics.searchStateStatus === 'invalid') return 'degraded';
|
|
110
|
+
if (metrics.searchStateStatus !== 'current') return 'warning';
|
|
111
|
+
if (metrics.lexicalStatus !== 'current') return 'degraded';
|
|
112
|
+
return 'healthy';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function inspectEvidenceSearchHealth(vaultBase) {
|
|
116
|
+
const metrics = emptyEvidenceSearchHealth();
|
|
117
|
+
try {
|
|
118
|
+
const brain = brainDir(vaultBase);
|
|
119
|
+
const authority = fileInfo(
|
|
120
|
+
vaultBase,
|
|
121
|
+
join(brain, EVIDENCE_INDEX_FILE),
|
|
122
|
+
'autoridade EVIDENCE_INDEX.jsonl',
|
|
123
|
+
);
|
|
124
|
+
const incrementalFile = fileInfo(
|
|
125
|
+
vaultBase,
|
|
126
|
+
join(brain, EVIDENCE_INDEX_STATE_FILE),
|
|
127
|
+
'estado incremental EVIDENCE_INDEX_STATE.json',
|
|
128
|
+
);
|
|
129
|
+
const searchFile = fileInfo(
|
|
130
|
+
vaultBase,
|
|
131
|
+
join(brain, EVIDENCE_SEARCH_STATE_FILE),
|
|
132
|
+
'estado derivado EVIDENCE_SEARCH_STATE.json',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
metrics.authorityStatus = authority.exists ? 'present' : 'missing';
|
|
136
|
+
metrics.authorityBytes = authority.bytes;
|
|
137
|
+
metrics.incrementalStateBytes = incrementalFile.bytes;
|
|
138
|
+
metrics.searchStateBytes = searchFile.bytes;
|
|
139
|
+
metrics.sqliteCapability = evidenceSearchSqliteAvailable();
|
|
140
|
+
|
|
141
|
+
const incremental = incrementalFile.exists ? loadEvidenceIndexState(vaultBase) : null;
|
|
142
|
+
metrics.incrementalStateStatus = incrementalFile.exists
|
|
143
|
+
? (incremental ? 'ok' : 'invalid')
|
|
144
|
+
: 'missing';
|
|
145
|
+
metrics.documentCount = incremental
|
|
146
|
+
? Object.keys(incremental.documents || {}).length
|
|
147
|
+
: 0;
|
|
148
|
+
|
|
149
|
+
const state = searchFile.exists ? loadEvidenceSearchState(vaultBase) : null;
|
|
150
|
+
if (!searchFile.exists) {
|
|
151
|
+
metrics.searchStateStatus = 'missing';
|
|
152
|
+
metrics.lexicalStatus = 'not-built';
|
|
153
|
+
metrics.sqliteStatus = 'not-built';
|
|
154
|
+
metrics.backend = authority.exists ? 'lexical-ephemeral' : 'unavailable';
|
|
155
|
+
metrics.status = deriveStatus(metrics);
|
|
156
|
+
return metrics;
|
|
157
|
+
}
|
|
158
|
+
if (!state) {
|
|
159
|
+
metrics.searchStateStatus = 'invalid';
|
|
160
|
+
metrics.lexicalStatus = 'unknown';
|
|
161
|
+
metrics.sqliteStatus = 'unknown';
|
|
162
|
+
metrics.backend = authority.exists ? 'lexical-ephemeral' : 'unavailable';
|
|
163
|
+
metrics.status = deriveStatus(metrics);
|
|
164
|
+
return metrics;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
metrics.rowCount = Number(state.row_count || 0);
|
|
168
|
+
metrics.sourceIndexCurrent = sameFingerprint(state.source?.index, authority.fingerprint);
|
|
169
|
+
metrics.sourceStateCurrent = sameFingerprint(
|
|
170
|
+
state.source?.state ?? null,
|
|
171
|
+
incrementalFile.fingerprint,
|
|
172
|
+
);
|
|
173
|
+
const lexical = artifactInfo(vaultBase, state.lexical, 'lexical');
|
|
174
|
+
const sqlite = artifactInfo(vaultBase, state.sqlite, 'SQLite FTS');
|
|
175
|
+
metrics.lexicalStatus = lexical.status;
|
|
176
|
+
metrics.lexicalBytes = lexical.bytes;
|
|
177
|
+
metrics.sqliteStatus = sqlite.status;
|
|
178
|
+
metrics.sqliteBytes = sqlite.bytes;
|
|
179
|
+
|
|
180
|
+
const sourceCurrent = metrics.sourceIndexCurrent && metrics.sourceStateCurrent;
|
|
181
|
+
const artifactsCurrent = lexical.current && (!state.sqlite || sqlite.current);
|
|
182
|
+
metrics.searchStateStatus = sourceCurrent && artifactsCurrent ? 'current' : 'stale';
|
|
183
|
+
metrics.backend = metrics.searchStateStatus === 'current'
|
|
184
|
+
&& sqlite.current
|
|
185
|
+
&& metrics.sqliteCapability
|
|
186
|
+
? 'sqlite-fts5'
|
|
187
|
+
: metrics.searchStateStatus === 'current' && lexical.current
|
|
188
|
+
? 'lexical-sidecar'
|
|
189
|
+
: authority.exists
|
|
190
|
+
? 'lexical-ephemeral'
|
|
191
|
+
: 'unavailable';
|
|
192
|
+
metrics.status = deriveStatus(metrics);
|
|
193
|
+
return metrics;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return {
|
|
196
|
+
...metrics,
|
|
197
|
+
status: 'blocked',
|
|
198
|
+
errorCode: safeCode(error?.code, 'EVIDENCE_SEARCH_HEALTH_UNSAFE'),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const statusLabel = (status) => ({
|
|
204
|
+
healthy: 'saudável', warning: 'atenção', degraded: 'degradado', blocked: 'bloqueado',
|
|
205
|
+
missing: 'ausente', current: 'atual', stale: 'stale', present: 'presente', ok: 'saudável',
|
|
206
|
+
invalid: 'inválido', 'not-built': 'não construído', unknown: 'desconhecido',
|
|
207
|
+
}[status] || status || 'desconhecido');
|
|
208
|
+
|
|
209
|
+
export function renderEvidenceSearchHealthLines(metrics) {
|
|
210
|
+
const lines = [
|
|
211
|
+
`[recall] ${statusLabel(metrics.status)} — backend: ${metrics.backend} · SQLite/FTS5: ${metrics.sqliteCapability ? 'disponível' : 'indisponível'}`,
|
|
212
|
+
` autoridade: ${statusLabel(metrics.authorityStatus)} · ${metrics.authorityBytes} bytes · documentos: ${metrics.documentCount} · chunks: ${metrics.rowCount}`,
|
|
213
|
+
` incremental: ${statusLabel(metrics.incrementalStateStatus)} · ${metrics.incrementalStateBytes} bytes · busca: ${statusLabel(metrics.searchStateStatus)} · ${metrics.searchStateBytes} bytes`,
|
|
214
|
+
` lexical: ${statusLabel(metrics.lexicalStatus)} · ${metrics.lexicalBytes} bytes · SQLite: ${statusLabel(metrics.sqliteStatus)} · ${metrics.sqliteBytes} bytes`,
|
|
215
|
+
];
|
|
216
|
+
if (metrics.searchStateStatus === 'stale' || metrics.searchStateStatus === 'missing') {
|
|
217
|
+
lines.push(' ! o próximo recall pode reconstruir o índice derivado a partir da autoridade JSONL');
|
|
218
|
+
}
|
|
219
|
+
if (metrics.errorCode) lines.push(` ✗ ${metrics.errorCode}`);
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readMemoryLedgerGeneration,
|
|
3
|
+
readMemoryProjectionSnapshot,
|
|
4
|
+
readMemoryRotationJournal,
|
|
5
|
+
readMemoryRotationReceipts,
|
|
6
|
+
readMemorySegmentManifest,
|
|
7
|
+
} from '../hooks/memory-store.mjs';
|
|
8
|
+
import {
|
|
9
|
+
quoteCommandArgument,
|
|
10
|
+
WENDKEEP_COMMAND,
|
|
11
|
+
} from '../hooks/obsidian-common.mjs';
|
|
12
|
+
|
|
13
|
+
export const MEMORY_SCALE_HEALTH_SCHEMA_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
function safeReason(value, fallback = '') {
|
|
16
|
+
const text = String(value || fallback).trim();
|
|
17
|
+
return text
|
|
18
|
+
? text.replace(/[^A-Za-z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 120)
|
|
19
|
+
: null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function numberMetric(value) {
|
|
23
|
+
const number = Number(value || 0);
|
|
24
|
+
return Number.isSafeInteger(number) && number >= 0 ? number : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function emptyMemoryScaleMetrics() {
|
|
28
|
+
return {
|
|
29
|
+
scaleSchemaVersion: MEMORY_SCALE_HEALTH_SCHEMA_VERSION,
|
|
30
|
+
scaleStatus: 'unknown',
|
|
31
|
+
scaleErrorCode: null,
|
|
32
|
+
snapshotStatus: 'unknown',
|
|
33
|
+
snapshotReason: null,
|
|
34
|
+
snapshotEvents: 0,
|
|
35
|
+
snapshotLedgerBytes: 0,
|
|
36
|
+
snapshotTailEvents: 0,
|
|
37
|
+
snapshotTailBytes: 0,
|
|
38
|
+
segmentStatus: 'unknown',
|
|
39
|
+
segmentCount: 0,
|
|
40
|
+
segmentCoveredEvents: 0,
|
|
41
|
+
segmentCoveredBytes: 0,
|
|
42
|
+
segmentPendingEvents: 0,
|
|
43
|
+
generationStatus: 'unknown',
|
|
44
|
+
generation: 0,
|
|
45
|
+
generationSourceEvents: 0,
|
|
46
|
+
generationActiveTailEvents: 0,
|
|
47
|
+
generationRotatedAt: null,
|
|
48
|
+
rotationJournal: 'unknown',
|
|
49
|
+
rotationRecoveryRequired: false,
|
|
50
|
+
rotationReceiptsStatus: 'unknown',
|
|
51
|
+
rotationReceipts: 0,
|
|
52
|
+
rotationReceiptCheckpoint: 'unknown',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function inspectSnapshot(vaultBase, metrics) {
|
|
57
|
+
try {
|
|
58
|
+
const snapshot = readMemoryProjectionSnapshot(vaultBase);
|
|
59
|
+
const tailStatus = snapshot.status === 'ok' ? snapshot.tail?.status : null;
|
|
60
|
+
metrics.snapshotStatus = snapshot.status === 'ok' && tailStatus !== 'ok'
|
|
61
|
+
? 'invalid'
|
|
62
|
+
: snapshot.status;
|
|
63
|
+
metrics.snapshotReason = safeReason(
|
|
64
|
+
snapshot.reason || snapshot.tail?.reason,
|
|
65
|
+
metrics.snapshotStatus === 'invalid' ? 'snapshot-tail-unavailable' : '',
|
|
66
|
+
);
|
|
67
|
+
if (snapshot.status === 'ok') {
|
|
68
|
+
metrics.snapshotEvents = numberMetric(snapshot.snapshot?.event_count);
|
|
69
|
+
metrics.snapshotLedgerBytes = numberMetric(snapshot.snapshot?.ledger_bytes);
|
|
70
|
+
metrics.snapshotTailEvents = Array.isArray(snapshot.tail?.events)
|
|
71
|
+
? snapshot.tail.events.length
|
|
72
|
+
: 0;
|
|
73
|
+
metrics.snapshotTailBytes = numberMetric(snapshot.tail?.bytes);
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
77
|
+
metrics.snapshotStatus = 'invalid';
|
|
78
|
+
metrics.snapshotReason = safeReason(error?.code, 'snapshot-read-failed');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inspectSegments(vaultBase, ledgerEvents, metrics) {
|
|
83
|
+
try {
|
|
84
|
+
const manifest = readMemorySegmentManifest(vaultBase);
|
|
85
|
+
metrics.segmentStatus = manifest.status;
|
|
86
|
+
if (manifest.status === 'ok') {
|
|
87
|
+
metrics.segmentCount = numberMetric(manifest.manifest?.segment_count);
|
|
88
|
+
metrics.segmentCoveredEvents = numberMetric(manifest.manifest?.covered_event_count);
|
|
89
|
+
metrics.segmentCoveredBytes = numberMetric(manifest.manifest?.covered_bytes);
|
|
90
|
+
}
|
|
91
|
+
metrics.segmentPendingEvents = Math.max(
|
|
92
|
+
0,
|
|
93
|
+
numberMetric(ledgerEvents) - metrics.segmentCoveredEvents,
|
|
94
|
+
);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
97
|
+
metrics.segmentStatus = 'invalid';
|
|
98
|
+
metrics.scaleErrorCode ||= safeReason(error?.code, 'segment-manifest-read-failed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function inspectRotation(vaultBase, ledgerEvents, metrics) {
|
|
103
|
+
try {
|
|
104
|
+
const generation = readMemoryLedgerGeneration(vaultBase);
|
|
105
|
+
metrics.generationStatus = generation.status;
|
|
106
|
+
if (generation.status === 'ok') {
|
|
107
|
+
metrics.generation = numberMetric(generation.state?.generation);
|
|
108
|
+
metrics.generationSourceEvents = numberMetric(generation.state?.source_event_count);
|
|
109
|
+
metrics.generationActiveTailEvents = Math.max(
|
|
110
|
+
0,
|
|
111
|
+
numberMetric(ledgerEvents) - metrics.generationSourceEvents,
|
|
112
|
+
);
|
|
113
|
+
metrics.generationRotatedAt = String(generation.state?.rotated_at || '') || null;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
117
|
+
metrics.generationStatus = 'invalid';
|
|
118
|
+
metrics.scaleErrorCode ||= safeReason(error?.code, 'generation-read-failed');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const journal = readMemoryRotationJournal(vaultBase);
|
|
123
|
+
metrics.rotationJournal = journal.status === 'ok'
|
|
124
|
+
? String(journal.journal?.stage || 'invalid')
|
|
125
|
+
: journal.status;
|
|
126
|
+
metrics.rotationRecoveryRequired = journal.status !== 'missing';
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
129
|
+
metrics.rotationJournal = 'invalid';
|
|
130
|
+
metrics.rotationRecoveryRequired = true;
|
|
131
|
+
metrics.scaleErrorCode ||= safeReason(error?.code, 'rotation-journal-read-failed');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const receipts = readMemoryRotationReceipts(vaultBase);
|
|
136
|
+
metrics.rotationReceiptsStatus = receipts.status;
|
|
137
|
+
metrics.rotationReceipts = Array.isArray(receipts.receipts)
|
|
138
|
+
? receipts.receipts.length
|
|
139
|
+
: 0;
|
|
140
|
+
metrics.rotationReceiptCheckpoint = String(receipts.checkpointStatus || 'unknown');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
143
|
+
metrics.rotationReceiptsStatus = 'invalid';
|
|
144
|
+
metrics.rotationReceiptCheckpoint = 'invalid';
|
|
145
|
+
metrics.scaleErrorCode ||= safeReason(error?.code, 'rotation-receipts-read-failed');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function deriveScaleStatus(metrics) {
|
|
150
|
+
const invalid = [
|
|
151
|
+
metrics.snapshotStatus,
|
|
152
|
+
metrics.segmentStatus,
|
|
153
|
+
metrics.generationStatus,
|
|
154
|
+
metrics.rotationJournal,
|
|
155
|
+
metrics.rotationReceiptsStatus,
|
|
156
|
+
metrics.rotationReceiptCheckpoint,
|
|
157
|
+
].some((status) => ['invalid', 'corrupt', 'blocked'].includes(status));
|
|
158
|
+
if (invalid || metrics.scaleErrorCode) return 'degraded';
|
|
159
|
+
if (metrics.rotationRecoveryRequired) return 'warning';
|
|
160
|
+
return 'healthy';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function inspectMemoryScaleHealth(vaultBase, { ledgerEvents = 0 } = {}) {
|
|
164
|
+
const metrics = emptyMemoryScaleMetrics();
|
|
165
|
+
inspectSnapshot(vaultBase, metrics);
|
|
166
|
+
inspectSegments(vaultBase, ledgerEvents, metrics);
|
|
167
|
+
inspectRotation(vaultBase, ledgerEvents, metrics);
|
|
168
|
+
metrics.scaleStatus = deriveScaleStatus(metrics);
|
|
169
|
+
return metrics;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function statusCommand(vaultBase) {
|
|
173
|
+
return `${WENDKEEP_COMMAND} memory status --gate --vault ${quoteCommandArgument(vaultBase)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function augmentVaultHealthWithMemoryScale(result, vaultBase) {
|
|
177
|
+
const memory = result?.metrics?.memory;
|
|
178
|
+
if (!memory || ['legacy', 'blocked'].includes(result.memoryStatus)) return result;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const scale = inspectMemoryScaleHealth(vaultBase, {
|
|
182
|
+
ledgerEvents: memory.ledgerEvents,
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
...result,
|
|
186
|
+
metrics: {
|
|
187
|
+
...result.metrics,
|
|
188
|
+
memory: { ...memory, ...scale },
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const code = safeReason(error?.code, 'memory-scale-boundary-unsafe');
|
|
193
|
+
const failure = `Memória: Artefatos de escala da memória estão inseguros ou ilegíveis (${code}). Inspecione com: ${statusCommand(vaultBase)}.`;
|
|
194
|
+
return {
|
|
195
|
+
...result,
|
|
196
|
+
ok: false,
|
|
197
|
+
memoryStatus: 'blocked',
|
|
198
|
+
failures: [...(result.failures || []), failure],
|
|
199
|
+
metrics: {
|
|
200
|
+
...result.metrics,
|
|
201
|
+
memory: {
|
|
202
|
+
...memory,
|
|
203
|
+
...emptyMemoryScaleMetrics(),
|
|
204
|
+
scaleStatus: 'blocked',
|
|
205
|
+
scaleErrorCode: code,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -5,6 +5,8 @@ import { allChangesState } from '../hooks/change-core.mjs';
|
|
|
5
5
|
import { readControl, readSessionRegistry } from '../hooks/obsidian-common.mjs';
|
|
6
6
|
import { runVaultHealth } from '../hooks/vault-health.mjs';
|
|
7
7
|
import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
|
|
8
|
+
import { inspectEvidenceSearchHealth } from './evidence-search-health.mjs';
|
|
9
|
+
import { augmentVaultHealthWithMemoryScale } from './memory-scale-health.mjs';
|
|
8
10
|
import { inspectSyncOutbox, readLocalSyncState } from './sync-outbox.mjs';
|
|
9
11
|
|
|
10
12
|
export const OBSERVER_SCHEMA_VERSION = 1;
|
|
@@ -27,6 +29,11 @@ function safeText(value, max = MAX_TEXT) {
|
|
|
27
29
|
.slice(0, max);
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function safeCount(value) {
|
|
33
|
+
const number = Number(value || 0);
|
|
34
|
+
return Number.isSafeInteger(number) && number >= 0 ? number : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
function isoNow(value) {
|
|
31
38
|
const date = value instanceof Date ? value : new Date(value ?? Date.now());
|
|
32
39
|
if (Number.isNaN(date.getTime())) throw fail('captured_at inválido.');
|
|
@@ -65,9 +72,86 @@ function activeSessionSummary(vaultBase, control, registry) {
|
|
|
65
72
|
};
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
function recallSearchSummary(metrics) {
|
|
76
|
+
return {
|
|
77
|
+
schema_version: 1,
|
|
78
|
+
status: safeText(metrics?.status || 'unknown', 32),
|
|
79
|
+
...(metrics?.errorCode ? { error_code: safeText(metrics.errorCode, 120) } : {}),
|
|
80
|
+
authority: {
|
|
81
|
+
status: safeText(metrics?.authorityStatus || 'unknown', 32),
|
|
82
|
+
bytes: safeCount(metrics?.authorityBytes),
|
|
83
|
+
},
|
|
84
|
+
incremental: {
|
|
85
|
+
status: safeText(metrics?.incrementalStateStatus || 'unknown', 32),
|
|
86
|
+
bytes: safeCount(metrics?.incrementalStateBytes),
|
|
87
|
+
documents: safeCount(metrics?.documentCount),
|
|
88
|
+
},
|
|
89
|
+
search: {
|
|
90
|
+
status: safeText(metrics?.searchStateStatus || 'unknown', 32),
|
|
91
|
+
bytes: safeCount(metrics?.searchStateBytes),
|
|
92
|
+
chunks: safeCount(metrics?.rowCount),
|
|
93
|
+
source_index_current: metrics?.sourceIndexCurrent === true,
|
|
94
|
+
source_state_current: metrics?.sourceStateCurrent === true,
|
|
95
|
+
},
|
|
96
|
+
lexical: {
|
|
97
|
+
status: safeText(metrics?.lexicalStatus || 'unknown', 32),
|
|
98
|
+
bytes: safeCount(metrics?.lexicalBytes),
|
|
99
|
+
},
|
|
100
|
+
sqlite: {
|
|
101
|
+
status: safeText(metrics?.sqliteStatus || 'unknown', 32),
|
|
102
|
+
bytes: safeCount(metrics?.sqliteBytes),
|
|
103
|
+
capability: metrics?.sqliteCapability === true,
|
|
104
|
+
},
|
|
105
|
+
backend: safeText(metrics?.backend || 'unavailable', 40),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function memoryScaleSummary(memory = {}) {
|
|
110
|
+
if (memory.scaleSchemaVersion !== 1) return null;
|
|
111
|
+
return {
|
|
112
|
+
schema_version: 1,
|
|
113
|
+
status: safeText(memory.scaleStatus || 'unknown', 32),
|
|
114
|
+
...(memory.scaleErrorCode ? { error_code: safeText(memory.scaleErrorCode, 120) } : {}),
|
|
115
|
+
snapshot: {
|
|
116
|
+
status: safeText(memory.snapshotStatus || 'unknown', 32),
|
|
117
|
+
...(memory.snapshotReason ? { reason: safeText(memory.snapshotReason, 120) } : {}),
|
|
118
|
+
event_count: safeCount(memory.snapshotEvents),
|
|
119
|
+
ledger_bytes: safeCount(memory.snapshotLedgerBytes),
|
|
120
|
+
tail_events: safeCount(memory.snapshotTailEvents),
|
|
121
|
+
tail_bytes: safeCount(memory.snapshotTailBytes),
|
|
122
|
+
},
|
|
123
|
+
segments: {
|
|
124
|
+
status: safeText(memory.segmentStatus || 'unknown', 32),
|
|
125
|
+
count: safeCount(memory.segmentCount),
|
|
126
|
+
covered_events: safeCount(memory.segmentCoveredEvents),
|
|
127
|
+
covered_bytes: safeCount(memory.segmentCoveredBytes),
|
|
128
|
+
pending_events: safeCount(memory.segmentPendingEvents),
|
|
129
|
+
},
|
|
130
|
+
generation: {
|
|
131
|
+
status: safeText(memory.generationStatus || 'unknown', 32),
|
|
132
|
+
number: safeCount(memory.generation),
|
|
133
|
+
source_events: safeCount(memory.generationSourceEvents),
|
|
134
|
+
active_tail_events: safeCount(memory.generationActiveTailEvents),
|
|
135
|
+
rotated_at: safeText(memory.generationRotatedAt || '', 40),
|
|
136
|
+
},
|
|
137
|
+
rotation: {
|
|
138
|
+
journal: safeText(memory.rotationJournal || 'unknown', 32),
|
|
139
|
+
recovery_required: memory.rotationRecoveryRequired === true,
|
|
140
|
+
receipts_status: safeText(memory.rotationReceiptsStatus || 'unknown', 32),
|
|
141
|
+
receipts: safeCount(memory.rotationReceipts),
|
|
142
|
+
receipt_checkpoint: safeText(memory.rotationReceiptCheckpoint || 'unknown', 32),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
68
147
|
function healthSummary(vaultBase) {
|
|
69
148
|
try {
|
|
70
|
-
const health =
|
|
149
|
+
const health = augmentVaultHealthWithMemoryScale(
|
|
150
|
+
runVaultHealth({ vaultBase }),
|
|
151
|
+
vaultBase,
|
|
152
|
+
);
|
|
153
|
+
const recall = recallSearchSummary(inspectEvidenceSearchHealth(vaultBase));
|
|
154
|
+
const memoryScale = memoryScaleSummary(health.metrics?.memory);
|
|
71
155
|
return {
|
|
72
156
|
ok: health.ok === true,
|
|
73
157
|
status: safeText(health.memoryStatus || (health.ok ? 'healthy' : 'degraded'), 40),
|
|
@@ -75,6 +159,8 @@ function healthSummary(vaultBase) {
|
|
|
75
159
|
warning_count: Array.isArray(health.warnings) ? health.warnings.length : 0,
|
|
76
160
|
registry_sessions: Number(health.metrics?.registrySessions || 0),
|
|
77
161
|
derived_notes: Number(health.metrics?.derivedNotes || 0),
|
|
162
|
+
recall_search: recall,
|
|
163
|
+
...(memoryScale ? { memory_scale: memoryScale } : {}),
|
|
78
164
|
};
|
|
79
165
|
} catch {
|
|
80
166
|
return {
|