wendkeep 0.85.1 → 0.87.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 (49) hide show
  1. package/.githooks/commit-msg +16 -0
  2. package/.githooks/prepare-commit-msg +16 -0
  3. package/CHANGELOG.md +40 -0
  4. package/README.en.md +4 -1
  5. package/README.md +4 -1
  6. package/docs/en/commands/commit.md +159 -0
  7. package/docs/en/commands/evidence-embeddings.md +243 -0
  8. package/docs/en/commands/mcp.md +67 -7
  9. package/docs/pt-BR/commands/commit.md +159 -0
  10. package/docs/pt-BR/commands/evidence-embeddings.md +244 -0
  11. package/docs/pt-BR/commands/mcp.md +66 -7
  12. package/hooks/evidence-context.mjs +41 -7
  13. package/hooks/evidence-recall.mjs +10 -0
  14. package/package.json +5 -2
  15. package/packages/cli/src/index.mjs +11 -1
  16. package/packages/commit/package.json +6 -0
  17. package/packages/commit/src/cli.mjs +89 -0
  18. package/packages/commit/src/commit-input.mjs +181 -0
  19. package/packages/commit/src/commit-message.mjs +51 -0
  20. package/packages/commit/src/commit-policy.mjs +144 -0
  21. package/packages/commit/src/git-runtime.mjs +428 -0
  22. package/packages/commit/src/index.mjs +28 -0
  23. package/packages/commit/src/proof-validation.mjs +443 -0
  24. package/packages/mcp/src/effects.mjs +3 -2
  25. package/packages/mcp/src/evidence-recall.mjs +130 -0
  26. package/packages/mcp/src/executor.mjs +4 -0
  27. package/packages/mcp/src/server.mjs +31 -1
  28. package/packages/vault/src/evidence-embedding-plugin.mjs +531 -0
  29. package/packages/vault/src/evidence-index-store.mjs +360 -0
  30. package/packages/vault/src/evidence-recall-page.mjs +381 -0
  31. package/packages/vault/src/evidence-search-index.mjs +917 -0
  32. package/packages/vault/src/index.mjs +12 -1
  33. package/packages/vault/src/memory-ledger-view-base.mjs +545 -0
  34. package/packages/vault/src/memory-ledger-view.mjs +41 -0
  35. package/packages/vault/src/memory-rotation-store.mjs +967 -0
  36. package/packages/vault/src/memory-segment-store.mjs +820 -0
  37. package/packages/vault/src/memory-snapshot-store.mjs +1105 -0
  38. package/packages/vault/src/memory-store-base.mjs +1161 -0
  39. package/packages/vault/src/memory-store-core.mjs +2 -0
  40. package/packages/vault/src/memory-store.mjs +46 -1161
  41. package/schema/commit-message-v1.schema.json +75 -0
  42. package/scripts/validate-commit-range.mjs +244 -0
  43. package/src/doctor.mjs +48 -5
  44. package/src/evidence-search-health.mjs +221 -0
  45. package/src/git-commit-hooks.mjs +112 -0
  46. package/src/init.mjs +13 -0
  47. package/src/memory-scale-health.mjs +210 -0
  48. package/src/observer-snapshot.mjs +87 -1
  49. package/src/skills-seed.mjs +79 -0
