wendkeep 0.58.3 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/README.en.md +45 -3
- package/README.md +45 -3
- package/bin/wendkeep.mjs +54 -6
- package/docs/en/commands/changes-and-verification.md +9 -3
- package/docs/en/commands/getting-started.md +7 -3
- package/docs/en/commands/memory.md +20 -2
- package/docs/en/commands/operating-profiles.md +173 -0
- package/docs/en/commands/sessions-and-import.md +8 -4
- package/docs/en/commands/verify.md +12 -6
- package/docs/pt-BR/commands/changes-and-verification.md +9 -4
- package/docs/pt-BR/commands/getting-started.md +7 -3
- package/docs/pt-BR/commands/memory.md +18 -2
- package/docs/pt-BR/commands/operating-profiles.md +171 -0
- package/docs/pt-BR/commands/sessions-and-import.md +7 -3
- package/docs/pt-BR/commands/verify.md +11 -5
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-inject.mjs +83 -26
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/change-context.mjs +24 -10
- package/hooks/change-core.mjs +174 -37
- package/hooks/change-guard.mjs +115 -16
- package/hooks/change-nag.mjs +20 -5
- package/hooks/change-warn.mjs +27 -9
- package/hooks/decision-capture.mjs +1 -1
- package/hooks/derived-sections.mjs +1 -1
- package/hooks/flow-core.mjs +891 -0
- package/hooks/flow-protected-policy.mjs +218 -0
- package/hooks/frontmatter-repair.mjs +3 -1
- package/hooks/git-snapshot.mjs +722 -0
- package/hooks/import-sessions.mjs +10 -5
- package/hooks/memory-mode.mjs +63 -13
- package/hooks/memory-store.mjs +309 -69
- package/hooks/obsidian-common.mjs +39 -55
- package/hooks/operating-profile-runtime.mjs +157 -0
- package/hooks/plan-capture.mjs +14 -3
- package/hooks/sensors-core.mjs +15 -3
- package/hooks/session-backfill.mjs +7 -2
- package/hooks/session-ensure.mjs +6 -4
- package/hooks/session-iteration.mjs +65 -0
- package/hooks/session-memory-lifecycle.mjs +10 -5
- package/hooks/session-note-io.mjs +130 -15
- package/hooks/session-observability.mjs +4 -2
- package/hooks/session-stop.mjs +65 -19
- package/hooks/spec-core.mjs +91 -12
- package/hooks/subagent-stop.mjs +4 -1
- package/hooks/subagent-usage.mjs +2 -2
- package/hooks/task-log.mjs +3 -1
- package/hooks/token-usage.mjs +1 -1
- package/hooks/vault-health.mjs +183 -37
- package/hooks/vault-path-safety.mjs +2 -0
- package/hooks/vault-runtime-store.mjs +558 -0
- package/package.json +10 -3
- package/packages/cli/package.json +5 -0
- package/packages/harness/package.json +5 -0
- package/packages/integrations/package.json +5 -0
- package/packages/mcp/package.json +5 -0
- package/packages/pi/package.json +5 -0
- package/packages/vault/package.json +6 -0
- package/packages/vault/src/index.mjs +2 -0
- package/packages/vault/src/project-vault.mjs +327 -0
- package/packages/vault/src/vault-path-safety.mjs +558 -0
- package/src/change.mjs +2 -1
- package/src/flow.mjs +232 -0
- package/src/init.mjs +26 -3
- package/src/memory.mjs +785 -35
- package/src/operating-profile.mjs +133 -0
- package/src/profile.mjs +224 -0
- package/src/project-vault.mjs +2 -221
- package/src/rebuild-costs.mjs +11 -4
- package/src/skills-seed.mjs +38 -16
- package/src/sync-defs.mjs +16 -7
- package/src/sync.mjs +9 -1
- package/src/taxonomy.mjs +8 -0
- package/src/validate-memory.mjs +21 -8
- package/src/verify.mjs +12 -2
package/hooks/memory-store.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// ledger, then atomically publishes SHARED_MEMORY.md and MEMORY_CANDIDATES.jsonl.
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import {
|
|
8
|
-
closeSync, copyFileSync,
|
|
9
|
-
readdirSync,
|
|
8
|
+
closeSync, constants as fsConstants, copyFileSync, fstatSync, fsyncSync, openSync,
|
|
9
|
+
readFileSync, readdirSync, statSync, writeFileSync,
|
|
10
10
|
} from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import {
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
sanitizeMemoryText,
|
|
15
15
|
validateMemoryEvent,
|
|
16
16
|
} from './memory-schema.mjs';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assertVaultPathSafe, assertVaultPathsSafe, mkdirVaultPath, unlinkVaultFile,
|
|
19
|
+
VAULT_LOCK_BUSY, withVaultPathLock, writeVaultFileAtomic,
|
|
20
|
+
} from './vault-path-safety.mjs';
|
|
18
21
|
|
|
19
22
|
const EVENT_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
20
23
|
|
|
@@ -74,6 +77,59 @@ function lockTarget(vaultBase) {
|
|
|
74
77
|
return join(brainDir(vaultBase), 'MEMORY');
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
function unsafeMemoryPath(message) {
|
|
81
|
+
const error = new Error(message);
|
|
82
|
+
error.code = 'VAULT_PATH_UNSAFE';
|
|
83
|
+
return error;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkedMemoryFile(vaultBase, path, label, {
|
|
87
|
+
allowMissing = true,
|
|
88
|
+
mustNotExist = false,
|
|
89
|
+
} = {}) {
|
|
90
|
+
return assertVaultPathSafe(vaultBase, path, {
|
|
91
|
+
allowMissing,
|
|
92
|
+
expectedType: 'file',
|
|
93
|
+
mustNotExist,
|
|
94
|
+
label,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkedMemoryDirectory(vaultBase, path, label, { allowMissing = true } = {}) {
|
|
99
|
+
return assertVaultPathSafe(vaultBase, path, {
|
|
100
|
+
allowMissing,
|
|
101
|
+
expectedType: 'directory',
|
|
102
|
+
label,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function readCheckedMemoryFile(vaultBase, path, encoding, label, { allowMissing = false } = {}) {
|
|
107
|
+
let checked = checkedMemoryFile(vaultBase, path, label, { allowMissing });
|
|
108
|
+
if (!checked.exists) return null;
|
|
109
|
+
// Deliberately adjacent to readFileSync: aliases created since the first policy check
|
|
110
|
+
// are rejected before Node opens the artifact.
|
|
111
|
+
checked = checkedMemoryFile(vaultBase, checked.target, label, { allowMissing: false });
|
|
112
|
+
return readFileSync(checked.target, encoding);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertOpenedMemoryFile(vaultBase, path, fd, label) {
|
|
116
|
+
const checked = checkedMemoryFile(vaultBase, path, label, { allowMissing: false });
|
|
117
|
+
const descriptor = fstatSync(fd);
|
|
118
|
+
const target = statSync(checked.target);
|
|
119
|
+
if (!descriptor.isFile() || descriptor.nlink > 1 || target.nlink > 1
|
|
120
|
+
|| descriptor.dev !== target.dev || descriptor.ino !== target.ino) {
|
|
121
|
+
throw unsafeMemoryPath(`${label} mudou de inode ou possui hardlink antes da mutação: ${checked.target}`);
|
|
122
|
+
}
|
|
123
|
+
return checked.target;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function preflightProjectionTargets(vaultBase) {
|
|
127
|
+
return assertVaultPathsSafe(vaultBase, [
|
|
128
|
+
{ path: sharedPath(vaultBase), expectedType: 'file', label: 'projeção SHARED_MEMORY.md' },
|
|
129
|
+
{ path: candidatesPath(vaultBase), expectedType: 'file', label: 'projeção MEMORY_CANDIDATES.jsonl' },
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
|
|
77
133
|
function canonicalize(value) {
|
|
78
134
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
79
135
|
if (value && typeof value === 'object') {
|
|
@@ -112,8 +168,13 @@ function sanitizeEvent(event) {
|
|
|
112
168
|
}
|
|
113
169
|
|
|
114
170
|
function projectIdForVault(vaultBase) {
|
|
171
|
+
const path = join(brainDir(vaultBase), 'PROJECT.json');
|
|
172
|
+
const raw = readCheckedMemoryFile(vaultBase, path, 'utf8', 'autoridade PROJECT.json', {
|
|
173
|
+
allowMissing: true,
|
|
174
|
+
});
|
|
175
|
+
if (raw === null) return undefined;
|
|
115
176
|
try {
|
|
116
|
-
const project = JSON.parse(
|
|
177
|
+
const project = JSON.parse(raw);
|
|
117
178
|
return typeof project.projectId === 'string' && project.projectId ? project.projectId : undefined;
|
|
118
179
|
} catch {
|
|
119
180
|
return undefined;
|
|
@@ -142,6 +203,34 @@ function eventHash(event) {
|
|
|
142
203
|
return sha256(canonicalMemoryJson(event));
|
|
143
204
|
}
|
|
144
205
|
|
|
206
|
+
const OUTBOX_PUBLICATION_WAIT_MS = 500;
|
|
207
|
+
const OUTBOX_PUBLICATION_POLL_MS = 5;
|
|
208
|
+
const OUTBOX_PUBLICATION_SIGNAL = new Int32Array(new SharedArrayBuffer(4));
|
|
209
|
+
|
|
210
|
+
function readConcurrentOutboxEvent(vaultBase, path, eventId) {
|
|
211
|
+
const deadline = Date.now() + OUTBOX_PUBLICATION_WAIT_MS;
|
|
212
|
+
while (true) {
|
|
213
|
+
let raw;
|
|
214
|
+
try {
|
|
215
|
+
raw = readCheckedMemoryFile(
|
|
216
|
+
vaultBase, path, 'utf8', 'evento imutável preexistente do outbox',
|
|
217
|
+
);
|
|
218
|
+
} catch (cause) {
|
|
219
|
+
if (cause?.code === 'VAULT_PATH_UNSAFE') throw cause;
|
|
220
|
+
throw new MemoryEventCollision(eventId, `Existing outbox event is unreadable: ${eventId}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(raw);
|
|
225
|
+
} catch (cause) {
|
|
226
|
+
if (!(cause instanceof SyntaxError) || Date.now() >= deadline) {
|
|
227
|
+
throw new MemoryEventCollision(eventId, `Existing outbox event is unreadable: ${eventId}`);
|
|
228
|
+
}
|
|
229
|
+
Atomics.wait(OUTBOX_PUBLICATION_SIGNAL, 0, 0, OUTBOX_PUBLICATION_POLL_MS);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
145
234
|
/**
|
|
146
235
|
* Persist one immutable producer event using exclusive creation. A retry with the same
|
|
147
236
|
* canonical payload is a no-op; reusing the ID for different bytes is an observable error.
|
|
@@ -149,25 +238,26 @@ function eventHash(event) {
|
|
|
149
238
|
export function enqueueMemoryEvent(vaultBase, event) {
|
|
150
239
|
const checked = assertValidEvent(event, vaultBase);
|
|
151
240
|
const dir = outboxDir(vaultBase);
|
|
152
|
-
|
|
241
|
+
mkdirVaultPath(vaultBase, dir, { label: 'outbox de memória' });
|
|
153
242
|
const path = join(dir, `${checked.event_id}.json`);
|
|
154
243
|
const payload = `${canonicalMemoryJson(checked)}\n`;
|
|
155
244
|
const hash = eventHash(checked);
|
|
156
245
|
|
|
157
246
|
let fd;
|
|
158
247
|
try {
|
|
248
|
+
checkedMemoryFile(vaultBase, path, 'evento imutável do outbox');
|
|
249
|
+
// The final lstat is deliberately adjacent to the exclusive open. A pre-created
|
|
250
|
+
// symlink/hardlink loses the wx race and is revalidated in the EEXIST branch.
|
|
251
|
+
checkedMemoryFile(vaultBase, path, 'evento imutável do outbox');
|
|
159
252
|
fd = openSync(path, 'wx');
|
|
253
|
+
assertOpenedMemoryFile(vaultBase, path, fd, 'evento imutável do outbox');
|
|
160
254
|
writeFileSync(fd, payload, 'utf8');
|
|
161
255
|
fsyncSync(fd);
|
|
256
|
+
assertOpenedMemoryFile(vaultBase, path, fd, 'evento imutável do outbox');
|
|
162
257
|
return { status: 'enqueued', path, eventId: checked.event_id, hash };
|
|
163
258
|
} catch (error) {
|
|
164
259
|
if (error?.code !== 'EEXIST') throw error;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
existing = JSON.parse(readFileSync(path, 'utf8'));
|
|
168
|
-
} catch (cause) {
|
|
169
|
-
throw new MemoryEventCollision(checked.event_id, `Existing outbox event is unreadable: ${checked.event_id}`);
|
|
170
|
-
}
|
|
260
|
+
const existing = readConcurrentOutboxEvent(vaultBase, path, checked.event_id);
|
|
171
261
|
if (eventHash(existing) !== hash || canonicalMemoryJson(existing) !== canonicalMemoryJson(checked)) {
|
|
172
262
|
throw new MemoryEventCollision(checked.event_id);
|
|
173
263
|
}
|
|
@@ -184,10 +274,13 @@ function ledgerError(line, message, partial = false) {
|
|
|
184
274
|
/** Read a ledger without hiding a valid prefix when its tail is corrupt or partial. */
|
|
185
275
|
export function readMemoryLedger(vaultBase) {
|
|
186
276
|
const path = ledgerPath(vaultBase);
|
|
187
|
-
|
|
277
|
+
const checked = checkedMemoryFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl');
|
|
278
|
+
if (!checked.exists) {
|
|
188
279
|
return { status: 'ok', path, raw: '', events: [], eventIds: new Set(), errors: [] };
|
|
189
280
|
}
|
|
190
|
-
const raw =
|
|
281
|
+
const raw = readCheckedMemoryFile(
|
|
282
|
+
vaultBase, checked.target, 'utf8', 'ledger MEMORY_EVENTS.jsonl',
|
|
283
|
+
).replace(/\r\n/g, '\n');
|
|
191
284
|
const lines = raw.split('\n');
|
|
192
285
|
const hasPartialTail = raw.length > 0 && !raw.endsWith('\n');
|
|
193
286
|
if (!hasPartialTail) lines.pop();
|
|
@@ -245,8 +338,14 @@ function eventOrder(left, right) {
|
|
|
245
338
|
|| String(left.event_id).localeCompare(String(right.event_id));
|
|
246
339
|
}
|
|
247
340
|
|
|
341
|
+
function sameCausalActivation(left, right) {
|
|
342
|
+
return Boolean(left?.canonical_session_id)
|
|
343
|
+
&& left.canonical_session_id === right?.canonical_session_id
|
|
344
|
+
&& left.activation_id === right?.activation_id;
|
|
345
|
+
}
|
|
346
|
+
|
|
248
347
|
function comparable(left, right) {
|
|
249
|
-
if (left
|
|
348
|
+
if (sameCausalActivation(left, right)) return true;
|
|
250
349
|
const leftSupersedes = left.supersedes_event_id || left.supersedes;
|
|
251
350
|
const rightSupersedes = right.supersedes_event_id || right.supersedes;
|
|
252
351
|
return leftSupersedes === right.event_id || rightSupersedes === left.event_id
|
|
@@ -268,12 +367,10 @@ function candidateId(reason, memoryKey, eventIds) {
|
|
|
268
367
|
// participates in precedence so the projector never guesses meaning from human text:
|
|
269
368
|
// <!-- wk-memory: release.push="manual-only" -->
|
|
270
369
|
function readCoreInvariants(vaultBase) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return new Map();
|
|
276
|
-
}
|
|
370
|
+
const core = readCheckedMemoryFile(
|
|
371
|
+
vaultBase, corePath(vaultBase), 'utf8', 'autoridade CORE.md', { allowMissing: true },
|
|
372
|
+
);
|
|
373
|
+
if (core === null) return new Map();
|
|
277
374
|
const invariants = new Map();
|
|
278
375
|
const marker = /^<!--\s*wk-memory:\s*([A-Za-z0-9][A-Za-z0-9._-]*)=(.+)\s*-->$/;
|
|
279
376
|
for (const line of core.split('\n')) {
|
|
@@ -333,7 +430,7 @@ function currentEventFromRecord(record) {
|
|
|
333
430
|
|
|
334
431
|
function isCausallyOlder(event, current) {
|
|
335
432
|
if (!current) return false;
|
|
336
|
-
if (event
|
|
433
|
+
if (sameCausalActivation(event, current)) {
|
|
337
434
|
return Number(event.turn_sequence) < Number(current.turn_sequence);
|
|
338
435
|
}
|
|
339
436
|
if (Number.isInteger(event.activation_epoch) && Number.isInteger(current.activation_epoch)
|
|
@@ -419,6 +516,15 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
419
516
|
|
|
420
517
|
if (item.operation === 'assert') {
|
|
421
518
|
if (current && hashMemoryValue(current.value) !== hashMemoryValue(item.value)) {
|
|
519
|
+
if (sameCausalActivation(item, currentSource)
|
|
520
|
+
&& Number(item.turn_sequence) > Number(currentSource.turn_sequence)) {
|
|
521
|
+
records.set(item.memory_key, { value: item.value, revision: current.revision + 1, source: item });
|
|
522
|
+
tombstones.delete(item.memory_key);
|
|
523
|
+
superseded.push({ event_id: currentSource.event_id, by_event_id: item.event_id });
|
|
524
|
+
revision += 1;
|
|
525
|
+
appliedEventIds.push(item.event_id);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
422
528
|
candidates.push(conflictCandidate(item.memory_key, [currentEventFromRecord(current), item]));
|
|
423
529
|
continue;
|
|
424
530
|
}
|
|
@@ -524,42 +630,107 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
524
630
|
};
|
|
525
631
|
}
|
|
526
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Re-derive one ledger projection using the explicit invariants declared by CORE.
|
|
635
|
+
* `eventCursor` is the reducer's deterministic causal cursor; `ledgerCursor` is the
|
|
636
|
+
* physical prefix boundary used by durable checkpoints.
|
|
637
|
+
*/
|
|
638
|
+
export function deriveMemoryProjection(vaultBase, inputEvents = []) {
|
|
639
|
+
const reduced = reduceMemoryEvents(inputEvents, { coreInvariants: readCoreInvariants(vaultBase) });
|
|
640
|
+
const ledgerCursor = inputEvents.at(-1)?.event_id || 'none';
|
|
641
|
+
const checkpoint = {
|
|
642
|
+
revision: reduced.revision,
|
|
643
|
+
event_cursor: ledgerCursor,
|
|
644
|
+
state_hash: reduced.stateHash,
|
|
645
|
+
};
|
|
646
|
+
if (reduced.eventCursor !== ledgerCursor) {
|
|
647
|
+
checkpoint.causal_event_cursor = reduced.eventCursor;
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
...reduced,
|
|
651
|
+
ledgerCursor,
|
|
652
|
+
checkpoint,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
527
656
|
function readOutbox(vaultBase) {
|
|
528
657
|
const dir = outboxDir(vaultBase);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
658
|
+
let checkedDir = checkedMemoryDirectory(vaultBase, dir, 'outbox de memória');
|
|
659
|
+
if (!checkedDir.exists) return [];
|
|
660
|
+
checkedDir = checkedMemoryDirectory(vaultBase, checkedDir.target, 'outbox de memória', {
|
|
661
|
+
allowMissing: false,
|
|
662
|
+
});
|
|
663
|
+
const entries = readdirSync(checkedDir.target, { withFileTypes: true });
|
|
664
|
+
for (const entry of entries) {
|
|
665
|
+
assertVaultPathSafe(vaultBase, join(checkedDir.target, entry.name), {
|
|
666
|
+
allowMissing: false,
|
|
667
|
+
label: `entrada ${entry.name} do outbox de memória`,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return entries
|
|
671
|
+
.filter((entry) => entry.name.endsWith('.json'))
|
|
532
672
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
533
673
|
.map((entry) => {
|
|
534
|
-
const path = join(
|
|
674
|
+
const path = join(checkedDir.target, entry.name);
|
|
535
675
|
try {
|
|
536
|
-
const event = assertValidEvent(JSON.parse(
|
|
676
|
+
const event = assertValidEvent(JSON.parse(readCheckedMemoryFile(
|
|
677
|
+
vaultBase, path, 'utf8', `evento ${entry.name} do outbox de memória`,
|
|
678
|
+
)), vaultBase);
|
|
537
679
|
if (`${event.event_id}.json` !== entry.name) throw new Error('filename does not match event_id');
|
|
538
680
|
return { event, path };
|
|
539
681
|
} catch (error) {
|
|
682
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
540
683
|
throw new MemoryOutboxCorruption(path, error);
|
|
541
684
|
}
|
|
542
685
|
});
|
|
543
686
|
}
|
|
544
687
|
|
|
545
|
-
function
|
|
688
|
+
function countPendingOutbox(vaultBase) {
|
|
689
|
+
const dir = outboxDir(vaultBase);
|
|
690
|
+
let checked = checkedMemoryDirectory(vaultBase, dir, 'outbox de memória');
|
|
691
|
+
if (!checked.exists) return 0;
|
|
692
|
+
checked = checkedMemoryDirectory(vaultBase, checked.target, 'outbox de memória', {
|
|
693
|
+
allowMissing: false,
|
|
694
|
+
});
|
|
695
|
+
return readdirSync(checked.target, { withFileTypes: true })
|
|
696
|
+
.filter((entry) => entry.name.endsWith('.json'))
|
|
697
|
+
.map((entry) => {
|
|
698
|
+
assertVaultPathSafe(vaultBase, join(checked.target, entry.name), {
|
|
699
|
+
allowMissing: false,
|
|
700
|
+
label: `entrada ${entry.name} do outbox de memória`,
|
|
701
|
+
});
|
|
702
|
+
return entry;
|
|
703
|
+
}).length;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function appendLedgerDurably(vaultBase, path, events) {
|
|
546
707
|
if (!events.length) return;
|
|
547
708
|
let fd;
|
|
548
709
|
try {
|
|
549
|
-
|
|
710
|
+
let checked = checkedMemoryFile(vaultBase, path, 'ledger MEMORY_EVENTS.jsonl');
|
|
711
|
+
checked = checkedMemoryFile(vaultBase, checked.target, 'ledger MEMORY_EVENTS.jsonl');
|
|
712
|
+
fd = openSync(checked.target, 'a');
|
|
713
|
+
assertOpenedMemoryFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl');
|
|
550
714
|
const payload = events.map((item) => canonicalMemoryJson(item)).join('\n') + '\n';
|
|
551
715
|
writeFileSync(fd, payload, 'utf8');
|
|
552
716
|
fsyncSync(fd);
|
|
717
|
+
assertOpenedMemoryFile(vaultBase, checked.target, fd, 'ledger MEMORY_EVENTS.jsonl');
|
|
553
718
|
} finally {
|
|
554
719
|
if (fd !== undefined) closeSync(fd);
|
|
555
720
|
}
|
|
556
721
|
}
|
|
557
722
|
|
|
558
|
-
function writeAtomicIfChanged(path, content) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
723
|
+
function writeAtomicIfChanged(vaultBase, path, content, label) {
|
|
724
|
+
const checked = checkedMemoryFile(vaultBase, path, label);
|
|
725
|
+
if (checked.exists) {
|
|
726
|
+
try {
|
|
727
|
+
if (readCheckedMemoryFile(vaultBase, checked.target, 'utf8', label) === content) return false;
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
730
|
+
// A contained but unreadable projection may be atomically replaced.
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
writeVaultFileAtomic(vaultBase, checked.target, content, 'utf8', { label });
|
|
563
734
|
return true;
|
|
564
735
|
}
|
|
565
736
|
|
|
@@ -567,6 +738,64 @@ function injectFault(faultAt, boundary) {
|
|
|
567
738
|
if (faultAt === boundary) throw new Error(`Injected memory-store fault: ${boundary}`);
|
|
568
739
|
}
|
|
569
740
|
|
|
741
|
+
/** Build every derived byte from an immutable authority snapshot without publishing it. */
|
|
742
|
+
export function prepareMemoryProjection(vaultBase, allEvents) {
|
|
743
|
+
const reduced = deriveMemoryProjection(vaultBase, allEvents);
|
|
744
|
+
const updatedAt = allEvents
|
|
745
|
+
.map((item) => item.observed_at)
|
|
746
|
+
.filter(Boolean)
|
|
747
|
+
.sort()
|
|
748
|
+
.at(-1);
|
|
749
|
+
const shared = renderSharedMemory({
|
|
750
|
+
revision: reduced.revision,
|
|
751
|
+
eventCursor: reduced.ledgerCursor,
|
|
752
|
+
events: reduced.activeEvents,
|
|
753
|
+
stateHash: reduced.stateHash,
|
|
754
|
+
updatedAt,
|
|
755
|
+
});
|
|
756
|
+
const candidates = reduced.candidates.map((item) => canonicalMemoryJson(item)).join('\n')
|
|
757
|
+
+ (reduced.candidates.length ? '\n' : '');
|
|
758
|
+
return {
|
|
759
|
+
sharedContent: shared,
|
|
760
|
+
candidatesContent: candidates,
|
|
761
|
+
revision: reduced.revision,
|
|
762
|
+
eventCursor: reduced.eventCursor,
|
|
763
|
+
ledgerCursor: reduced.ledgerCursor,
|
|
764
|
+
stateHash: reduced.stateHash,
|
|
765
|
+
checkpoint: reduced.checkpoint,
|
|
766
|
+
candidates: reduced.candidates.length,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Publish a projection prepared from the same locked authority snapshot. */
|
|
771
|
+
export function publishMemoryProjection(vaultBase, prepared) {
|
|
772
|
+
const {
|
|
773
|
+
sharedContent, candidatesContent, ...projection
|
|
774
|
+
} = prepared;
|
|
775
|
+
// Validate the complete publication set before the first rename: a bad candidates
|
|
776
|
+
// alias cannot leave SHARED partially advanced (and vice versa).
|
|
777
|
+
preflightProjectionTargets(vaultBase);
|
|
778
|
+
const sharedWritten = writeAtomicIfChanged(
|
|
779
|
+
vaultBase, sharedPath(vaultBase), sharedContent, 'projeção SHARED_MEMORY.md',
|
|
780
|
+
);
|
|
781
|
+
const candidatesWritten = writeAtomicIfChanged(
|
|
782
|
+
vaultBase, candidatesPath(vaultBase), candidatesContent, 'projeção MEMORY_CANDIDATES.jsonl',
|
|
783
|
+
);
|
|
784
|
+
return { ...projection, projectionsWritten: sharedWritten || candidatesWritten };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function publishDerivedProjection(vaultBase, allEvents) {
|
|
788
|
+
return publishMemoryProjection(vaultBase, prepareMemoryProjection(vaultBase, allEvents));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Reconciliation needs to validate proof, perform registry CAS, and publish the exact
|
|
792
|
+
// prepared bytes inside one MEMORY critical section. This primitive deliberately does
|
|
793
|
+
// not create `.brain`: callers must complete their read-only Vault preflight first.
|
|
794
|
+
export const MEMORY_LOCK_BUSY = VAULT_LOCK_BUSY;
|
|
795
|
+
export function withMemoryLock(vaultBase, fn, options = {}) {
|
|
796
|
+
return withVaultPathLock(vaultBase, lockTarget(vaultBase), fn, options);
|
|
797
|
+
}
|
|
798
|
+
|
|
570
799
|
function projectLocked(vaultBase, { faultAt } = {}) {
|
|
571
800
|
const ledger = readMemoryLedger(vaultBase);
|
|
572
801
|
if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
|
|
@@ -584,70 +813,81 @@ function projectLocked(vaultBase, { faultAt } = {}) {
|
|
|
584
813
|
newEvents.push(item);
|
|
585
814
|
}
|
|
586
815
|
|
|
587
|
-
|
|
816
|
+
const allEvents = [...ledger.events, ...newEvents];
|
|
817
|
+
const prepared = prepareMemoryProjection(vaultBase, allEvents);
|
|
818
|
+
// CORE and both sidecars are checked before the append, so an unsafe derived target
|
|
819
|
+
// cannot partially advance the authoritative ledger.
|
|
820
|
+
preflightProjectionTargets(vaultBase);
|
|
821
|
+
appendLedgerDurably(vaultBase, ledger.path, newEvents);
|
|
588
822
|
injectFault(faultAt, 'after-ledger');
|
|
589
823
|
|
|
590
|
-
const
|
|
591
|
-
const reduced = reduceMemoryEvents(allEvents, { coreInvariants: readCoreInvariants(vaultBase) });
|
|
592
|
-
const updatedAt = allEvents
|
|
593
|
-
.map((item) => item.observed_at)
|
|
594
|
-
.filter(Boolean)
|
|
595
|
-
.sort()
|
|
596
|
-
.at(-1);
|
|
597
|
-
const shared = renderSharedMemory({
|
|
598
|
-
revision: reduced.revision,
|
|
599
|
-
eventCursor: reduced.eventCursor,
|
|
600
|
-
events: reduced.activeEvents,
|
|
601
|
-
stateHash: reduced.stateHash,
|
|
602
|
-
updatedAt,
|
|
603
|
-
});
|
|
604
|
-
const candidates = reduced.candidates.map((item) => canonicalMemoryJson(item)).join('\n')
|
|
605
|
-
+ (reduced.candidates.length ? '\n' : '');
|
|
606
|
-
const sharedWritten = writeAtomicIfChanged(sharedPath(vaultBase), shared);
|
|
607
|
-
const candidatesWritten = writeAtomicIfChanged(candidatesPath(vaultBase), candidates);
|
|
824
|
+
const projection = publishMemoryProjection(vaultBase, prepared);
|
|
608
825
|
injectFault(faultAt, 'after-projection');
|
|
609
826
|
|
|
610
|
-
for (const entry of outbox)
|
|
827
|
+
for (const entry of outbox) {
|
|
828
|
+
unlinkVaultFile(vaultBase, entry.path, {
|
|
829
|
+
missingOk: false, label: 'evento consumido do outbox de memória',
|
|
830
|
+
});
|
|
831
|
+
}
|
|
611
832
|
return {
|
|
612
833
|
status: 'projected',
|
|
613
834
|
appended: newEvents.length,
|
|
614
835
|
consumed: outbox.length,
|
|
615
836
|
pending: 0,
|
|
616
|
-
|
|
617
|
-
eventCursor: reduced.eventCursor,
|
|
618
|
-
stateHash: reduced.stateHash,
|
|
619
|
-
candidates: reduced.candidates.length,
|
|
620
|
-
projectionsWritten: sharedWritten || candidatesWritten,
|
|
837
|
+
...projection,
|
|
621
838
|
};
|
|
622
839
|
}
|
|
623
840
|
|
|
624
841
|
/** Serialize ledger append + full deterministic replay under .brain/MEMORY.lock. */
|
|
625
842
|
export function projectMemoryOutbox(vaultBase, options = {}) {
|
|
626
|
-
|
|
627
|
-
const pending =
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const result = withPathLock(
|
|
843
|
+
mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
|
|
844
|
+
const pending = countPendingOutbox(vaultBase);
|
|
845
|
+
const result = withVaultPathLock(
|
|
846
|
+
vaultBase,
|
|
631
847
|
lockTarget(vaultBase),
|
|
632
848
|
() => projectLocked(vaultBase, options),
|
|
633
849
|
options.lock || {},
|
|
634
850
|
);
|
|
635
|
-
if (result ===
|
|
851
|
+
if (result === VAULT_LOCK_BUSY) return { status: 'busy', pending };
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Rebuild generated projections from the existing ledger only. This path never
|
|
857
|
+
* enumerates, reads, acknowledges, or consumes producer outbox files.
|
|
858
|
+
*/
|
|
859
|
+
export function reprojectMemoryLedger(vaultBase, options = {}) {
|
|
860
|
+
mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
|
|
861
|
+
const result = withVaultPathLock(vaultBase, lockTarget(vaultBase), () => {
|
|
862
|
+
const ledger = readMemoryLedger(vaultBase);
|
|
863
|
+
if (ledger.status !== 'ok') throw new MemoryLedgerCorruption(ledger.errors);
|
|
864
|
+
return {
|
|
865
|
+
status: 'reprojected',
|
|
866
|
+
...publishDerivedProjection(vaultBase, ledger.events),
|
|
867
|
+
};
|
|
868
|
+
}, options.lock || {});
|
|
869
|
+
if (result === VAULT_LOCK_BUSY) return { status: 'busy' };
|
|
636
870
|
return result;
|
|
637
871
|
}
|
|
638
872
|
|
|
639
873
|
/** Explicit repair: preserve exact corrupt bytes, then retain every independently valid line. */
|
|
640
874
|
export function repairMemoryLedger(vaultBase, options = {}) {
|
|
641
|
-
|
|
642
|
-
const result =
|
|
875
|
+
mkdirVaultPath(vaultBase, brainDir(vaultBase), { label: 'raiz .brain da memória' });
|
|
876
|
+
const result = withVaultPathLock(vaultBase, lockTarget(vaultBase), () => {
|
|
643
877
|
const ledger = readMemoryLedger(vaultBase);
|
|
644
878
|
if (ledger.status === 'ok') return { status: 'unchanged', repairedLines: 0, backupPath: null };
|
|
645
879
|
const backupPath = `${ledger.path}.corrupt-${Date.now()}.bak`;
|
|
646
880
|
// copyFileSync preserves the exact evidence before any canonical rewrite.
|
|
647
|
-
|
|
881
|
+
checkedMemoryFile(vaultBase, ledger.path, 'ledger corrompido', { allowMissing: false });
|
|
882
|
+
checkedMemoryFile(vaultBase, backupPath, 'backup do ledger corrompido', { mustNotExist: true });
|
|
883
|
+
// Revalidate immediately before the exclusive copy, then verify the created inode.
|
|
884
|
+
checkedMemoryFile(vaultBase, ledger.path, 'ledger corrompido', { allowMissing: false });
|
|
885
|
+
checkedMemoryFile(vaultBase, backupPath, 'backup do ledger corrompido', { mustNotExist: true });
|
|
886
|
+
copyFileSync(ledger.path, backupPath, fsConstants.COPYFILE_EXCL);
|
|
887
|
+
checkedMemoryFile(vaultBase, backupPath, 'backup do ledger corrompido', { allowMissing: false });
|
|
648
888
|
const repaired = ledger.events.map((item) => canonicalMemoryJson(item)).join('\n')
|
|
649
889
|
+ (ledger.events.length ? '\n' : '');
|
|
650
|
-
|
|
890
|
+
writeVaultFileAtomic(vaultBase, ledger.path, repaired, 'utf8', { label: 'ledger reparado' });
|
|
651
891
|
return {
|
|
652
892
|
status: 'repaired',
|
|
653
893
|
repairedLines: ledger.errors.length,
|
|
@@ -655,6 +895,6 @@ export function repairMemoryLedger(vaultBase, options = {}) {
|
|
|
655
895
|
backupPath,
|
|
656
896
|
};
|
|
657
897
|
}, options.lock || {});
|
|
658
|
-
if (result ===
|
|
898
|
+
if (result === VAULT_LOCK_BUSY) return { status: 'busy', repairedLines: 0, backupPath: null };
|
|
659
899
|
return result;
|
|
660
900
|
}
|