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
@@ -1,1161 +1,46 @@
1
- // Durable, local-only store for shared-memory events.
2
- //
3
- // Producers only create immutable outbox files. The projector is the sole ledger writer:
4
- // it takes MEMORY.lock, durably appends events, deterministically replays the complete
5
- // ledger, then atomically publishes SHARED_MEMORY.md and MEMORY_CANDIDATES.jsonl.
6
- import { createHash } from 'node:crypto';
7
- import {
8
- closeSync, constants as fsConstants, copyFileSync, fstatSync, fsyncSync, openSync,
9
- readFileSync, readdirSync, statSync, writeFileSync,
10
- } from 'node:fs';
11
- import { join } from 'node:path';
12
- import {
13
- parseSharedMemory,
14
- renderSharedMemory,
15
- sanitizeMemoryText,
16
- validateMemoryEvent,
17
- } from './memory-schema.mjs';
18
- import {
19
- effectiveMemoryScope,
20
- isRegisterMemoryKey,
21
- memoryRecordKey,
22
- sameMemoryScope,
23
- } from './memory-scope.mjs';
24
- import {
25
- assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, unlinkVaultFile,
26
- VAULT_LOCK_BUSY, withVaultPathLock, writeVaultFileAtomic,
27
- } from './vault-path-safety.mjs';
28
-
29
- const EVENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
30
-
31
- export class MemoryEventCollision extends Error {
32
- constructor(eventId, message = `Memory event ID collision: ${eventId}`) {
33
- super(message);
34
- this.name = 'MemoryEventCollision';
35
- this.code = 'MEMORY_EVENT_COLLISION';
36
- this.eventId = eventId;
37
- }
38
- }
39
-
40
- export class MemoryLedgerCorruption extends Error {
41
- constructor(errors, message = 'Memory ledger is corrupt; run `wendkeep memory repair`.') {
42
- super(message);
43
- this.name = 'MemoryLedgerCorruption';
44
- this.code = 'MEMORY_LEDGER_CORRUPT';
45
- this.errors = Array.isArray(errors) ? errors : [];
46
- }
47
- }
48
-
49
- export class MemoryOutboxCorruption extends Error {
50
- constructor(path, cause) {
51
- super(`Memory outbox file is corrupt: ${path}`);
52
- this.name = 'MemoryOutboxCorruption';
53
- this.code = 'MEMORY_OUTBOX_CORRUPT';
54
- this.path = path;
55
- this.cause = cause;
56
- }
57
- }
58
-
59
- function brainDir(vaultBase) {
60
- return join(vaultBase, '.brain');
61
- }
62
-
63
- function outboxDir(vaultBase) {
64
- return join(brainDir(vaultBase), 'memory-outbox');
65
- }
66
-
67
- function ledgerPath(vaultBase) {
68
- return join(brainDir(vaultBase), 'MEMORY_EVENTS.jsonl');
69
- }
70
-
71
- function sharedPath(vaultBase) {
72
- return join(brainDir(vaultBase), 'SHARED_MEMORY.md');
73
- }
74
-
75
- function corePath(vaultBase) {
76
- return join(brainDir(vaultBase), 'CORE.md');
77
- }
78
-
79
- function candidatesPath(vaultBase) {
80
- return join(brainDir(vaultBase), 'MEMORY_CANDIDATES.jsonl');
81
- }
82
-
83
- function lockTarget(vaultBase) {
84
- return join(brainDir(vaultBase), 'MEMORY');
85
- }
86
-
87
- function unsafeMemoryPath(message) {
88
- const error = new Error(message);
89
- error.code = 'VAULT_PATH_UNSAFE';
90
- return error;
91
- }
92
-
93
- function checkedMemoryFile(vaultBase, path, label, {
94
- allowMissing = true,
95
- mustNotExist = false,
96
- } = {}) {
97
- return assertVaultPathSafe(vaultBase, path, {
98
- allowMissing,
99
- expectedType: 'file',
100
- mustNotExist,
101
- label,
102
- });
103
- }
104
-
105
- function checkedMemoryDirectory(vaultBase, path, label, { allowMissing = true } = {}) {
106
- return assertVaultPathSafe(vaultBase, path, {
107
- allowMissing,
108
- expectedType: 'directory',
109
- label,
110
- });
111
- }
112
-
113
- function readCheckedMemoryFile(vaultBase, path, encoding, label, { allowMissing = false } = {}) {
114
- let checked = checkedMemoryFile(vaultBase, path, label, { allowMissing });
115
- if (!checked.exists) return null;
116
- // Deliberately adjacent to readFileSync: aliases created since the first policy check
117
- // are rejected before Node opens the artifact.
118
- checked = checkedMemoryFile(vaultBase, checked.target, label, { allowMissing: false });
119
- return readFileSync(checked.target, encoding);
120
- }
121
-
122
- export function memoryFileIdentityMatches(descriptor, target, {
123
- platform = process.platform,
124
- } = {}) {
125
- if (descriptor.ino !== target.ino) return false;
126
- // libuv before 1.51 can report an inconsistent Windows volume serial number
127
- // between stat(path) and fstat(fd). The inode is still the file index; path
128
- // containment/reparse checks and nlink validation remain independent guards.
129
- return platform === 'win32' || descriptor.dev === target.dev;
130
- }
131
-
132
- function assertOpenedMemoryFile(vaultBase, path, fd, label) {
133
- const checked = checkedMemoryFile(vaultBase, path, label, { allowMissing: false });
134
- // Windows file identities can exceed Number's safe integer range. Node 22.13 may
135
- // round stat(path) and fstat(fd) differently for the same file, so compare the
136
- // exact bigint values and keep nlink as the independent hardlink guard.
137
- const descriptor = fstatSync(fd, { bigint: true });
138
- const target = statSync(checked.target, { bigint: true });
139
- if (!descriptor.isFile() || descriptor.nlink > 1n || target.nlink > 1n
140
- || !memoryFileIdentityMatches(descriptor, target)) {
141
- throw unsafeMemoryPath(`${label} mudou de inode ou possui hardlink antes da mutação: ${checked.target}`);
142
- }
143
- return checked.target;
144
- }
145
-
146
- function preflightProjectionTargets(vaultBase) {
147
- return assertVaultPathsSafe(vaultBase, [
148
- { path: sharedPath(vaultBase), expectedType: 'file', label: 'projeção SHARED_MEMORY.md' },
149
- { path: candidatesPath(vaultBase), expectedType: 'file', label: 'projeção MEMORY_CANDIDATES.jsonl' },
150
- ]);
151
- }
152
-
153
- function canonicalize(value) {
154
- if (Array.isArray(value)) return value.map(canonicalize);
155
- if (value && typeof value === 'object') {
156
- const out = {};
157
- for (const key of Object.keys(value).sort()) {
158
- if (value[key] !== undefined) out[key] = canonicalize(value[key]);
159
- }
160
- return out;
161
- }
162
- return value;
163
- }
164
-
165
- export function canonicalMemoryJson(value) {
166
- return JSON.stringify(canonicalize(value));
167
- }
168
-
169
- function sha256(value) {
170
- return createHash('sha256').update(String(value)).digest('hex');
171
- }
172
-
173
- export function hashMemoryValue(value) {
174
- return sha256(canonicalMemoryJson(value));
175
- }
176
-
177
- function sanitizeValue(value) {
178
- if (typeof value === 'string') return sanitizeMemoryText(value);
179
- if (Array.isArray(value)) return value.map(sanitizeValue);
180
- if (value && typeof value === 'object') {
181
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeValue(item)]));
182
- }
183
- return value;
184
- }
185
-
186
- function sanitizeEvent(event) {
187
- return sanitizeValue(structuredClone(event));
188
- }
189
-
190
- function projectIdForVault(vaultBase) {
191
- const path = join(brainDir(vaultBase), 'PROJECT.json');
192
- const raw = readCheckedMemoryFile(vaultBase, path, 'utf8', 'autoridade PROJECT.json', {
193
- allowMissing: true,
194
- });
195
- if (raw === null) return undefined;
196
- try {
197
- const project = JSON.parse(raw);
198
- return typeof project.projectId === 'string' && project.projectId ? project.projectId : undefined;
199
- } catch {
200
- return undefined;
201
- }
202
- }
203
-
204
- function assertValidEvent(event, vaultBase) {
205
- const sanitized = sanitizeEvent(event);
206
- const projectId = vaultBase ? projectIdForVault(vaultBase) : undefined;
207
- const result = validateMemoryEvent(sanitized, projectId ? { projectId } : {});
208
- if (!result.ok) {
209
- const error = new TypeError(`Invalid memory event: ${result.errors.join(' ')}`);
210
- error.code = 'MEMORY_EVENT_INVALID';
211
- error.errors = result.errors;
212
- throw error;
213
- }
214
- if (!EVENT_ID.test(sanitized.event_id)) {
215
- const error = new TypeError('Invalid memory event: event_id is not filename-safe.');
216
- error.code = 'MEMORY_EVENT_INVALID';
217
- throw error;
218
- }
219
- return sanitized;
220
- }
221
-
222
- function eventHash(event) {
223
- return sha256(canonicalMemoryJson(event));
224
- }
225
-
226
- const OUTBOX_PUBLICATION_WAIT_MS = 500;
227
- const OUTBOX_PUBLICATION_POLL_MS = 5;
228
- const OUTBOX_PUBLICATION_SIGNAL = new Int32Array(new SharedArrayBuffer(4));
229
-
230
- function readConcurrentOutboxEvent(vaultBase, path, eventId) {
231
- const deadline = Date.now() + OUTBOX_PUBLICATION_WAIT_MS;
232
- while (true) {
233
- let raw;
234
- try {
235
- raw = readCheckedMemoryFile(
236
- vaultBase, path, 'utf8', 'evento imutável preexistente do outbox',
237
- );
238
- } catch (cause) {
239
- if (cause?.code === 'VAULT_PATH_UNSAFE') throw cause;
240
- throw new MemoryEventCollision(eventId, `Existing outbox event is unreadable: ${eventId}`);
241
- }
242
-
243
- try {
244
- return JSON.parse(raw);
245
- } catch (cause) {
246
- if (!(cause instanceof SyntaxError) || Date.now() >= deadline) {
247
- throw new MemoryEventCollision(eventId, `Existing outbox event is unreadable: ${eventId}`);
248
- }
249
- Atomics.wait(OUTBOX_PUBLICATION_SIGNAL, 0, 0, OUTBOX_PUBLICATION_POLL_MS);
250
- }
251
- }
252
- }
253
-
254
- /**
255
- * Persist one immutable producer event using exclusive creation. A retry with the same
256
- * canonical payload is a no-op; reusing the ID for different bytes is an observable error.
257
- */
258
- export function enqueueMemoryEvent(vaultBase, event) {
259
- const checked = assertValidEvent(event, vaultBase);
260
- const dir = outboxDir(vaultBase);
261
- mkdirVaultPath(vaultBase, dir, { label: 'outbox de memória' });
262
- const path = join(dir, `${checked.event_id}.json`);
263
- const payload = `${canonicalMemoryJson(checked)}\n`;
264
- const hash = eventHash(checked);
265
-
266
- let fd;
267
- try {
268
- checkedMemoryFile(vaultBase, path, 'evento imutável do outbox');
269
- // The final lstat is deliberately adjacent to the exclusive open. A pre-created
270
- // symlink/hardlink loses the wx race and is revalidated in the EEXIST branch.
271
- checkedMemoryFile(vaultBase, path, 'evento imutável do outbox');
272
- fd = openSync(path, 'wx');
273
- assertOpenedMemoryFile(vaultBase, path, fd, 'evento imutável do outbox');
274
- writeFileSync(fd, payload, 'utf8');
275
- fsyncSync(fd);
276
- assertOpenedMemoryFile(vaultBase, path, fd, 'evento imutável do outbox');
277
- return { status: 'enqueued', path, eventId: checked.event_id, hash };
278
- } catch (error) {
279
- if (error?.code !== 'EEXIST') throw error;
280
- const existing = readConcurrentOutboxEvent(vaultBase, path, checked.event_id);
281
- if (eventHash(existing) !== hash || canonicalMemoryJson(existing) !== canonicalMemoryJson(checked)) {
282
- throw new MemoryEventCollision(checked.event_id);
283
- }
284
- return { status: 'duplicate', path, eventId: checked.event_id, hash };
285
- } finally {
286
- if (fd !== undefined) closeSync(fd);
287
- }
288
- }
289
-
290
- function ledgerError(line, message, partial = false) {
291
- return { line, message, partial };
292
- }
293
-
294
- /** Read a ledger without hiding a valid prefix when its tail is corrupt or partial. */
295
- export function readMemoryLedger(vaultBase) {
296
- const path = ledgerPath(vaultBase);
297
- const checked = checkedMemoryFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl');
298
- if (!checked.exists) {
299
- return { status: 'ok', path, raw: '', events: [], eventIds: new Set(), errors: [] };
300
- }
301
- const raw = readCheckedMemoryFile(
302
- vaultBase, checked.target, 'utf8', 'ledger MEMORY_EVENTS.jsonl',
303
- ).replace(/\r\n/g, '\n');
304
- const lines = raw.split('\n');
305
- const hasPartialTail = raw.length > 0 && !raw.endsWith('\n');
306
- if (!hasPartialTail) lines.pop();
307
- const events = [];
308
- const eventIds = new Set();
309
- const eventPayloads = new Map();
310
- const errors = [];
311
- const projectId = projectIdForVault(vaultBase);
312
-
313
- lines.forEach((line, index) => {
314
- const lineNumber = index + 1;
315
- const partial = hasPartialTail && index === lines.length - 1;
316
- if (!line.trim()) {
317
- errors.push(ledgerError(lineNumber, 'blank ledger line', partial));
318
- return;
319
- }
320
- let parsed;
321
- try {
322
- parsed = JSON.parse(line);
323
- } catch (error) {
324
- errors.push(ledgerError(lineNumber, `invalid JSON: ${error.message}`, partial));
325
- return;
326
- }
327
- const validation = validateMemoryEvent(parsed, projectId ? { projectId } : {});
328
- if (!validation.ok) {
329
- errors.push(ledgerError(lineNumber, validation.errors.join(' '), partial));
330
- return;
331
- }
332
- const payload = canonicalMemoryJson(parsed);
333
- if (eventIds.has(parsed.event_id)) {
334
- if (eventPayloads.get(parsed.event_id) !== payload) {
335
- errors.push(ledgerError(lineNumber, `event_id collision: ${parsed.event_id}`, partial));
336
- } else {
337
- errors.push(ledgerError(lineNumber, `duplicate event_id: ${parsed.event_id}`, partial));
338
- }
339
- return;
340
- }
341
- eventIds.add(parsed.event_id);
342
- eventPayloads.set(parsed.event_id, payload);
343
- events.push(parsed);
344
- });
345
-
346
- return {
347
- status: errors.length ? 'corrupt' : 'ok',
348
- path,
349
- raw,
350
- events,
351
- eventIds,
352
- errors,
353
- };
354
- }
355
-
356
- function eventOrder(left, right) {
357
- return (Number(left.base_revision ?? 0) - Number(right.base_revision ?? 0))
358
- || String(left.effective_at || left.observed_at).localeCompare(String(right.effective_at || right.observed_at))
359
- || Number(left.turn_sequence ?? 0) - Number(right.turn_sequence ?? 0)
360
- || String(left.event_id).localeCompare(String(right.event_id));
361
- }
362
-
363
- function sameCausalActivation(left, right) {
364
- return Boolean(left?.canonical_session_id)
365
- && left.canonical_session_id === right?.canonical_session_id
366
- && left.activation_id === right?.activation_id;
367
- }
368
-
369
- function hasCompleteCausalIdentity(event) {
370
- return Boolean(event?.canonical_session_id)
371
- && Boolean(event?.activation_id)
372
- && Boolean(event?.source_turn_id)
373
- && Number.isInteger(event?.activation_epoch)
374
- && Number.isInteger(event?.turn_sequence);
375
- }
376
-
377
- function sameCompleteCausalLineage(left, right) {
378
- return hasCompleteCausalIdentity(left)
379
- && hasCompleteCausalIdentity(right)
380
- && left.canonical_session_id === right.canonical_session_id
381
- && left.activation_id === right.activation_id
382
- && left.activation_epoch === right.activation_epoch;
383
- }
384
-
385
- function comparable(left, right) {
386
- if (left?.project_id !== right?.project_id || !sameMemoryScope(left, right)) return false;
387
- if (sameCausalActivation(left, right)) return true;
388
- const leftSupersedes = left.supersedes_event_id || left.supersedes;
389
- const rightSupersedes = right.supersedes_event_id || right.supersedes;
390
- return leftSupersedes === right.event_id || rightSupersedes === left.event_id
391
- || (Array.isArray(leftSupersedes) && leftSupersedes.includes(right.event_id))
392
- || (Array.isArray(rightSupersedes) && rightSupersedes.includes(left.event_id));
393
- }
394
-
395
- function conflictGroupKey(event) {
396
- if (event.operation !== 'replace') return null;
397
- if (!Number.isInteger(event.base_revision) || typeof event.base_value_hash !== 'string') return null;
398
- return `${memoryRecordKey(event)}\u0000${event.base_revision}\u0000${event.base_value_hash}`;
399
- }
400
-
401
- function candidateId(reason, memoryKey, eventIds) {
402
- return `memcand-${sha256(`${reason}\u0000${memoryKey}\u0000${eventIds.join('\u0000')}`).slice(0, 16)}`;
403
- }
404
-
405
- // CORE is prose, not an operational data store. Only an explicit, single-line marker
406
- // participates in precedence so the projector never guesses meaning from human text:
407
- // <!-- wk-memory: release.push="manual-only" -->
408
- function readCoreInvariants(vaultBase) {
409
- const core = readCheckedMemoryFile(
410
- vaultBase, corePath(vaultBase), 'utf8', 'autoridade CORE.md', { allowMissing: true },
411
- );
412
- if (core === null) return new Map();
413
- const invariants = new Map();
414
- const marker = /^<!--\s*wk-memory:\s*([A-Za-z0-9][A-Za-z0-9._-]*)=(.+)\s*-->$/;
415
- for (const line of core.split('\n')) {
416
- const match = line.trim().match(marker);
417
- if (!match) continue;
418
- try {
419
- invariants.set(match[1], JSON.parse(match[2].trim()));
420
- } catch { /* malformed prose markers do not become implicit authority */ }
421
- }
422
- return invariants;
423
- }
424
-
425
- function blockedByCoreCandidate(event, coreValue) {
426
- return {
427
- v: 1,
428
- candidate_id: candidateId('blocked_by_core', event.memory_key, [event.event_id]),
429
- reason: 'blocked_by_core',
430
- status: 'blocked_by_core',
431
- memory_key: event.memory_key,
432
- ...(event.scope ? { scope: effectiveMemoryScope(event), record_key: memoryRecordKey(event) } : {}),
433
- event_ids: [event.event_id],
434
- proposed_value: event.value,
435
- core_value: coreValue,
436
- provenance: {
437
- authority: 'core',
438
- source: '.brain/CORE.md',
439
- core_value_hash: hashMemoryValue(coreValue),
440
- },
441
- events: [event],
442
- };
443
- }
444
-
445
- function conflictCandidate(memoryKey, events, currentEvent = null) {
446
- const ordered = [...events].sort(eventOrder);
447
- const eventIds = ordered.map((item) => item.event_id).sort();
448
- const byId = new Map(ordered.map((item) => [item.event_id, item]));
449
- return {
450
- v: 1,
451
- candidate_id: candidateId('conflict', memoryKey, eventIds),
452
- reason: 'conflict',
453
- memory_key: memoryKey,
454
- ...((ordered[0] || currentEvent)?.scope ? {
455
- scope: effectiveMemoryScope(ordered[0] || currentEvent || {}),
456
- record_key: memoryRecordKey(ordered[0] || currentEvent || { memory_key: memoryKey }),
457
- } : {}),
458
- event_ids: eventIds,
459
- values: eventIds.map((id) => byId.get(id).value),
460
- base_revision: ordered[0]?.base_revision ?? currentEvent?.revision ?? 0,
461
- base_value_hash: ordered[0]?.base_value_hash ?? (currentEvent ? hashMemoryValue(currentEvent.value) : null),
462
- events: eventIds.map((id) => byId.get(id)),
463
- };
464
- }
465
-
466
- function conflictReviewEvent(candidate, candidateCount = 1) {
467
- const source = candidate.events?.[0] || {};
468
- const memoryKey = sanitizeMemoryText(candidate.memory_key || 'unknown');
469
- const eventCount = Array.isArray(candidate.event_ids) ? candidate.event_ids.length : 0;
470
- return {
471
- v: 1,
472
- event_id: `mem-review-${candidate.candidate_id}`,
473
- project_id: source.project_id || '',
474
- memory_key: candidate.memory_key,
475
- scope: candidate.scope,
476
- operation: 'assert',
477
- value: `[revisão pendente: ${memoryKey}; candidates: ${candidateCount}; events: ${eventCount}]`,
478
- authority: 'candidate',
479
- canonical_session_id: source.canonical_session_id || 'memory-reducer',
480
- activation_id: source.activation_id || 'memory-reducer',
481
- activation_epoch: Number.isInteger(source.activation_epoch) ? source.activation_epoch : 0,
482
- turn_sequence: 0,
483
- source_turn_id: 'memory-review',
484
- observed_at: source.observed_at || new Date(0).toISOString(),
485
- evidence: ['MEMORY_CANDIDATES.jsonl'],
486
- review_pending: true,
487
- };
488
- }
489
-
490
- function sortedObject(entries) {
491
- return Object.fromEntries([...entries].sort(([left], [right]) => left.localeCompare(right)));
492
- }
493
-
494
- function currentEventFromRecord(record) {
495
- if (!record) return null;
496
- return { ...record.source, value: record.value, revision: record.revision };
497
- }
498
-
499
- function supersededTransitively(superseded, sourceEventId, finalEventId) {
500
- if (!sourceEventId || !finalEventId || sourceEventId === finalEventId) return false;
501
- const edges = new Map();
502
- for (const item of superseded) {
503
- if (!edges.has(item.event_id)) edges.set(item.event_id, []);
504
- edges.get(item.event_id).push(item.by_event_id);
505
- }
506
- const pending = [sourceEventId];
507
- const visited = new Set();
508
- while (pending.length) {
509
- const current = pending.shift();
510
- if (visited.has(current)) continue;
511
- visited.add(current);
512
- for (const next of edges.get(current) || []) {
513
- if (next === finalEventId) return true;
514
- if (!visited.has(next)) pending.push(next);
515
- }
516
- }
517
- return false;
518
- }
519
-
520
- function isCausallyOlder(event, current) {
521
- if (!current) return false;
522
- if (sameCausalActivation(event, current)) {
523
- return Number(event.turn_sequence) < Number(current.turn_sequence);
524
- }
525
- if (event.canonical_session_id && event.canonical_session_id === current.canonical_session_id
526
- && sameMemoryScope(event, current)
527
- && Number.isInteger(event.activation_epoch) && Number.isInteger(current.activation_epoch)
528
- && event.activation_epoch !== current.activation_epoch) {
529
- return event.activation_epoch < current.activation_epoch;
530
- }
531
- const eventEffective = Date.parse(event.effective_at || '');
532
- const currentEffective = Date.parse(current.effective_at || '');
533
- return Number.isFinite(eventEffective) && Number.isFinite(currentEffective)
534
- && eventEffective < currentEffective;
535
- }
536
-
537
- const AUTHORITY_RANK = Object.freeze({ candidate: 0, reported: 1, verified: 2 });
538
-
539
- function sameRegisterLineage(left, right) {
540
- return Boolean(left?.canonical_session_id)
541
- && left.canonical_session_id === right?.canonical_session_id
542
- && left.project_id === right?.project_id
543
- && sameMemoryScope(left, right);
544
- }
545
-
546
- /** Positive means `incoming` is a safe successor; null means human comparison is required. */
547
- function registerPrecedence(incoming, current) {
548
- if (!incoming?.scope || !current?.scope
549
- || !isRegisterMemoryKey(incoming?.memory_key) || !sameRegisterLineage(incoming, current)) return null;
550
- const epoch = Number(incoming.activation_epoch ?? -1) - Number(current.activation_epoch ?? -1);
551
- if (epoch) return epoch;
552
- const turn = Number(incoming.turn_sequence ?? -1) - Number(current.turn_sequence ?? -1);
553
- if (turn) return turn;
554
- const authority = (AUTHORITY_RANK[incoming.authority] ?? -1) - (AUTHORITY_RANK[current.authority] ?? -1);
555
- if (authority) return authority;
556
- const observed = String(incoming.observed_at || '').localeCompare(String(current.observed_at || ''));
557
- if (observed) return observed;
558
- return String(incoming.event_id || '').localeCompare(String(current.event_id || ''));
559
- }
560
-
561
- /**
562
- * Pure deterministic reducer. It pre-detects incomparable scalar siblings so replay order
563
- * never turns one concurrent writer into an accidental winner.
564
- */
565
- export function reduceMemoryEvents(inputEvents = [], {
566
- coreInvariants = new Map(), resolveDeferredAsserts = true,
567
- } = {}) {
568
- const protectedValues = coreInvariants instanceof Map
569
- ? coreInvariants
570
- : new Map(Object.entries(coreInvariants || {}));
571
- const unique = new Map();
572
- for (const raw of inputEvents) {
573
- const event = assertValidEvent(raw);
574
- const existing = unique.get(event.event_id);
575
- if (existing && canonicalMemoryJson(existing) !== canonicalMemoryJson(event)) {
576
- throw new MemoryEventCollision(event.event_id, `Ledger contains divergent payloads for ${event.event_id}`);
577
- }
578
- if (!existing) unique.set(event.event_id, event);
579
- }
580
- const rescopeTargets = new Set(
581
- [...unique.values()].flatMap((item) => [
582
- item.rescopes_event_id,
583
- ...(Array.isArray(item.rescopes_event_ids) ? item.rescopes_event_ids : []),
584
- ]).filter(Boolean),
585
- );
586
- const projectIds = new Set([...unique.values()].map((item) => item.project_id).filter(Boolean));
587
- if (projectIds.size > 1) {
588
- const error = new TypeError('Memory reducer cannot compare events from different projects.');
589
- error.code = 'MEMORY_PROJECT_MIXED';
590
- throw error;
591
- }
592
- const events = [...unique.values()]
593
- .filter((item) => !rescopeTargets.has(item.event_id))
594
- .sort(eventOrder);
595
- const candidateDecisions = new Map();
596
- for (const item of events) {
597
- const decision = item.candidate_decision;
598
- if (!decision) continue;
599
- const existing = candidateDecisions.get(decision.candidate_id);
600
- if (existing && canonicalMemoryJson(existing.decision) !== canonicalMemoryJson(decision)) {
601
- throw new MemoryEventCollision(
602
- decision.candidate_id,
603
- `Ledger contains incompatible decisions for candidate ${decision.candidate_id}`,
604
- );
605
- }
606
- if (!existing) candidateDecisions.set(decision.candidate_id, { decision, event: item });
607
- }
608
-
609
- const peerGroups = new Map();
610
- for (const item of events) {
611
- const key = conflictGroupKey(item);
612
- if (!key) continue;
613
- if (!peerGroups.has(key)) peerGroups.set(key, []);
614
- peerGroups.get(key).push(item);
615
- }
616
- const conflictingIds = new Set();
617
- const groupedCandidates = new Map();
618
- for (const [key, group] of peerGroups) {
619
- const incomparable = group.filter((item, index) => group.some((other, otherIndex) => index !== otherIndex && !comparable(item, other)));
620
- if (incomparable.length < 2) continue;
621
- const ordered = [...incomparable].sort(eventOrder);
622
- ordered.forEach((item) => conflictingIds.add(item.event_id));
623
- groupedCandidates.set(key, ordered);
624
- }
625
-
626
- const records = new Map();
627
- const tombstones = new Map();
628
- const candidates = [];
629
- const pendingAssertConflicts = [];
630
- const resolvedCandidateIds = new Set();
631
- const emittedGroups = new Set();
632
- const appliedEventIds = [];
633
- const superseded = [];
634
- let revision = 0;
635
-
636
- for (const item of events) {
637
- if (item.candidate_decision && item.candidate_decision.action === 'reject') {
638
- appliedEventIds.push(item.event_id);
639
- continue;
640
- }
641
-
642
- if (protectedValues.has(item.memory_key)) {
643
- const coreValue = protectedValues.get(item.memory_key);
644
- const agreesWithCore = item.operation === 'assert'
645
- && hashMemoryValue(item.value) === hashMemoryValue(coreValue);
646
- if (!agreesWithCore) {
647
- candidates.push(blockedByCoreCandidate(item, coreValue));
648
- continue;
649
- }
650
- }
651
-
652
- const groupKey = conflictGroupKey(item);
653
- if (conflictingIds.has(item.event_id)) {
654
- if (!emittedGroups.has(groupKey)) {
655
- candidates.push(conflictCandidate(item.memory_key, groupedCandidates.get(groupKey)));
656
- emittedGroups.add(groupKey);
657
- }
658
- continue;
659
- }
660
-
661
- const recordKey = memoryRecordKey(item);
662
- const current = records.get(recordKey);
663
- const currentSource = current?.source;
664
- if (isCausallyOlder(item, currentSource)) {
665
- superseded.push({ event_id: item.event_id, by_event_id: currentSource.event_id });
666
- continue;
667
- }
668
-
669
- if (item.operation === 'assert') {
670
- if (current && hashMemoryValue(current.value) !== hashMemoryValue(item.value)) {
671
- const precedence = registerPrecedence(item, currentSource);
672
- if ((sameCausalActivation(item, currentSource)
673
- && Number(item.turn_sequence) > Number(currentSource.turn_sequence))
674
- || precedence > 0) {
675
- records.set(recordKey, { value: item.value, revision: current.revision + 1, source: item });
676
- tombstones.delete(recordKey);
677
- superseded.push({ event_id: currentSource.event_id, by_event_id: item.event_id });
678
- revision += 1;
679
- appliedEventIds.push(item.event_id);
680
- continue;
681
- }
682
- const candidate = conflictCandidate(item.memory_key, [currentEventFromRecord(current), item]);
683
- candidates.push(candidate);
684
- pendingAssertConflicts.push({ candidate, event: item });
685
- continue;
686
- }
687
- if (!current) {
688
- records.set(recordKey, { value: item.value, revision: 1, source: item });
689
- tombstones.delete(recordKey);
690
- revision += 1;
691
- }
692
- appliedEventIds.push(item.event_id);
693
- continue;
694
- }
695
-
696
- if (item.operation === 'add') {
697
- const oldValues = Array.isArray(current?.value) ? current.value : (current ? [current.value] : []);
698
- const additions = Array.isArray(item.value) ? item.value : [item.value];
699
- const byHash = new Map(oldValues.map((value) => [hashMemoryValue(value), value]));
700
- additions.forEach((value) => byHash.set(hashMemoryValue(value), value));
701
- const value = [...byHash.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([, entry]) => entry);
702
- records.set(recordKey, { value, revision: (current?.revision || 0) + 1, source: item });
703
- tombstones.delete(recordKey);
704
- revision += 1;
705
- appliedEventIds.push(item.event_id);
706
- continue;
707
- }
708
-
709
- if (item.operation === 'remove') {
710
- if (!current) {
711
- appliedEventIds.push(item.event_id);
712
- continue;
713
- }
714
- const baseMatches = item.base_revision === undefined
715
- || (item.base_revision === current.revision
716
- && (!item.base_value_hash || item.base_value_hash === hashMemoryValue(current.value)));
717
- if (!baseMatches) {
718
- candidates.push(conflictCandidate(item.memory_key, [currentEventFromRecord(current), item]));
719
- continue;
720
- }
721
- if (item.value !== null && item.value !== undefined && Array.isArray(current.value)) {
722
- const removalHash = hashMemoryValue(item.value);
723
- const value = current.value.filter((entry) => hashMemoryValue(entry) !== removalHash);
724
- records.set(recordKey, { value, revision: current.revision + 1, source: item });
725
- tombstones.set(`${recordKey}:${removalHash}`, {
726
- event_id: item.event_id, removed_event_id: current.source.event_id, value_hash: removalHash,
727
- });
728
- } else {
729
- records.delete(recordKey);
730
- tombstones.set(recordKey, {
731
- event_id: item.event_id,
732
- removed_event_id: current.source.event_id,
733
- value_hash: hashMemoryValue(current.value),
734
- });
735
- }
736
- revision += 1;
737
- appliedEventIds.push(item.event_id);
738
- continue;
739
- }
740
-
741
- // replace
742
- const explicitlySupersedes = item.supersedes_event_id === currentSource?.event_id
743
- || (Array.isArray(item.supersedes) && item.supersedes.includes(currentSource?.event_id));
744
- const baseMatches = current
745
- && item.base_revision === current.revision
746
- && item.base_value_hash === hashMemoryValue(current.value);
747
- if (!current || (!baseMatches && !explicitlySupersedes)) {
748
- candidates.push(conflictCandidate(item.memory_key, current ? [currentEventFromRecord(current), item] : [item]));
749
- continue;
750
- }
751
- records.set(recordKey, { value: item.value, revision: current.revision + 1, source: item });
752
- tombstones.delete(recordKey);
753
- const explicitlySupersededIds = new Set(
754
- Array.isArray(item.supersedes)
755
- ? item.supersedes
756
- : (item.supersedes_event_id ? [item.supersedes_event_id] : []),
757
- );
758
- if (item.candidate_decision?.action === 'promote' && explicitlySupersededIds.size) {
759
- for (const candidate of candidates) {
760
- if (candidate.reason !== 'conflict' || !candidate.event_ids?.length) continue;
761
- if (candidate.event_ids.every((eventId) => explicitlySupersededIds.has(eventId))) {
762
- resolvedCandidateIds.add(candidate.candidate_id);
763
- }
764
- }
765
- for (const pending of pendingAssertConflicts) {
766
- if (explicitlySupersededIds.has(pending.event.event_id)) {
767
- resolvedCandidateIds.add(pending.candidate.candidate_id);
768
- }
769
- }
770
- }
771
- revision += 1;
772
- appliedEventIds.push(item.event_id);
773
- }
774
-
775
- if (resolveDeferredAsserts) {
776
- // A physically late assert can sort before a corrective promotion because effective time
777
- // precedes CLI decision time. Revisit only scalar assert conflicts left without an explicit
778
- // decision, against the final complete causal source; the ledger and global ordering stay put.
779
- const deferredAsserts = pendingAssertConflicts
780
- .filter((pending) => !candidateDecisions.has(pending.candidate.candidate_id))
781
- .sort((left, right) => String(left.event.memory_key).localeCompare(String(right.event.memory_key))
782
- || String(left.event.canonical_session_id || '').localeCompare(String(right.event.canonical_session_id || ''))
783
- || String(left.event.activation_id || '').localeCompare(String(right.event.activation_id || ''))
784
- || Number(left.event.activation_epoch ?? -1) - Number(right.event.activation_epoch ?? -1)
785
- || Number(left.event.turn_sequence ?? -1) - Number(right.event.turn_sequence ?? -1)
786
- || eventOrder(left.event, right.event));
787
- let advanced = true;
788
- while (advanced) {
789
- advanced = false;
790
- for (const pending of deferredAsserts) {
791
- if (resolvedCandidateIds.has(pending.candidate.candidate_id)) continue;
792
- const current = records.get(memoryRecordKey(pending.event));
793
- const currentSource = current?.source;
794
- if (!sameCompleteCausalLineage(pending.event, currentSource)) continue;
795
- if (pending.event.turn_sequence > currentSource.turn_sequence) {
796
- records.set(memoryRecordKey(pending.event), {
797
- value: pending.event.value,
798
- revision: current.revision + 1,
799
- source: pending.event,
800
- });
801
- tombstones.delete(memoryRecordKey(pending.event));
802
- superseded.push({ event_id: currentSource.event_id, by_event_id: pending.event.event_id });
803
- revision += 1;
804
- appliedEventIds.push(pending.event.event_id);
805
- resolvedCandidateIds.add(pending.candidate.candidate_id);
806
- advanced = true;
807
- } else if (pending.event.turn_sequence < currentSource.turn_sequence) {
808
- superseded.push({ event_id: pending.event.event_id, by_event_id: currentSource.event_id });
809
- resolvedCandidateIds.add(pending.candidate.candidate_id);
810
- }
811
- }
812
- }
813
- }
814
-
815
- const pendingByCandidateId = new Map(
816
- pendingAssertConflicts.map((pending) => [pending.candidate.candidate_id, pending]),
817
- );
818
- const reanchoredCandidates = candidates
819
- .map((candidate) => {
820
- const pending = pendingByCandidateId.get(candidate.candidate_id);
821
- if (!pending) return candidate;
822
- const finalSource = currentEventFromRecord(records.get(candidate.record_key || candidate.memory_key));
823
- const previousSource = candidate.events?.find(
824
- (event) => event.event_id !== pending.event.event_id,
825
- );
826
- if (!finalSource || !previousSource || finalSource.event_id === previousSource.event_id) {
827
- return candidate;
828
- }
829
- if (!sameCompleteCausalLineage(previousSource, finalSource)
830
- || !supersededTransitively(superseded, previousSource.event_id, finalSource.event_id)) {
831
- return candidate;
832
- }
833
- if (hashMemoryValue(finalSource.value) === hashMemoryValue(pending.event.value)) return null;
834
- return conflictCandidate(candidate.memory_key, [finalSource, pending.event], finalSource);
835
- })
836
- .filter(Boolean);
837
-
838
- const stateEntries = [...records].map(([key, record]) => [key, record.value]);
839
- const recordEntries = [...records].map(([key, record]) => [key, record]);
840
- const tombstoneEntries = [...tombstones];
841
- const state = sortedObject(stateEntries);
842
- const recordObject = sortedObject(recordEntries);
843
- const tombstoneObject = sortedObject(tombstoneEntries);
844
- const unresolvedCandidates = reanchoredCandidates
845
- .filter((item) => !candidateDecisions.has(item.candidate_id)
846
- && !resolvedCandidateIds.has(item.candidate_id));
847
- unresolvedCandidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
848
- superseded.sort((left, right) => left.event_id.localeCompare(right.event_id));
849
- const eventCursor = events.at(-1)?.event_id || 'none';
850
- const stateHash = hashMemoryValue({ state, tombstones: tombstoneObject });
851
- const ambiguousRecordKeys = new Set(
852
- unresolvedCandidates.map((candidate) => candidate.record_key || candidate.memory_key),
853
- );
854
- const activeEvents = [
855
- ...Object.entries(recordObject)
856
- .filter(([recordKey]) => !ambiguousRecordKeys.has(recordKey))
857
- .map(([recordKey, record]) => ({
858
- ...record.source,
859
- memory_key: record.source.memory_key,
860
- projection_key: recordKey,
861
- operation: 'assert',
862
- value: record.value,
863
- })),
864
- ...unresolvedCandidates.map((candidate) => (
865
- conflictReviewEvent(candidate, unresolvedCandidates.length)
866
- )),
867
- ];
868
-
869
- return {
870
- state,
871
- records: recordObject,
872
- candidates: unresolvedCandidates,
873
- tombstones: tombstoneObject,
874
- superseded,
875
- appliedEventIds,
876
- eventIds: events.map((item) => item.event_id),
877
- activeEvents,
878
- revision,
879
- eventCursor,
880
- stateHash,
881
- };
882
- }
883
-
884
- /**
885
- * Re-derive one ledger projection using the explicit invariants declared by CORE.
886
- * `eventCursor` is the reducer's deterministic causal cursor; `ledgerCursor` is the
887
- * physical prefix boundary used by durable checkpoints.
888
- */
889
- export function deriveMemoryProjection(vaultBase, inputEvents = [], {
890
- resolveDeferredAsserts = true,
891
- } = {}) {
892
- const reduced = reduceMemoryEvents(inputEvents, {
893
- coreInvariants: readCoreInvariants(vaultBase), resolveDeferredAsserts,
894
- });
895
- const ledgerCursor = inputEvents.at(-1)?.event_id || 'none';
896
- const checkpoint = {
897
- revision: reduced.revision,
898
- event_cursor: ledgerCursor,
899
- state_hash: reduced.stateHash,
900
- };
901
- if (reduced.eventCursor !== ledgerCursor) {
902
- checkpoint.causal_event_cursor = reduced.eventCursor;
903
- }
904
- return {
905
- ...reduced,
906
- ledgerCursor,
907
- checkpoint,
908
- };
909
- }
910
-
911
- function readOutbox(vaultBase) {
912
- const dir = outboxDir(vaultBase);
913
- let checkedDir = checkedMemoryDirectory(vaultBase, dir, 'outbox de memória');
914
- if (!checkedDir.exists) return [];
915
- checkedDir = checkedMemoryDirectory(vaultBase, checkedDir.target, 'outbox de memória', {
916
- allowMissing: false,
917
- });
918
- const entries = readdirSync(checkedDir.target, { withFileTypes: true });
919
- for (const entry of entries) {
920
- assertVaultPathSafe(vaultBase, join(checkedDir.target, entry.name), {
921
- allowMissing: false,
922
- label: `entrada ${entry.name} do outbox de memória`,
923
- });
924
- }
925
- return entries
926
- .filter((entry) => entry.name.endsWith('.json'))
927
- .sort((left, right) => left.name.localeCompare(right.name))
928
- .map((entry) => {
929
- const path = join(checkedDir.target, entry.name);
930
- try {
931
- const event = assertValidEvent(JSON.parse(readCheckedMemoryFile(
932
- vaultBase, path, 'utf8', `evento ${entry.name} do outbox de memória`,
933
- )), vaultBase);
934
- if (`${event.event_id}.json` !== entry.name) throw new Error('filename does not match event_id');
935
- return { event, path };
936
- } catch (error) {
937
- if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
938
- throw new MemoryOutboxCorruption(path, error);
939
- }
940
- });
941
- }
942
-
943
- function countPendingOutbox(vaultBase) {
944
- const dir = outboxDir(vaultBase);
945
- let checked = checkedMemoryDirectory(vaultBase, dir, 'outbox de memória');
946
- if (!checked.exists) return 0;
947
- checked = checkedMemoryDirectory(vaultBase, checked.target, 'outbox de memória', {
948
- allowMissing: false,
949
- });
950
- return readdirSync(checked.target, { withFileTypes: true })
951
- .filter((entry) => entry.name.endsWith('.json'))
952
- .map((entry) => {
953
- assertVaultPathSafe(vaultBase, join(checked.target, entry.name), {
954
- allowMissing: false,
955
- label: `entrada ${entry.name} do outbox de memória`,
956
- });
957
- return entry;
958
- }).length;
959
- }
960
-
961
- function appendLedgerDurably(vaultBase, path, events) {
962
- if (!events.length) return;
963
- let fd;
964
- try {
965
- let checked = checkedMemoryFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl');
966
- checked = checkedMemoryFile(vaultBase, checked.target, 'ledger MEMORY_EVENTS.jsonl');
967
- fd = openSync(checked.target, 'a');
968
- assertOpenedMemoryFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl');
969
- const payload = events.map((item) => canonicalMemoryJson(item)).join('\n') + '\n';
970
- writeFileSync(fd, payload, 'utf8');
971
- fsyncSync(fd);
972
- assertOpenedMemoryFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl');
973
- } finally {
974
- if (fd !== undefined) closeSync(fd);
975
- }
976
- }
977
-
978
- function writeAtomicIfChanged(vaultBase, path, content, label) {
979
- const checked = checkedMemoryFile(vaultBase, path, label);
980
- if (checked.exists) {
981
- try {
982
- if (readCheckedMemoryFile(vaultBase, checked.target, 'utf8', label) === content) return false;
983
- } catch (error) {
984
- if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
985
- // A contained but unreadable projection may be atomically replaced.
986
- }
987
- }
988
- writeVaultFileAtomic(vaultBase, checked.target, content, 'utf8', { label });
989
- return true;
990
- }
991
-
992
- function injectFault(faultAt, boundary) {
993
- if (faultAt === boundary) throw new Error(`Injected memory-store fault: ${boundary}`);
994
- }
995
-
996
- /** Build every derived byte from an immutable authority snapshot without publishing it. */
997
- export function prepareMemoryProjection(vaultBase, allEvents) {
998
- const reduced = deriveMemoryProjection(vaultBase, allEvents);
999
- const updatedAt = allEvents
1000
- .map((item) => item.observed_at)
1001
- .filter(Boolean)
1002
- .sort()
1003
- .at(-1);
1004
- const shared = renderSharedMemory({
1005
- revision: reduced.revision,
1006
- eventCursor: reduced.ledgerCursor,
1007
- events: reduced.activeEvents,
1008
- stateHash: reduced.stateHash,
1009
- updatedAt,
1010
- });
1011
- const sharedMetadata = parseSharedMemory(shared).metadata;
1012
- const candidates = reduced.candidates.map((item) => canonicalMemoryJson(item)).join('\n')
1013
- + (reduced.candidates.length ? '\n' : '');
1014
- return {
1015
- sharedContent: shared,
1016
- candidatesContent: candidates,
1017
- revision: reduced.revision,
1018
- eventCursor: reduced.eventCursor,
1019
- ledgerCursor: reduced.ledgerCursor,
1020
- stateHash: reduced.stateHash,
1021
- checkpoint: reduced.checkpoint,
1022
- candidates: reduced.candidates.length,
1023
- projectedEvents: sharedMetadata.projected_events ?? reduced.activeEvents.length,
1024
- omittedEvents: sharedMetadata.omitted_events ?? 0,
1025
- };
1026
- }
1027
-
1028
- /** Publish a projection prepared from the same locked authority snapshot. */
1029
- export function publishMemoryProjection(vaultBase, prepared) {
1030
- const {
1031
- sharedContent, candidatesContent, ...projection
1032
- } = prepared;
1033
- // Validate the complete publication set before the first rename: a bad candidates
1034
- // alias cannot leave SHARED partially advanced (and vice versa).
1035
- preflightProjectionTargets(vaultBase);
1036
- const sharedWritten = writeAtomicIfChanged(
1037
- vaultBase, sharedPath(vaultBase), sharedContent, 'projeção SHARED_MEMORY.md',
1038
- );
1039
- const candidatesWritten = writeAtomicIfChanged(
1040
- vaultBase, candidatesPath(vaultBase), candidatesContent, 'projeção MEMORY_CANDIDATES.jsonl',
1041
- );
1042
- return { ...projection, projectionsWritten: sharedWritten || candidatesWritten };
1043
- }
1044
-
1045
- function publishDerivedProjection(vaultBase, allEvents) {
1046
- return publishMemoryProjection(vaultBase, prepareMemoryProjection(vaultBase, allEvents));
1047
- }
1048
-
1049
- // Reconciliation needs to validate proof, perform registry CAS, and publish the exact
1050
- // prepared bytes inside one MEMORY critical section. This primitive deliberately does
1051
- // not create `.brain`: callers must complete their read-only Vault preflight first.
1052
- export const MEMORY_LOCK_BUSY = VAULT_LOCK_BUSY;
1053
- export function withMemoryLock(vaultBase, fn, options = {}) {
1054
- return withVaultPathLock(vaultBase, lockTarget(vaultBase), fn, options);
1055
- }
1056
-
1057
- function projectLocked(vaultBase, { faultAt } = {}) {
1058
- const ledger = readMemoryLedger(vaultBase);
1059
- if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
1060
- const outbox = readOutbox(vaultBase);
1061
- const ledgerById = new Map(ledger.events.map((item) => [item.event_id, item]));
1062
- const newEvents = [];
1063
-
1064
- for (const { event: item } of outbox) {
1065
- const existing = ledgerById.get(item.event_id);
1066
- if (existing) {
1067
- if (canonicalMemoryJson(existing) !== canonicalMemoryJson(item)) throw new MemoryEventCollision(item.event_id);
1068
- continue;
1069
- }
1070
- ledgerById.set(item.event_id, item);
1071
- newEvents.push(item);
1072
- }
1073
-
1074
- const allEvents = [...ledger.events, ...newEvents];
1075
- const prepared = prepareMemoryProjection(vaultBase, allEvents);
1076
- // CORE and both sidecars are checked before the append, so an unsafe derived target
1077
- // cannot partially advance the authoritative ledger.
1078
- preflightProjectionTargets(vaultBase);
1079
- appendLedgerDurably(vaultBase, ledger.path, newEvents);
1080
- injectFault(faultAt, 'after-ledger');
1081
-
1082
- const projection = publishMemoryProjection(vaultBase, prepared);
1083
- injectFault(faultAt, 'after-projection');
1084
-
1085
- const consumedEventIds = [];
1086
- for (const entry of outbox) {
1087
- unlinkVaultFile(vaultBase, entry.path, {
1088
- missingOk: false, label: 'evento consumido do outbox de memória',
1089
- });
1090
- consumedEventIds.push(entry.event.event_id);
1091
- }
1092
- return {
1093
- status: 'projected',
1094
- appended: newEvents.length,
1095
- consumed: outbox.length,
1096
- consumedEventIds,
1097
- pending: 0,
1098
- ...projection,
1099
- };
1100
- }
1101
-
1102
- /** Serialize ledger append + full deterministic replay under .brain/MEMORY.lock. */
1103
- export function projectMemoryOutbox(vaultBase, options = {}) {
1104
- mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
1105
- const pending = countPendingOutbox(vaultBase);
1106
- const result = withVaultPathLock(
1107
- vaultBase,
1108
- lockTarget(vaultBase),
1109
- () => projectLocked(vaultBase, options),
1110
- options.lock || {},
1111
- );
1112
- if (result === VAULT_LOCK_BUSY) return { status: 'busy', pending };
1113
- return result;
1114
- }
1115
-
1116
- /**
1117
- * Rebuild generated projections from the existing ledger only. This path never
1118
- * enumerates, reads, acknowledges, or consumes producer outbox files.
1119
- */
1120
- export function reprojectMemoryLedger(vaultBase, options = {}) {
1121
- mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
1122
- const result = withVaultPathLock(vaultBase, lockTarget(vaultBase), () => {
1123
- const ledger = readMemoryLedger(vaultBase);
1124
- if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
1125
- return {
1126
- status: 'reprojected',
1127
- ...publishDerivedProjection(vaultBase, ledger.events),
1128
- };
1129
- }, options.lock || {});
1130
- if (result === VAULT_LOCK_BUSY) return { status: 'busy' };
1131
- return result;
1132
- }
1133
-
1134
- /** Explicit repair: preserve exact corrupt bytes, then retain every independently valid line. */
1135
- export function repairMemoryLedger(vaultBase, options = {}) {
1136
- mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
1137
- const result = withVaultPathLock(vaultBase, lockTarget(vaultBase), () => {
1138
- const ledger = readMemoryLedger(vaultBase);
1139
- if (ledger.status === 'ok') return { status: 'unchanged', repairedLines: 0, backupPath: null };
1140
- const backupPath = `${ledger.path}.corrupt-${Date.now()}.bak`;
1141
- // copyFileSync preserves the exact evidence before any canonical rewrite.
1142
- checkedMemoryFile(vaultBase, ledger.path, 'ledger corrompido', { allowMissing: false });
1143
- checkedMemoryFile(vaultBase, backupPath, 'backup do ledger corrompido', { mustNotExist: true });
1144
- // Revalidate immediately before the exclusive copy, then verify the created inode.
1145
- checkedMemoryFile(vaultBase, ledger.path, 'ledger corrompido', { allowMissing: false });
1146
- checkedMemoryFile(vaultBase, backupPath, 'backup do ledger corrompido', { mustNotExist: true });
1147
- copyFileSync(ledger.path, backupPath, fsConstants.COPYFILE_EXCL);
1148
- checkedMemoryFile(vaultBase, backupPath, 'backup do ledger corrompido', { allowMissing: false });
1149
- const repaired = ledger.events.map((item) => canonicalMemoryJson(item)).join('\n')
1150
- + (ledger.events.length ? '\n' : '');
1151
- writeVaultFileAtomic(vaultBase, ledger.path, repaired, 'utf8', { label: 'ledger reparado' });
1152
- return {
1153
- status: 'repaired',
1154
- repairedLines: ledger.errors.length,
1155
- retainedEvents: ledger.events.length,
1156
- backupPath,
1157
- };
1158
- }, options.lock || {});
1159
- if (result === VAULT_LOCK_BUSY) return { status: 'busy', repairedLines: 0, backupPath: null };
1160
- return result;
1161
- }
1
+ export * from './memory-store-core.mjs';
2
+ export {
3
+ MEMORY_SNAPSHOT_FILE,
4
+ MEMORY_SNAPSHOT_REDUCER_VERSION,
5
+ MEMORY_SNAPSHOT_SCHEMA_VERSION,
6
+ projectMemoryOutbox,
7
+ readMemoryProjectionSnapshot,
8
+ reprojectMemoryLedger,
9
+ } from './memory-snapshot-store.mjs';
10
+ export {
11
+ MEMORY_SEGMENT_DEFAULT_MAX_BYTES,
12
+ MEMORY_SEGMENT_DEFAULT_MAX_EVENTS,
13
+ MEMORY_SEGMENT_DIRECTORY,
14
+ MEMORY_SEGMENT_MANIFEST_FILE,
15
+ MEMORY_SEGMENT_MANIFEST_SCHEMA_VERSION,
16
+ MEMORY_SEGMENT_SCHEMA_VERSION,
17
+ MemorySegmentCorruption,
18
+ readMemorySegmentManifest,
19
+ repairMemorySegmentManifest,
20
+ sealMemorySegments,
21
+ verifyMemorySegments,
22
+ } from './memory-segment-store.mjs';
23
+ export {
24
+ MEMORY_LEDGER_BACKUP_DIRECTORY,
25
+ MEMORY_LEDGER_GENERATION_FILE,
26
+ MEMORY_LEDGER_GENERATION_SCHEMA_VERSION,
27
+ MEMORY_ROTATION_JOURNAL_FILE,
28
+ MEMORY_ROTATION_RECEIPT_CHECKPOINT_FILE,
29
+ MEMORY_ROTATION_RECEIPTS_FILE,
30
+ MemoryLedgerGenerationCorruption,
31
+ MemoryRotationReceiptCorruption,
32
+ memoryLedgerGenerationStatus,
33
+ readMemoryLedgerGeneration,
34
+ readMemoryRotationJournal,
35
+ readMemoryRotationReceipts,
36
+ } from './memory-ledger-view.mjs';
37
+ export {
38
+ MEMORY_ROTATION_CANDIDATE_PREFIX,
39
+ MEMORY_ROTATION_POLICY,
40
+ MemoryLedgerRotationBlocked,
41
+ compactMemoryLedger,
42
+ memoryLedgerRotationStatus,
43
+ planMemoryLedgerRotation,
44
+ recoverMemoryLedgerRotation,
45
+ rotateMemoryLedger,
46
+ } from './memory-rotation-store.mjs';