@@ -0,0 +1,1105 @@
1
+ import { createHash } from 'node:crypto';
2
+ import {
3
+ closeSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, readdirSync,
4
+ statSync, writeFileSync,
5
+ } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ import {
9
+ MEMORY_LOCK_BUSY,
10
+ MemoryEventCollision,
11
+ MemoryLedgerCorruption,
12
+ MemoryOutboxCorruption,
13
+ canonicalMemoryJson,
14
+ deriveMemoryProjection,
15
+ hashMemoryValue,
16
+ memoryFileIdentityMatches,
17
+ publishMemoryProjection,
18
+ readMemoryLedger,
19
+ withMemoryLock,
20
+ } from './memory-store.mjs';
21
+ import {
22
+ parseSharedMemory,
23
+ renderSharedMemory,
24
+ validateMemoryEvent,
25
+ } from './memory-schema.mjs';
26
+ import {
27
+ isRegisterMemoryKey,
28
+ memoryRecordKey,
29
+ sameMemoryScope,
30
+ } from './memory-scope.mjs';
31
+ import {
32
+ assertVaultPathSafe,
33
+ assertVaultPathsSafe,
34
+ mkdirVaultPath,
35
+ unlinkVaultFile,
36
+ writeVaultFileAtomic,
37
+ } from './vault-path-safety.mjs';
38
+
39
+ export const MEMORY_SNAPSHOT_FILE = 'MEMORY_SNAPSHOT.json';
40
+ export const MEMORY_SNAPSHOT_SCHEMA_VERSION = 1;
41
+ export const MEMORY_SNAPSHOT_REDUCER_VERSION = 1;
42
+
43
+ const EVENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
44
+ const SHA256 = /^[a-f0-9]{64}$/;
45
+ const SNAPSHOT_BLOOM_BYTES = 512 * 1024;
46
+ const SNAPSHOT_BLOOM_HASHES = 7;
47
+ const SNAPSHOT_ADVANCE_EVENTS = 128;
48
+ const SNAPSHOT_ADVANCE_BYTES = 1024 * 1024;
49
+ const SNAPSHOT_MAX_TAIL_BYTES = 16 * 1024 * 1024;
50
+ const SNAPSHOT_MAX_LINE_BYTES = 8 * 1024 * 1024;
51
+ const CHAIN_GENESIS = '0'.repeat(64);
52
+
53
+ function brainDir(vaultBase) { return join(vaultBase, '.brain'); }
54
+ function outboxDir(vaultBase) { return join(brainDir(vaultBase), 'memory-outbox'); }
55
+ function ledgerPath(vaultBase) { return join(brainDir(vaultBase), 'MEMORY_EVENTS.jsonl'); }
56
+ function sharedPath(vaultBase) { return join(brainDir(vaultBase), 'SHARED_MEMORY.md'); }
57
+ function candidatesPath(vaultBase) { return join(brainDir(vaultBase), 'MEMORY_CANDIDATES.jsonl'); }
58
+ function snapshotPath(vaultBase) { return join(brainDir(vaultBase), MEMORY_SNAPSHOT_FILE); }
59
+ function corePath(vaultBase) { return join(brainDir(vaultBase), 'CORE.md'); }
60
+ function projectPath(vaultBase) { return join(brainDir(vaultBase), 'PROJECT.json'); }
61
+
62
+ function sha256(value) {
63
+ return createHash('sha256').update(value).digest('hex');
64
+ }
65
+
66
+ function canonicalize(value) {
67
+ if (Array.isArray(value)) return value.map(canonicalize);
68
+ if (value && typeof value === 'object') {
69
+ const out = {};
70
+ for (const key of Object.keys(value).sort()) {
71
+ if (value[key] !== undefined) out[key] = canonicalize(value[key]);
72
+ }
73
+ return out;
74
+ }
75
+ return value;
76
+ }
77
+
78
+ function canonicalSnapshotJson(value) {
79
+ return JSON.stringify(canonicalize(value));
80
+ }
81
+
82
+ function snapshotHash(snapshot) {
83
+ const { snapshot_hash: _ignored, ...unsigned } = snapshot || {};
84
+ return sha256(canonicalSnapshotJson(unsigned));
85
+ }
86
+
87
+ function checkedFile(vaultBase, path, label, { allowMissing = true } = {}) {
88
+ return assertVaultPathSafe(vaultBase, path, {
89
+ allowMissing,
90
+ expectedType: 'file',
91
+ label,
92
+ });
93
+ }
94
+
95
+ function checkedDirectory(vaultBase, path, label, { allowMissing = true } = {}) {
96
+ return assertVaultPathSafe(vaultBase, path, {
97
+ allowMissing,
98
+ expectedType: 'directory',
99
+ label,
100
+ });
101
+ }
102
+
103
+ function readCheckedFile(vaultBase, path, encoding, label, { allowMissing = false } = {}) {
104
+ let checked = checkedFile(vaultBase, path, label, { allowMissing });
105
+ if (!checked.exists) return null;
106
+ checked = checkedFile(vaultBase, checked.target, label, { allowMissing: false });
107
+ return readFileSync(checked.target, encoding);
108
+ }
109
+
110
+ function projectIdForVault(vaultBase) {
111
+ const raw = readCheckedFile(
112
+ vaultBase, projectPath(vaultBase), 'utf8', 'autoridade PROJECT.json do snapshot de memória',
113
+ { allowMissing: true },
114
+ );
115
+ if (raw === null) return '';
116
+ try {
117
+ const project = JSON.parse(raw);
118
+ return typeof project.projectId === 'string' ? project.projectId : '';
119
+ } catch {
120
+ return '';
121
+ }
122
+ }
123
+
124
+ function projectIdForEvents(events, fallback = '') {
125
+ const values = new Set(events.map((event) => event?.project_id).filter(Boolean));
126
+ return values.size === 1 ? [...values][0] : fallback;
127
+ }
128
+
129
+ function readCoreAuthority(vaultBase) {
130
+ const raw = readCheckedFile(
131
+ vaultBase, corePath(vaultBase), 'utf8', 'autoridade CORE.md do snapshot de memória',
132
+ { allowMissing: true },
133
+ ) ?? '';
134
+ const invariants = new Map();
135
+ const marker = /^<!--\s*wk-memory:\s*([A-Za-z0-9][A-Za-z0-9._-]*)=(.+)\s*-->$/;
136
+ for (const line of raw.split('\n')) {
137
+ const match = line.trim().match(marker);
138
+ if (!match) continue;
139
+ try { invariants.set(match[1], JSON.parse(match[2].trim())); } catch { /* prose only */ }
140
+ }
141
+ return { raw, hash: sha256(raw), invariants };
142
+ }
143
+
144
+ function assertOpenedFile(vaultBase, path, fd, label) {
145
+ const checked = checkedFile(vaultBase, path, label, { allowMissing: false });
146
+ const descriptor = fstatSync(fd, { bigint: true });
147
+ const target = statSync(checked.target, { bigint: true });
148
+ if (!descriptor.isFile() || descriptor.nlink > 1n || target.nlink > 1n
149
+ || !memoryFileIdentityMatches(descriptor, target)) {
150
+ const error = new Error(`${label} mudou de inode ou possui hardlink antes da leitura/mutação.`);
151
+ error.code = 'VAULT_PATH_UNSAFE';
152
+ throw error;
153
+ }
154
+ return checked.target;
155
+ }
156
+
157
+ function readRange(fd, offset, length) {
158
+ const buffer = Buffer.alloc(length);
159
+ let cursor = 0;
160
+ while (cursor < length) {
161
+ const count = readSync(fd, buffer, cursor, length - cursor, offset + cursor);
162
+ if (count === 0) break;
163
+ cursor += count;
164
+ }
165
+ return cursor === length ? buffer : buffer.subarray(0, cursor);
166
+ }
167
+
168
+ function eventOrderTuple(event = {}) {
169
+ return {
170
+ base_revision: Number(event.base_revision ?? 0),
171
+ effective_at: String(event.effective_at || event.observed_at || ''),
172
+ turn_sequence: Number(event.turn_sequence ?? 0),
173
+ event_id: String(event.event_id || ''),
174
+ };
175
+ }
176
+
177
+ function compareOrderTuple(left, right) {
178
+ return Number(left.base_revision ?? 0) - Number(right.base_revision ?? 0)
179
+ || String(left.effective_at || '').localeCompare(String(right.effective_at || ''))
180
+ || Number(left.turn_sequence ?? 0) - Number(right.turn_sequence ?? 0)
181
+ || String(left.event_id || '').localeCompare(String(right.event_id || ''));
182
+ }
183
+
184
+ function maximumOrder(events) {
185
+ if (!events.length) return eventOrderTuple();
186
+ return events.map(eventOrderTuple).sort(compareOrderTuple).at(-1);
187
+ }
188
+
189
+ function chainStep(previous, event) {
190
+ return sha256(`${previous}\u0000${canonicalMemoryJson(event)}`);
191
+ }
192
+
193
+ function chainForEvents(events, initial = CHAIN_GENESIS) {
194
+ return events.reduce((chain, event) => chainStep(chain, event), initial);
195
+ }
196
+
197
+ function bloomIndexes(eventId, byteLength = SNAPSHOT_BLOOM_BYTES) {
198
+ const digest = createHash('sha256').update(String(eventId)).digest();
199
+ const bits = BigInt(byteLength * 8);
200
+ const first = digest.readBigUInt64BE(0);
201
+ const second = digest.readBigUInt64BE(8) | 1n;
202
+ return Array.from({ length: SNAPSHOT_BLOOM_HASHES }, (_, index) => (
203
+ Number((first + BigInt(index) * second + BigInt(index * index)) % bits)
204
+ ));
205
+ }
206
+
207
+ function bloomAdd(buffer, eventId) {
208
+ for (const index of bloomIndexes(eventId, buffer.length)) {
209
+ buffer[Math.floor(index / 8)] |= 1 << (index % 8);
210
+ }
211
+ }
212
+
213
+ function bloomMayContain(buffer, eventId) {
214
+ return bloomIndexes(eventId, buffer.length).every((index) => (
215
+ (buffer[Math.floor(index / 8)] & (1 << (index % 8))) !== 0
216
+ ));
217
+ }
218
+
219
+ function bloomForEvents(events) {
220
+ const bloom = Buffer.alloc(SNAPSHOT_BLOOM_BYTES);
221
+ for (const event of events) bloomAdd(bloom, event.event_id);
222
+ return bloom;
223
+ }
224
+
225
+ function validPlainObject(value) {
226
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
227
+ }
228
+
229
+ function sortedObject(entries) {
230
+ return Object.fromEntries([...entries].sort(([left], [right]) => left.localeCompare(right)));
231
+ }
232
+
233
+ function stateFromRecords(records) {
234
+ return sortedObject(Object.entries(records).map(([key, record]) => [key, record.value]));
235
+ }
236
+
237
+ function stateHashFor(records, tombstones) {
238
+ return hashMemoryValue({
239
+ state: stateFromRecords(records),
240
+ tombstones: sortedObject(Object.entries(tombstones)),
241
+ });
242
+ }
243
+
244
+ function validSnapshotRecord(record, key, projectId) {
245
+ if (!validPlainObject(record) || !Number.isInteger(record.revision) || record.revision < 1
246
+ || !Object.hasOwn(record, 'value') || !validPlainObject(record.source)) return false;
247
+ const validation = validateMemoryEvent(record.source, projectId ? { projectId } : {});
248
+ return validation.ok && memoryRecordKey(record.source) === key;
249
+ }
250
+
251
+ function decodeBloom(snapshot) {
252
+ if (!validPlainObject(snapshot.bloom)
253
+ || snapshot.bloom.bytes !== SNAPSHOT_BLOOM_BYTES
254
+ || snapshot.bloom.hashes !== SNAPSHOT_BLOOM_HASHES
255
+ || typeof snapshot.bloom.data !== 'string') return null;
256
+ try {
257
+ const buffer = Buffer.from(snapshot.bloom.data, 'base64');
258
+ return buffer.length === SNAPSHOT_BLOOM_BYTES ? buffer : null;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ function validateSnapshot(snapshot, { projectId, coreHash } = {}) {
265
+ if (!validPlainObject(snapshot)
266
+ || snapshot.schema_version !== MEMORY_SNAPSHOT_SCHEMA_VERSION
267
+ || snapshot.reducer_version !== MEMORY_SNAPSHOT_REDUCER_VERSION
268
+ || typeof snapshot.project_id !== 'string'
269
+ || (projectId && snapshot.project_id !== projectId)
270
+ || snapshot.core_hash !== coreHash
271
+ || !Number.isInteger(snapshot.event_count) || snapshot.event_count < 0
272
+ || !Number.isInteger(snapshot.ledger_bytes) || snapshot.ledger_bytes < 0
273
+ || !Number.isInteger(snapshot.through_line_start) || snapshot.through_line_start < 0
274
+ || !Number.isInteger(snapshot.through_line_length) || snapshot.through_line_length < 0
275
+ || typeof snapshot.through_event_id !== 'string'
276
+ || !SHA256.test(String(snapshot.through_line_hash || ''))
277
+ || !SHA256.test(String(snapshot.chain_hash || ''))
278
+ || !SHA256.test(String(snapshot.snapshot_hash || ''))
279
+ || snapshot.snapshot_hash !== snapshotHash(snapshot)
280
+ || !validPlainObject(snapshot.order_cursor)
281
+ || !validPlainObject(snapshot.projection)
282
+ || !validPlainObject(snapshot.projection.records)
283
+ || !validPlainObject(snapshot.projection.tombstones)
284
+ || !Number.isInteger(snapshot.projection.revision) || snapshot.projection.revision < 0
285
+ || !SHA256.test(String(snapshot.projection.state_hash || ''))
286
+ || typeof snapshot.projection.event_cursor !== 'string'
287
+ || typeof snapshot.projection.updated_at !== 'string') {
288
+ return { ok: false, reason: 'snapshot-schema' };
289
+ }
290
+ if (snapshot.event_count === 0) {
291
+ if (snapshot.ledger_bytes !== 0 || snapshot.through_event_id !== 'none'
292
+ || snapshot.through_line_start !== 0 || snapshot.through_line_length !== 0) {
293
+ return { ok: false, reason: 'snapshot-empty-boundary' };
294
+ }
295
+ } else if (!EVENT_ID.test(snapshot.through_event_id) || snapshot.through_line_length === 0) {
296
+ return { ok: false, reason: 'snapshot-boundary' };
297
+ }
298
+ for (const [key, record] of Object.entries(snapshot.projection.records)) {
299
+ if (!validSnapshotRecord(record, key, snapshot.project_id || projectId)) {
300
+ return { ok: false, reason: 'snapshot-record' };
301
+ }
302
+ }
303
+ if (stateHashFor(snapshot.projection.records, snapshot.projection.tombstones)
304
+ !== snapshot.projection.state_hash) {
305
+ return { ok: false, reason: 'snapshot-state-hash' };
306
+ }
307
+ const bloom = decodeBloom(snapshot);
308
+ return bloom ? { ok: true, bloom } : { ok: false, reason: 'snapshot-bloom' };
309
+ }
310
+
311
+ function readSnapshotDocument(vaultBase, coreHash, projectId) {
312
+ let raw;
313
+ try {
314
+ raw = readCheckedFile(
315
+ vaultBase, snapshotPath(vaultBase), 'utf8', 'snapshot incremental de memória',
316
+ { allowMissing: true },
317
+ );
318
+ } catch (error) {
319
+ if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
320
+ return { status: 'invalid', reason: 'snapshot-unreadable' };
321
+ }
322
+ if (raw === null) return { status: 'missing', reason: 'snapshot-missing' };
323
+ let snapshot;
324
+ try { snapshot = JSON.parse(raw); } catch { return { status: 'invalid', reason: 'snapshot-json' }; }
325
+ const validation = validateSnapshot(snapshot, { projectId, coreHash });
326
+ return validation.ok
327
+ ? { status: 'ok', snapshot, bloom: validation.bloom }
328
+ : { status: 'invalid', reason: validation.reason };
329
+ }
330
+
331
+ function parseLedgerEvent(line, projectId) {
332
+ const parsed = JSON.parse(line);
333
+ const validation = validateMemoryEvent(parsed, projectId ? { projectId } : {});
334
+ if (!validation.ok || !EVENT_ID.test(String(parsed.event_id || ''))) {
335
+ const error = new Error(validation.errors?.join(' ') || 'event_id inválido');
336
+ error.code = 'MEMORY_EVENT_INVALID';
337
+ throw error;
338
+ }
339
+ return parsed;
340
+ }
341
+
342
+ function readSnapshotTail(vaultBase, snapshot, projectId) {
343
+ const path = ledgerPath(vaultBase);
344
+ let checked = checkedFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl do snapshot', {
345
+ allowMissing: snapshot.ledger_bytes === 0,
346
+ });
347
+ if (!checked.exists) {
348
+ return snapshot.ledger_bytes === 0
349
+ ? { status: 'ok', path, events: [], bytes: 0, size: 0 }
350
+ : { status: 'invalid', reason: 'ledger-missing' };
351
+ }
352
+ checked = checkedFile(vaultBase, checked.target, 'ledger MEMORY_EVENTS.jsonl do snapshot', {
353
+ allowMissing: false,
354
+ });
355
+ let fd;
356
+ try {
357
+ fd = openSync(checked.target, 'r');
358
+ assertOpenedFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl do snapshot');
359
+ const stat = fstatSync(fd);
360
+ if (stat.size < snapshot.ledger_bytes) return { status: 'invalid', reason: 'ledger-truncated' };
361
+ if (snapshot.event_count > 0) {
362
+ if (snapshot.through_line_start + snapshot.through_line_length + 1 !== snapshot.ledger_bytes) {
363
+ return { status: 'invalid', reason: 'snapshot-offset' };
364
+ }
365
+ const boundary = readRange(fd, snapshot.through_line_start, snapshot.through_line_length + 1);
366
+ if (boundary.length !== snapshot.through_line_length + 1
367
+ || boundary.at(-1) !== 0x0a
368
+ || sha256(boundary.subarray(0, -1)) !== snapshot.through_line_hash) {
369
+ return { status: 'invalid', reason: 'ledger-boundary-hash' };
370
+ }
371
+ try {
372
+ const event = parseLedgerEvent(
373
+ boundary.subarray(0, -1).toString('utf8'), projectId || snapshot.project_id,
374
+ );
375
+ if (event.event_id !== snapshot.through_event_id) {
376
+ return { status: 'invalid', reason: 'ledger-boundary-event' };
377
+ }
378
+ } catch {
379
+ return { status: 'invalid', reason: 'ledger-boundary-event' };
380
+ }
381
+ }
382
+ const tailBytes = stat.size - snapshot.ledger_bytes;
383
+ if (tailBytes > SNAPSHOT_MAX_TAIL_BYTES) return { status: 'invalid', reason: 'tail-too-large' };
384
+ if (tailBytes === 0) {
385
+ return { status: 'ok', path: checked.target, events: [], bytes: 0, size: stat.size };
386
+ }
387
+ const tail = readRange(fd, snapshot.ledger_bytes, tailBytes);
388
+ if (tail.length !== tailBytes || tail.at(-1) !== 0x0a) {
389
+ return { status: 'corrupt', reason: 'tail-partial' };
390
+ }
391
+ const lines = tail.toString('utf8').split('\n');
392
+ lines.pop();
393
+ const events = [];
394
+ const ids = new Set();
395
+ try {
396
+ for (const line of lines) {
397
+ if (!line.trim()) return { status: 'corrupt', reason: 'tail-blank-line' };
398
+ const event = parseLedgerEvent(line, projectId || snapshot.project_id);
399
+ if (ids.has(event.event_id)) return { status: 'corrupt', reason: 'tail-duplicate' };
400
+ ids.add(event.event_id);
401
+ events.push(event);
402
+ }
403
+ } catch {
404
+ return { status: 'corrupt', reason: 'tail-invalid-event' };
405
+ }
406
+ return { status: 'ok', path: checked.target, events, bytes: tailBytes, size: stat.size };
407
+ } finally {
408
+ if (fd !== undefined) closeSync(fd);
409
+ }
410
+ }
411
+
412
+ function readOutbox(vaultBase, projectId) {
413
+ const dir = outboxDir(vaultBase);
414
+ let checked = checkedDirectory(vaultBase, dir, 'outbox de memória do runtime incremental');
415
+ if (!checked.exists) return [];
416
+ checked = checkedDirectory(vaultBase, checked.target, 'outbox de memória do runtime incremental', {
417
+ allowMissing: false,
418
+ });
419
+ const entries = readdirSync(checked.target, { withFileTypes: true });
420
+ for (const entry of entries) {
421
+ assertVaultPathSafe(vaultBase, join(checked.target, entry.name), {
422
+ allowMissing: false,
423
+ label: `entrada ${entry.name} do outbox de memória`,
424
+ });
425
+ }
426
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
427
+ .sort((left, right) => left.name.localeCompare(right.name))
428
+ .map((entry) => {
429
+ const path = join(checked.target, entry.name);
430
+ try {
431
+ const event = parseLedgerEvent(readCheckedFile(
432
+ vaultBase, path, 'utf8', `evento ${entry.name} do outbox de memória`,
433
+ ), projectId);
434
+ if (`${event.event_id}.json` !== entry.name) throw new Error('filename does not match event_id');
435
+ return { event, path };
436
+ } catch (error) {
437
+ if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
438
+ throw new MemoryOutboxCorruption(path, error);
439
+ }
440
+ });
441
+ }
442
+
443
+ function countPendingOutbox(vaultBase) {
444
+ const dir = outboxDir(vaultBase);
445
+ let checked = checkedDirectory(vaultBase, dir, 'outbox de memória do runtime incremental');
446
+ if (!checked.exists) return 0;
447
+ checked = checkedDirectory(vaultBase, checked.target, 'outbox de memória do runtime incremental', {
448
+ allowMissing: false,
449
+ });
450
+ return readdirSync(checked.target, { withFileTypes: true })
451
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json')).length;
452
+ }
453
+
454
+ function appendLedgerDurably(vaultBase, events) {
455
+ if (!events.length) return { bytes: 0 };
456
+ const path = ledgerPath(vaultBase);
457
+ let fd;
458
+ const payload = `${events.map((event) => canonicalMemoryJson(event)).join('\n')}\n`;
459
+ try {
460
+ let checked = checkedFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl incremental');
461
+ checked = checkedFile(vaultBase, checked.target, 'ledger MEMORY_EVENTS.jsonl incremental');
462
+ fd = openSync(checked.target, 'a');
463
+ assertOpenedFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl incremental');
464
+ writeFileSync(fd, payload, 'utf8');
465
+ fsyncSync(fd);
466
+ assertOpenedFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl incremental');
467
+ } finally {
468
+ if (fd !== undefined) closeSync(fd);
469
+ }
470
+ return { bytes: Buffer.byteLength(payload) };
471
+ }
472
+
473
+ function preflightDerivedTargets(vaultBase, { snapshot = false } = {}) {
474
+ const targets = [
475
+ { path: sharedPath(vaultBase), expectedType: 'file', label: 'projeção SHARED_MEMORY.md' },
476
+ { path: candidatesPath(vaultBase), expectedType: 'file', label: 'projeção MEMORY_CANDIDATES.jsonl' },
477
+ ];
478
+ if (snapshot) {
479
+ targets.push({
480
+ path: snapshotPath(vaultBase),
481
+ expectedType: 'file',
482
+ label: 'snapshot incremental de memória',
483
+ });
484
+ }
485
+ return assertVaultPathsSafe(vaultBase, targets);
486
+ }
487
+
488
+ function sameCausalActivation(left, right) {
489
+ return Boolean(left?.canonical_session_id)
490
+ && left.canonical_session_id === right?.canonical_session_id
491
+ && left.activation_id === right?.activation_id;
492
+ }
493
+
494
+ function isCausallyOlder(event, current) {
495
+ if (!current) return false;
496
+ if (sameCausalActivation(event, current)) {
497
+ return Number(event.turn_sequence) < Number(current.turn_sequence);
498
+ }
499
+ if (event.canonical_session_id && event.canonical_session_id === current.canonical_session_id
500
+ && sameMemoryScope(event, current)
501
+ && Number.isInteger(event.activation_epoch) && Number.isInteger(current.activation_epoch)
502
+ && event.activation_epoch !== current.activation_epoch) {
503
+ return event.activation_epoch < current.activation_epoch;
504
+ }
505
+ const eventEffective = Date.parse(event.effective_at || '');
506
+ const currentEffective = Date.parse(current.effective_at || '');
507
+ return Number.isFinite(eventEffective) && Number.isFinite(currentEffective)
508
+ && eventEffective < currentEffective;
509
+ }
510
+
511
+ const AUTHORITY_RANK = Object.freeze({ candidate: 0, reported: 1, verified: 2 });
512
+
513
+ function sameRegisterLineage(left, right) {
514
+ return Boolean(left?.canonical_session_id)
515
+ && left.canonical_session_id === right?.canonical_session_id
516
+ && left.project_id === right?.project_id
517
+ && sameMemoryScope(left, right);
518
+ }
519
+
520
+ function registerPrecedence(incoming, current) {
521
+ if (!incoming?.scope || !current?.scope
522
+ || !isRegisterMemoryKey(incoming?.memory_key) || !sameRegisterLineage(incoming, current)) return null;
523
+ const epoch = Number(incoming.activation_epoch ?? -1) - Number(current.activation_epoch ?? -1);
524
+ if (epoch) return epoch;
525
+ const turn = Number(incoming.turn_sequence ?? -1) - Number(current.turn_sequence ?? -1);
526
+ if (turn) return turn;
527
+ const authority = (AUTHORITY_RANK[incoming.authority] ?? -1)
528
+ - (AUTHORITY_RANK[current.authority] ?? -1);
529
+ if (authority) return authority;
530
+ const observed = String(incoming.observed_at || '').localeCompare(String(current.observed_at || ''));
531
+ if (observed) return observed;
532
+ return String(incoming.event_id || '').localeCompare(String(current.event_id || ''));
533
+ }
534
+
535
+ function hasComplexTailSemantics(event) {
536
+ return Boolean(event.candidate_decision)
537
+ || Boolean(event.rescopes_event_id)
538
+ || (Array.isArray(event.rescopes_event_ids) && event.rescopes_event_ids.length > 0);
539
+ }
540
+
541
+ function fastApplySnapshot(snapshot, bloom, events, coreInvariants) {
542
+ const records = new Map(Object.entries(structuredClone(snapshot.projection.records)));
543
+ const tombstones = new Map(Object.entries(structuredClone(snapshot.projection.tombstones)));
544
+ let revision = snapshot.projection.revision;
545
+ let stateHash = snapshot.projection.state_hash;
546
+ let eventCursor = snapshot.projection.event_cursor;
547
+ let orderCursor = structuredClone(snapshot.order_cursor);
548
+ let chainHash = snapshot.chain_hash;
549
+ let eventCount = snapshot.event_count;
550
+ let updatedAt = snapshot.projection.updated_at;
551
+
552
+ for (const event of events) {
553
+ const tuple = eventOrderTuple(event);
554
+ if (compareOrderTuple(tuple, orderCursor) < 0) {
555
+ return { status: 'fallback', reason: 'non-monotonic-event-order' };
556
+ }
557
+ if (hasComplexTailSemantics(event)) return { status: 'fallback', reason: 'complex-tail-event' };
558
+ if (coreInvariants.has(event.memory_key)) {
559
+ const coreValue = coreInvariants.get(event.memory_key);
560
+ if (event.operation !== 'assert' || hashMemoryValue(event.value) !== hashMemoryValue(coreValue)) {
561
+ return { status: 'fallback', reason: 'core-conflict' };
562
+ }
563
+ }
564
+
565
+ const key = memoryRecordKey(event);
566
+ const current = records.get(key);
567
+ const currentSource = current?.source;
568
+ let applied = false;
569
+
570
+ if (isCausallyOlder(event, currentSource)) {
571
+ // The state stays unchanged, but the physical tail and rolling chain still advance.
572
+ } else if (event.operation === 'assert') {
573
+ if (current && hashMemoryValue(current.value) !== hashMemoryValue(event.value)) {
574
+ const precedence = registerPrecedence(event, currentSource);
575
+ if ((sameCausalActivation(event, currentSource)
576
+ && Number(event.turn_sequence) > Number(currentSource.turn_sequence))
577
+ || precedence > 0) {
578
+ records.set(key, { value: event.value, revision: current.revision + 1, source: event });
579
+ tombstones.delete(key);
580
+ revision += 1;
581
+ applied = true;
582
+ } else {
583
+ return { status: 'fallback', reason: 'assert-conflict' };
584
+ }
585
+ } else if (!current) {
586
+ records.set(key, { value: event.value, revision: 1, source: event });
587
+ tombstones.delete(key);
588
+ revision += 1;
589
+ applied = true;
590
+ }
591
+ } else if (event.operation === 'add') {
592
+ const oldValues = Array.isArray(current?.value) ? current.value : (current ? [current.value] : []);
593
+ const additions = Array.isArray(event.value) ? event.value : [event.value];
594
+ const byHash = new Map(oldValues.map((value) => [hashMemoryValue(value), value]));
595
+ additions.forEach((value) => byHash.set(hashMemoryValue(value), value));
596
+ const value = [...byHash.entries()]
597
+ .sort(([left], [right]) => left.localeCompare(right)).map(([, item]) => item);
598
+ records.set(key, { value, revision: (current?.revision || 0) + 1, source: event });
599
+ tombstones.delete(key);
600
+ revision += 1;
601
+ applied = true;
602
+ } else if (event.operation === 'remove') {
603
+ if (current) {
604
+ const baseMatches = event.base_revision === undefined
605
+ || (event.base_revision === current.revision
606
+ && (!event.base_value_hash || event.base_value_hash === hashMemoryValue(current.value)));
607
+ if (!baseMatches) return { status: 'fallback', reason: 'remove-base-mismatch' };
608
+ if (event.value !== null && event.value !== undefined && Array.isArray(current.value)) {
609
+ const removalHash = hashMemoryValue(event.value);
610
+ const value = current.value.filter((entry) => hashMemoryValue(entry) !== removalHash);
611
+ records.set(key, { value, revision: current.revision + 1, source: event });
612
+ tombstones.set(`${key}:${removalHash}`, {
613
+ event_id: event.event_id,
614
+ removed_event_id: current.source.event_id,
615
+ value_hash: removalHash,
616
+ });
617
+ } else {
618
+ records.delete(key);
619
+ tombstones.set(key, {
620
+ event_id: event.event_id,
621
+ removed_event_id: current.source.event_id,
622
+ value_hash: hashMemoryValue(current.value),
623
+ });
624
+ }
625
+ revision += 1;
626
+ applied = true;
627
+ }
628
+ } else if (event.operation === 'replace') {
629
+ if (!current) return { status: 'fallback', reason: 'replace-without-current' };
630
+ const supersedes = Array.isArray(event.supersedes)
631
+ ? event.supersedes
632
+ : (event.supersedes_event_id ? [event.supersedes_event_id] : []);
633
+ if (supersedes.some((eventId) => eventId !== currentSource?.event_id)) {
634
+ return { status: 'fallback', reason: 'replace-historical-supersedes' };
635
+ }
636
+ const explicitlySupersedes = supersedes.includes(currentSource?.event_id);
637
+ const baseMatches = event.base_revision === current.revision
638
+ && event.base_value_hash === hashMemoryValue(current.value);
639
+ if (!baseMatches && !explicitlySupersedes) {
640
+ return { status: 'fallback', reason: 'replace-base-mismatch' };
641
+ }
642
+ records.set(key, { value: event.value, revision: current.revision + 1, source: event });
643
+ tombstones.delete(key);
644
+ revision += 1;
645
+ applied = true;
646
+ }
647
+
648
+ if (applied) stateHash = stateHashFor(sortedObject(records), sortedObject(tombstones));
649
+ orderCursor = tuple;
650
+ eventCursor = event.event_id;
651
+ chainHash = chainStep(chainHash, event);
652
+ bloomAdd(bloom, event.event_id);
653
+ eventCount += 1;
654
+ if (String(event.observed_at || '') > updatedAt) updatedAt = String(event.observed_at || '');
655
+ }
656
+
657
+ const recordObject = sortedObject(records);
658
+ const tombstoneObject = sortedObject(tombstones);
659
+ stateHash = stateHashFor(recordObject, tombstoneObject);
660
+ const activeEvents = Object.entries(recordObject).map(([projectionKey, record]) => ({
661
+ ...record.source,
662
+ memory_key: record.source.memory_key,
663
+ projection_key: projectionKey,
664
+ operation: 'assert',
665
+ value: record.value,
666
+ }));
667
+ return {
668
+ status: 'ok',
669
+ records: recordObject,
670
+ tombstones: tombstoneObject,
671
+ revision,
672
+ stateHash,
673
+ eventCursor,
674
+ orderCursor,
675
+ chainHash,
676
+ eventCount,
677
+ updatedAt,
678
+ activeEvents,
679
+ bloom,
680
+ };
681
+ }
682
+
683
+ function prepareProjection({
684
+ revision, ledgerCursor, eventCursor, stateHash, activeEvents, candidates = [], updatedAt,
685
+ }) {
686
+ const sharedContent = renderSharedMemory({
687
+ revision,
688
+ eventCursor: ledgerCursor,
689
+ events: activeEvents,
690
+ stateHash,
691
+ updatedAt,
692
+ });
693
+ const metadata = parseSharedMemory(sharedContent).metadata;
694
+ const candidatesContent = candidates.map((item) => canonicalMemoryJson(item)).join('\n')
695
+ + (candidates.length ? '\n' : '');
696
+ const checkpoint = {
697
+ revision,
698
+ event_cursor: ledgerCursor,
699
+ state_hash: stateHash,
700
+ };
701
+ if (eventCursor !== ledgerCursor) checkpoint.causal_event_cursor = eventCursor;
702
+ return {
703
+ sharedContent,
704
+ candidatesContent,
705
+ revision,
706
+ eventCursor,
707
+ ledgerCursor,
708
+ stateHash,
709
+ checkpoint,
710
+ candidates: candidates.length,
711
+ projectedEvents: metadata.projected_events ?? activeEvents.length,
712
+ omittedEvents: metadata.omitted_events ?? 0,
713
+ };
714
+ }
715
+
716
+ function fullProjection(vaultBase, events) {
717
+ const reduced = deriveMemoryProjection(vaultBase, events);
718
+ const updatedAt = events.map((event) => String(event.observed_at || '')).sort().at(-1)
719
+ || new Date(0).toISOString();
720
+ const prepared = prepareProjection({
721
+ revision: reduced.revision,
722
+ ledgerCursor: reduced.ledgerCursor,
723
+ eventCursor: reduced.eventCursor,
724
+ stateHash: reduced.stateHash,
725
+ activeEvents: reduced.activeEvents,
726
+ candidates: reduced.candidates,
727
+ updatedAt,
728
+ });
729
+ return {
730
+ mode: 'full',
731
+ eventCount: events.length,
732
+ prepared,
733
+ snapshotState: reduced.candidates.length === 0 ? {
734
+ records: reduced.records,
735
+ tombstones: reduced.tombstones,
736
+ revision: reduced.revision,
737
+ stateHash: reduced.stateHash,
738
+ eventCursor: reduced.eventCursor,
739
+ orderCursor: maximumOrder(events),
740
+ chainHash: chainForEvents(events),
741
+ bloom: bloomForEvents(events),
742
+ eventCount: events.length,
743
+ updatedAt,
744
+ } : null,
745
+ };
746
+ }
747
+
748
+ function incrementalProjection(snapshot, bloom, tailEvents, newEvents, coreInvariants) {
749
+ const combined = [...tailEvents, ...newEvents];
750
+ const applied = fastApplySnapshot(snapshot, Buffer.from(bloom), combined, coreInvariants);
751
+ if (applied.status !== 'ok') return applied;
752
+ const ledgerCursor = combined.at(-1)?.event_id || snapshot.through_event_id;
753
+ return {
754
+ status: 'ok',
755
+ mode: 'snapshot-tail',
756
+ eventCount: applied.eventCount,
757
+ prepared: prepareProjection({
758
+ revision: applied.revision,
759
+ ledgerCursor,
760
+ eventCursor: applied.eventCursor,
761
+ stateHash: applied.stateHash,
762
+ activeEvents: applied.activeEvents,
763
+ updatedAt: applied.updatedAt,
764
+ }),
765
+ snapshotState: applied,
766
+ };
767
+ }
768
+
769
+ function snapshotBoundary(vaultBase) {
770
+ const path = ledgerPath(vaultBase);
771
+ let checked = checkedFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl para snapshot', {
772
+ allowMissing: true,
773
+ });
774
+ if (!checked.exists) {
775
+ return {
776
+ ledgerBytes: 0,
777
+ throughLineStart: 0,
778
+ throughLineLength: 0,
779
+ throughLineHash: sha256(Buffer.alloc(0)),
780
+ throughEventId: 'none',
781
+ };
782
+ }
783
+ checked = checkedFile(vaultBase, checked.target, 'ledger MEMORY_EVENTS.jsonl para snapshot', {
784
+ allowMissing: false,
785
+ });
786
+ let fd;
787
+ try {
788
+ fd = openSync(checked.target, 'r');
789
+ assertOpenedFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl para snapshot');
790
+ const size = fstatSync(fd).size;
791
+ if (size === 0) {
792
+ return {
793
+ ledgerBytes: 0,
794
+ throughLineStart: 0,
795
+ throughLineLength: 0,
796
+ throughLineHash: sha256(Buffer.alloc(0)),
797
+ throughEventId: 'none',
798
+ };
799
+ }
800
+ const finalByte = readRange(fd, size - 1, 1);
801
+ if (finalByte.length !== 1 || finalByte[0] !== 0x0a) return null;
802
+ let cursor = size - 1;
803
+ let lineStart = 0;
804
+ while (cursor > 0) {
805
+ const chunkStart = Math.max(0, cursor - 64 * 1024);
806
+ const chunk = readRange(fd, chunkStart, cursor - chunkStart);
807
+ const newline = chunk.lastIndexOf(0x0a);
808
+ if (newline >= 0) {
809
+ lineStart = chunkStart + newline + 1;
810
+ break;
811
+ }
812
+ cursor = chunkStart;
813
+ if (size - cursor > SNAPSHOT_MAX_LINE_BYTES) return null;
814
+ }
815
+ const lineLength = size - 1 - lineStart;
816
+ if (lineLength > SNAPSHOT_MAX_LINE_BYTES) return null;
817
+ const line = readRange(fd, lineStart, lineLength);
818
+ let throughEventId;
819
+ try { throughEventId = JSON.parse(line.toString('utf8')).event_id; } catch { return null; }
820
+ if (!EVENT_ID.test(String(throughEventId || ''))) return null;
821
+ return {
822
+ ledgerBytes: size,
823
+ throughLineStart: lineStart,
824
+ throughLineLength: lineLength,
825
+ throughLineHash: sha256(line),
826
+ throughEventId,
827
+ };
828
+ } finally {
829
+ if (fd !== undefined) closeSync(fd);
830
+ }
831
+ }
832
+
833
+ function writeSnapshot(vaultBase, state, { projectId, coreHash } = {}) {
834
+ if (!state || !state.bloom || state.bloom.length !== SNAPSHOT_BLOOM_BYTES) {
835
+ return { status: 'skipped', reason: 'snapshot-state-unavailable' };
836
+ }
837
+ const boundary = snapshotBoundary(vaultBase);
838
+ if (!boundary) return { status: 'skipped', reason: 'snapshot-boundary-unavailable' };
839
+ const snapshot = {
840
+ schema_version: MEMORY_SNAPSHOT_SCHEMA_VERSION,
841
+ reducer_version: MEMORY_SNAPSHOT_REDUCER_VERSION,
842
+ project_id: projectId || '',
843
+ core_hash: coreHash,
844
+ event_count: state.eventCount,
845
+ ledger_bytes: boundary.ledgerBytes,
846
+ through_event_id: boundary.throughEventId,
847
+ through_line_start: boundary.throughLineStart,
848
+ through_line_length: boundary.throughLineLength,
849
+ through_line_hash: boundary.throughLineHash,
850
+ chain_hash: state.chainHash,
851
+ order_cursor: state.orderCursor,
852
+ bloom: {
853
+ bytes: SNAPSHOT_BLOOM_BYTES,
854
+ hashes: SNAPSHOT_BLOOM_HASHES,
855
+ data: state.bloom.toString('base64'),
856
+ },
857
+ projection: {
858
+ records: state.records,
859
+ tombstones: state.tombstones,
860
+ revision: state.revision,
861
+ state_hash: state.stateHash,
862
+ event_cursor: state.eventCursor,
863
+ updated_at: state.updatedAt,
864
+ },
865
+ };
866
+ snapshot.snapshot_hash = snapshotHash(snapshot);
867
+ const content = `${JSON.stringify(snapshot, null, 2)}\n`;
868
+ const path = snapshotPath(vaultBase);
869
+ const current = readCheckedFile(
870
+ vaultBase, path, 'utf8', 'snapshot incremental de memória', { allowMissing: true },
871
+ );
872
+ if (current === content) return { status: 'unchanged', snapshot };
873
+ writeVaultFileAtomic(vaultBase, path, content, 'utf8', {
874
+ label: 'snapshot incremental de memória',
875
+ });
876
+ return { status: 'written', snapshot };
877
+ }
878
+
879
+ export function readMemoryProjectionSnapshot(vaultBase) {
880
+ const core = readCoreAuthority(vaultBase);
881
+ const projectId = projectIdForVault(vaultBase);
882
+ const loaded = readSnapshotDocument(vaultBase, core.hash, projectId);
883
+ if (loaded.status !== 'ok') return loaded;
884
+ const tail = readSnapshotTail(vaultBase, loaded.snapshot, projectId || loaded.snapshot.project_id);
885
+ return { ...loaded, tail };
886
+ }
887
+
888
+ function fullLedgerForOutbox(vaultBase, outbox) {
889
+ const ledger = readMemoryLedger(vaultBase);
890
+ if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
891
+ const byId = new Map(ledger.events.map((event) => [event.event_id, event]));
892
+ const newEvents = [];
893
+ for (const entry of outbox) {
894
+ const existing = byId.get(entry.event.event_id);
895
+ if (existing) {
896
+ if (canonicalMemoryJson(existing) !== canonicalMemoryJson(entry.event)) {
897
+ throw new MemoryEventCollision(entry.event.event_id);
898
+ }
899
+ continue;
900
+ }
901
+ byId.set(entry.event.event_id, entry.event);
902
+ newEvents.push(entry.event);
903
+ }
904
+ return { ledger, newEvents, allEvents: [...ledger.events, ...newEvents] };
905
+ }
906
+
907
+ function trySnapshotForOutbox(vaultBase, outbox, core, projectId) {
908
+ const loaded = readSnapshotDocument(vaultBase, core.hash, projectId);
909
+ if (loaded.status !== 'ok') return { status: 'fallback', reason: loaded.reason };
910
+ const tail = readSnapshotTail(vaultBase, loaded.snapshot, projectId || loaded.snapshot.project_id);
911
+ if (tail.status !== 'ok') return { status: 'fallback', reason: tail.reason };
912
+ const tailById = new Map(tail.events.map((event) => [event.event_id, event]));
913
+ const newEvents = [];
914
+ for (const entry of outbox) {
915
+ const existing = tailById.get(entry.event.event_id);
916
+ if (existing) {
917
+ if (canonicalMemoryJson(existing) !== canonicalMemoryJson(entry.event)) {
918
+ throw new MemoryEventCollision(entry.event.event_id);
919
+ }
920
+ continue;
921
+ }
922
+ if (bloomMayContain(loaded.bloom, entry.event.event_id)) {
923
+ return { status: 'fallback', reason: 'snapshot-bloom-hit' };
924
+ }
925
+ newEvents.push(entry.event);
926
+ }
927
+ const projection = incrementalProjection(
928
+ loaded.snapshot, loaded.bloom, tail.events, newEvents, core.invariants,
929
+ );
930
+ if (projection.status !== 'ok') return projection;
931
+ return {
932
+ status: 'ok',
933
+ loaded,
934
+ tail,
935
+ newEvents,
936
+ projection,
937
+ };
938
+ }
939
+
940
+ function shouldAdvanceSnapshot({
941
+ mode, tailBytes = 0, tailEvents = 0, appendedBytes = 0, appended = 0,
942
+ }, options) {
943
+ return mode === 'full'
944
+ || options?.snapshot?.force === true
945
+ || tailEvents + appended >= SNAPSHOT_ADVANCE_EVENTS
946
+ || tailBytes + appendedBytes >= SNAPSHOT_ADVANCE_BYTES;
947
+ }
948
+
949
+ function injectFault(faultAt, boundary) {
950
+ if (faultAt === boundary) throw new Error(`Injected memory-store fault: ${boundary}`);
951
+ }
952
+
953
+ function projectLocked(vaultBase, options = {}) {
954
+ const core = readCoreAuthority(vaultBase);
955
+ const vaultProjectId = projectIdForVault(vaultBase);
956
+ const outbox = readOutbox(vaultBase, vaultProjectId);
957
+ const attempt = trySnapshotForOutbox(vaultBase, outbox, core, vaultProjectId);
958
+ let projection;
959
+ let newEvents;
960
+ let projectId = vaultProjectId;
961
+ let tailBytes = 0;
962
+ let tailEvents = 0;
963
+ let fallbackReason = '';
964
+
965
+ if (attempt.status === 'ok') {
966
+ projection = attempt.projection;
967
+ newEvents = attempt.newEvents;
968
+ projectId ||= attempt.loaded.snapshot.project_id;
969
+ tailBytes = attempt.tail.bytes;
970
+ tailEvents = attempt.tail.events.length;
971
+ } else {
972
+ fallbackReason = attempt.reason || 'snapshot-unavailable';
973
+ const full = fullLedgerForOutbox(vaultBase, outbox);
974
+ newEvents = full.newEvents;
975
+ projectId ||= projectIdForEvents(full.allEvents);
976
+ projection = fullProjection(vaultBase, full.allEvents);
977
+ }
978
+
979
+ const appendedBytes = newEvents.reduce(
980
+ (total, event) => total + Buffer.byteLength(`${canonicalMemoryJson(event)}\n`), 0,
981
+ );
982
+ const willSnapshot = Boolean(projection.snapshotState)
983
+ && shouldAdvanceSnapshot({
984
+ mode: projection.mode,
985
+ tailBytes,
986
+ tailEvents,
987
+ appended: newEvents.length,
988
+ appendedBytes,
989
+ }, options);
990
+ preflightDerivedTargets(vaultBase, { snapshot: willSnapshot });
991
+ const append = appendLedgerDurably(vaultBase, newEvents);
992
+ injectFault(options.faultAt, 'after-ledger');
993
+ const published = publishMemoryProjection(vaultBase, projection.prepared);
994
+ injectFault(options.faultAt, 'after-projection');
995
+
996
+ let snapshotResult = { status: projection.snapshotState ? 'deferred' : 'candidate-conflict' };
997
+ if (willSnapshot) {
998
+ snapshotResult = writeSnapshot(vaultBase, projection.snapshotState, {
999
+ projectId,
1000
+ coreHash: core.hash,
1001
+ });
1002
+ }
1003
+ injectFault(options.faultAt, 'after-snapshot');
1004
+
1005
+ const consumedEventIds = [];
1006
+ for (const entry of outbox) {
1007
+ unlinkVaultFile(vaultBase, entry.path, {
1008
+ missingOk: false,
1009
+ label: 'evento consumido do outbox de memória',
1010
+ });
1011
+ consumedEventIds.push(entry.event.event_id);
1012
+ }
1013
+ return {
1014
+ status: 'projected',
1015
+ appended: newEvents.length,
1016
+ consumed: outbox.length,
1017
+ consumedEventIds,
1018
+ pending: 0,
1019
+ ...published,
1020
+ replayMode: projection.mode,
1021
+ replayedEvents: projection.mode === 'snapshot-tail'
1022
+ ? tailEvents + newEvents.length
1023
+ : projection.eventCount,
1024
+ snapshotStatus: snapshotResult.status,
1025
+ snapshotFallback: fallbackReason || null,
1026
+ snapshotEventCount: projection.eventCount,
1027
+ appendedBytes: append.bytes,
1028
+ };
1029
+ }
1030
+
1031
+ export function projectMemoryOutbox(vaultBase, options = {}) {
1032
+ mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
1033
+ const pending = countPendingOutbox(vaultBase);
1034
+ const result = withMemoryLock(
1035
+ vaultBase, () => projectLocked(vaultBase, options), options.lock || {},
1036
+ );
1037
+ if (result === MEMORY_LOCK_BUSY) return { status: 'busy', pending };
1038
+ return result;
1039
+ }
1040
+
1041
+ function reprojectLocked(vaultBase, options = {}) {
1042
+ const core = readCoreAuthority(vaultBase);
1043
+ const projectId = projectIdForVault(vaultBase);
1044
+ const loaded = readSnapshotDocument(vaultBase, core.hash, projectId);
1045
+ let projection;
1046
+ let tailBytes = 0;
1047
+ let tailEvents = 0;
1048
+ let fallbackReason = '';
1049
+
1050
+ if (loaded.status === 'ok') {
1051
+ const tail = readSnapshotTail(vaultBase, loaded.snapshot, projectId || loaded.snapshot.project_id);
1052
+ if (tail.status === 'ok') {
1053
+ const incremental = incrementalProjection(
1054
+ loaded.snapshot, loaded.bloom, tail.events, [], core.invariants,
1055
+ );
1056
+ if (incremental.status === 'ok') {
1057
+ projection = incremental;
1058
+ tailBytes = tail.bytes;
1059
+ tailEvents = tail.events.length;
1060
+ } else {
1061
+ fallbackReason = incremental.reason;
1062
+ }
1063
+ } else {
1064
+ fallbackReason = tail.reason;
1065
+ }
1066
+ } else {
1067
+ fallbackReason = loaded.reason;
1068
+ }
1069
+
1070
+ if (!projection) {
1071
+ const ledger = readMemoryLedger(vaultBase);
1072
+ if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
1073
+ projection = fullProjection(vaultBase, ledger.events);
1074
+ }
1075
+
1076
+ const willSnapshot = Boolean(projection.snapshotState)
1077
+ && shouldAdvanceSnapshot({ mode: projection.mode, tailBytes, tailEvents }, options);
1078
+ preflightDerivedTargets(vaultBase, { snapshot: willSnapshot });
1079
+ const published = publishMemoryProjection(vaultBase, projection.prepared);
1080
+ let snapshotResult = { status: projection.snapshotState ? 'deferred' : 'candidate-conflict' };
1081
+ if (willSnapshot) {
1082
+ snapshotResult = writeSnapshot(vaultBase, projection.snapshotState, {
1083
+ projectId: projectId || loaded.snapshot?.project_id || '',
1084
+ coreHash: core.hash,
1085
+ });
1086
+ }
1087
+ return {
1088
+ status: 'reprojected',
1089
+ ...published,
1090
+ replayMode: projection.mode,
1091
+ replayedEvents: projection.mode === 'snapshot-tail' ? tailEvents : projection.eventCount,
1092
+ snapshotStatus: snapshotResult.status,
1093
+ snapshotFallback: fallbackReason || null,
1094
+ snapshotEventCount: projection.eventCount,
1095
+ };
1096
+ }
1097
+
1098
+ export function reprojectMemoryLedger(vaultBase, options = {}) {
1099
+ mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
1100
+ const result = withMemoryLock(
1101
+ vaultBase, () => reprojectLocked(vaultBase, options), options.lock || {},
1102
+ );
1103
+ if (result === MEMORY_LOCK_BUSY) return { status: 'busy' };
1104
+ return result;
1105
+ }