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.
Files changed (31) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.en.md +2 -1
  3. package/README.md +2 -1
  4. package/docs/en/commands/evidence-embeddings.md +243 -0
  5. package/docs/en/commands/mcp.md +67 -7
  6. package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
  7. package/docs/pt-BR/commands/mcp.md +66 -7
  8. package/hooks/evidence-context.mjs +41 -7
  9. package/hooks/evidence-recall.mjs +10 -0
  10. package/package.json +1 -1
  11. package/packages/mcp/src/effects.mjs +3 -2
  12. package/packages/mcp/src/evidence-recall.mjs +130 -0
  13. package/packages/mcp/src/executor.mjs +4 -0
  14. package/packages/mcp/src/server.mjs +31 -1
  15. package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
  16. package/packages/vault/src/evidence-index-store.mjs +360 -0
  17. package/packages/vault/src/evidence-recall-page.mjs +381 -0
  18. package/packages/vault/src/evidence-search-index.mjs +917 -0
  19. package/packages/vault/src/index.mjs +12 -1
  20. package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
  21. package/packages/vault/src/memory-ledger-view.mjs +41 -0
  22. package/packages/vault/src/memory-rotation-store.mjs +967 -0
  23. package/packages/vault/src/memory-segment-store.mjs +820 -0
  24. package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
  25. package/packages/vault/src/memory-store-base.mjs +1161 -0
  26. package/packages/vault/src/memory-store-core.mjs +2 -0
  27. package/packages/vault/src/memory-store.mjs +46 -1161
  28. package/src/doctor.mjs +41 -5
  29. package/src/evidence-search-health.mjs +221 -0
  30. package/src/memory-scale-health.mjs +210 -0
  31. 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 = runVaultHealth({ vaultBase, session });
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
- if (scope !== 'runtime') process.stdout.write(`${renderVaultHealthLines(health).join('\n')}\n`);
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 = runVaultHealth({ vaultBase });
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 {