wendkeep 0.66.1 → 0.66.4
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 +36 -0
- package/README.en.md +18 -5
- package/README.md +18 -5
- package/docs/en/commands/memory.md +46 -3
- package/docs/pt-BR/commands/memory.md +46 -2
- package/hooks/vault-health.mjs +6 -0
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -0
- package/packages/vault/src/memory-store.mjs +77 -5
- package/src/memory.mjs +763 -12
package/src/memory.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import {
|
|
3
3
|
constants as fsConstants, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync,
|
|
4
|
-
rmSync, writeFileSync,
|
|
4
|
+
readdirSync, rmSync, statSync, writeFileSync,
|
|
5
5
|
} from 'node:fs';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { sanitizeMemoryText, renderSharedMemory, validateSharedMemory } from '../hooks/memory-schema.mjs';
|
|
@@ -263,6 +263,80 @@ function assertCompatibleDecision(prior, { action, eventId }) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
function promotedMemoryValue(value) {
|
|
267
|
+
return typeof value === 'string' ? sanitizeMemoryText(value) : cloneJson(value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function eventSupersedes(event) {
|
|
271
|
+
if (Array.isArray(event?.supersedes)) return event.supersedes;
|
|
272
|
+
return event?.supersedes_event_id ? [event.supersedes_event_id] : [];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function hasCompleteCausalIdentity(event) {
|
|
276
|
+
return Boolean(event?.canonical_session_id && event?.activation_id && event?.source_turn_id)
|
|
277
|
+
&& Number.isInteger(event.activation_epoch)
|
|
278
|
+
&& Number.isInteger(event.turn_sequence);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sameCausalLineage(left, right) {
|
|
282
|
+
return hasCompleteCausalIdentity(left) && hasCompleteCausalIdentity(right)
|
|
283
|
+
&& left.canonical_session_id === right.canonical_session_id
|
|
284
|
+
&& left.activation_id === right.activation_id
|
|
285
|
+
&& left.activation_epoch === right.activation_epoch;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function promotedSupersedes(vault, candidate, selected) {
|
|
289
|
+
const memberIds = new Set(Array.isArray(candidate.event_ids) ? candidate.event_ids : []);
|
|
290
|
+
const ledger = readMemoryLedger(vault);
|
|
291
|
+
if (ledger.status !== 'ok') throw new Error('Ledger de memória inválido durante a promoção.');
|
|
292
|
+
const current = deriveMemoryProjection(vault, ledger.events)
|
|
293
|
+
.records?.[candidate.memory_key]?.source;
|
|
294
|
+
if (!current?.event_id || memberIds.has(current.event_id)) return [...memberIds].sort();
|
|
295
|
+
|
|
296
|
+
const currentSelectedId = current.candidate_decision?.selected_event_id;
|
|
297
|
+
const currentSelected = currentSelectedId
|
|
298
|
+
? ledger.events.find((event) => event.event_id === currentSelectedId)
|
|
299
|
+
: null;
|
|
300
|
+
const selectedLedger = ledger.events.find((event) => event.event_id === selected.event_id);
|
|
301
|
+
const currentAncestors = eventSupersedes(current);
|
|
302
|
+
const decisionMembers = Array.isArray(current.candidate_decision?.event_ids)
|
|
303
|
+
? current.candidate_decision.event_ids
|
|
304
|
+
: [];
|
|
305
|
+
const canonicalAncestors = [...new Set(currentAncestors)].sort();
|
|
306
|
+
const canonicalDecisionMembers = [...new Set(decisionMembers)].sort();
|
|
307
|
+
const currentObserved = String(current.effective_at || current.observed_at || '');
|
|
308
|
+
const selectedObserved = String(selectedLedger?.effective_at || selectedLedger?.observed_at || '');
|
|
309
|
+
const currentPhysicalIndex = ledger.events.findIndex((event) => event.event_id === current.event_id);
|
|
310
|
+
const selectedPhysicalIndex = ledger.events.findIndex((event) => event.event_id === selected.event_id);
|
|
311
|
+
const bridgeChecks = {
|
|
312
|
+
legacyShape: current.operation === 'replace' && current.authority === 'verified'
|
|
313
|
+
&& !current.canonical_session_id && !current.source_turn_id,
|
|
314
|
+
promotion: current.candidate_decision?.action === 'promote',
|
|
315
|
+
selectedPresent: Boolean(currentSelected),
|
|
316
|
+
selectedPersisted: Boolean(selectedLedger),
|
|
317
|
+
exactDecisionMembers: canonicalAncestors.length === currentAncestors.length
|
|
318
|
+
&& canonicalDecisionMembers.length === decisionMembers.length
|
|
319
|
+
&& canonicalAncestors.length === canonicalDecisionMembers.length
|
|
320
|
+
&& canonicalAncestors.every((eventId, index) => eventId === canonicalDecisionMembers[index]),
|
|
321
|
+
selectedSuperseded: currentAncestors.includes(currentSelectedId)
|
|
322
|
+
&& decisionMembers.includes(currentSelectedId),
|
|
323
|
+
candidateAncestor: currentAncestors.some((eventId) => memberIds.has(eventId)),
|
|
324
|
+
sameLineage: sameCausalLineage(currentSelected, selectedLedger),
|
|
325
|
+
laterTurn: selectedLedger?.turn_sequence > currentSelected?.turn_sequence,
|
|
326
|
+
appendedAfterCurrent: selectedPhysicalIndex > currentPhysicalIndex && currentPhysicalIndex >= 0,
|
|
327
|
+
observedBeforeCurrent: selectedObserved < currentObserved,
|
|
328
|
+
};
|
|
329
|
+
const bridgesObservedOrder = Object.values(bridgeChecks).every(Boolean);
|
|
330
|
+
if (!bridgesObservedOrder) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Candidate ${candidate.candidate_id} não corresponde mais à projeção causal atual; `
|
|
333
|
+
+ 'recarregue os candidates antes de promover.',
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
memberIds.add(current.event_id);
|
|
337
|
+
return [...memberIds].sort();
|
|
338
|
+
}
|
|
339
|
+
|
|
266
340
|
function matchesPromotedAttempt(attempt, selected) {
|
|
267
341
|
if (attempt?.memory_mode !== 'v2' || attempt.state !== 'projected'
|
|
268
342
|
|| !Array.isArray(attempt.event_ids) || !attempt.event_ids.includes(selected.event_id)) return false;
|
|
@@ -378,6 +452,9 @@ export function decideMemoryCandidate(vault, {
|
|
|
378
452
|
if (action === 'promote' && selectedValue === undefined) {
|
|
379
453
|
throw new Error(`Candidate ${candidateId} não contém valor promovível.`);
|
|
380
454
|
}
|
|
455
|
+
const supersedes = action === 'promote' && selected
|
|
456
|
+
? promotedSupersedes(vault, candidate, selected)
|
|
457
|
+
: [];
|
|
381
458
|
const now = new Date().toISOString();
|
|
382
459
|
const decision = {
|
|
383
460
|
candidate_id: candidateId,
|
|
@@ -391,15 +468,19 @@ export function decideMemoryCandidate(vault, {
|
|
|
391
468
|
project_id: projectId(vault),
|
|
392
469
|
memory_key: action === 'promote' ? candidate.memory_key : `candidate.decision.${candidateId}`,
|
|
393
470
|
operation: action === 'promote' && selected ? 'replace' : 'assert',
|
|
394
|
-
value:
|
|
471
|
+
value: action === 'promote' ? promotedMemoryValue(selectedValue) : 'rejected',
|
|
395
472
|
authority: 'verified',
|
|
473
|
+
...(selected?.canonical_session_id
|
|
474
|
+
? { canonical_session_id: selected.canonical_session_id }
|
|
475
|
+
: {}),
|
|
396
476
|
activation_id: selected?.activation_id || 'wendkeep-memory-cli',
|
|
397
477
|
...(Number.isInteger(selected?.activation_epoch) ? { activation_epoch: selected.activation_epoch } : {}),
|
|
398
478
|
turn_sequence: selected?.turn_sequence ?? 0,
|
|
479
|
+
...(selected?.source_turn_id ? { source_turn_id: selected.source_turn_id } : {}),
|
|
399
480
|
observed_at: now,
|
|
400
481
|
evidence: [`candidate:${candidateId}`],
|
|
401
482
|
candidate_decision: decision,
|
|
402
|
-
...(selected ? { supersedes
|
|
483
|
+
...(selected ? { supersedes } : {}),
|
|
403
484
|
};
|
|
404
485
|
enqueueMemoryEvent(vault, event);
|
|
405
486
|
const projection = projectMemoryOutbox(vault);
|
|
@@ -457,6 +538,520 @@ function memoryCheckpointFingerprint(entry) {
|
|
|
457
538
|
}));
|
|
458
539
|
}
|
|
459
540
|
|
|
541
|
+
function fieldSnapshot(value, key) {
|
|
542
|
+
const present = Object.prototype.hasOwnProperty.call(value || {}, key);
|
|
543
|
+
return { present, value: present ? (value[key] ?? null) : null };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function attemptAuthorityFingerprint(entry, attempt) {
|
|
547
|
+
const activation = entry?.activations?.[attempt?.activation_id];
|
|
548
|
+
return hash(canonicalMemoryJson({
|
|
549
|
+
status: fieldSnapshot(entry, 'status'),
|
|
550
|
+
active_activation_id: fieldSnapshot(entry, 'active_activation_id'),
|
|
551
|
+
activation_epoch: fieldSnapshot(entry, 'activation_epoch'),
|
|
552
|
+
last_turn_id: fieldSnapshot(entry, 'last_turn_id'),
|
|
553
|
+
last_turn_sequence: fieldSnapshot(entry, 'last_turn_sequence'),
|
|
554
|
+
turn_sequence: fieldSnapshot(entry?.turn_sequences, attempt?.turn_id),
|
|
555
|
+
activation: {
|
|
556
|
+
status: fieldSnapshot(activation, 'status'),
|
|
557
|
+
epoch: fieldSnapshot(activation, 'epoch'),
|
|
558
|
+
last_stop_turn_id: fieldSnapshot(activation, 'last_stop_turn_id'),
|
|
559
|
+
last_stop_turn_sequence: fieldSnapshot(activation, 'last_stop_turn_sequence'),
|
|
560
|
+
last_turn_sequence: fieldSnapshot(activation, 'last_turn_sequence'),
|
|
561
|
+
},
|
|
562
|
+
memory_status: fieldSnapshot(entry, 'memory_status'),
|
|
563
|
+
memory_activation_id: fieldSnapshot(entry, 'memory_activation_id'),
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function ownsAttemptContext(entry, attempt) {
|
|
568
|
+
const activation = entry?.activations?.[attempt?.activation_id];
|
|
569
|
+
return entry?.status === 'active'
|
|
570
|
+
&& entry.active_activation_id === attempt.activation_id
|
|
571
|
+
&& entry.activation_epoch === attempt.activation_epoch
|
|
572
|
+
&& activation?.status === 'active'
|
|
573
|
+
&& activation.epoch === attempt.activation_epoch
|
|
574
|
+
&& entry.last_turn_id === attempt.turn_id
|
|
575
|
+
&& entry.last_turn_sequence === attempt.turn_sequence
|
|
576
|
+
&& entry.turn_sequences?.[attempt.turn_id] === attempt.turn_sequence
|
|
577
|
+
&& activation.last_stop_turn_id === attempt.turn_id
|
|
578
|
+
&& activation.last_stop_turn_sequence === attempt.turn_sequence
|
|
579
|
+
&& activation.last_turn_sequence === attempt.turn_sequence
|
|
580
|
+
&& entry.memory_status === attempt.state
|
|
581
|
+
&& entry.memory_activation_id === attempt.activation_id;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function attemptEventMatches(event, attempt, expectedProjectId) {
|
|
585
|
+
return event?.project_id === expectedProjectId
|
|
586
|
+
&& event.canonical_session_id === attempt.canonical_session_id
|
|
587
|
+
&& event.activation_id === attempt.activation_id
|
|
588
|
+
&& event.activation_epoch === attempt.activation_epoch
|
|
589
|
+
&& event.source_turn_id === attempt.turn_id
|
|
590
|
+
&& event.turn_sequence === attempt.turn_sequence;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function readAttemptOutboxEvents(vault, eventIds) {
|
|
594
|
+
const events = [];
|
|
595
|
+
for (const eventId of eventIds) {
|
|
596
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(eventId)) return null;
|
|
597
|
+
const path = brainPath(vault, join('memory-outbox', `${eventId}.json`));
|
|
598
|
+
const checked = checkedVaultFile(vault, path, `outbox do attempt ${eventId}`);
|
|
599
|
+
if (!checked.exists) return null;
|
|
600
|
+
try {
|
|
601
|
+
const event = JSON.parse(readVaultFile(
|
|
602
|
+
vault, checked.target, 'utf8', `outbox do attempt ${eventId}`,
|
|
603
|
+
));
|
|
604
|
+
if (event?.event_id !== eventId) return null;
|
|
605
|
+
events.push(event);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error?.code === 'VAULT_PATH_UNSAFE') throw error;
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return events;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function targetOutboxIsAbsent(vault, eventIds) {
|
|
615
|
+
return eventIds.every((eventId) => {
|
|
616
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(eventId)) return false;
|
|
617
|
+
const path = brainPath(vault, join('memory-outbox', `${eventId}.json`));
|
|
618
|
+
return !checkedVaultFile(vault, path, `outbox do attempt ${eventId}`).exists;
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function freezeRepairAttemptAcknowledgements(vault, options = {}) {
|
|
623
|
+
const result = withMemoryLock(vault, () => {
|
|
624
|
+
const expectedProjectId = projectId(vault);
|
|
625
|
+
const registry = readSessionRegistry(vault);
|
|
626
|
+
const frozen = [];
|
|
627
|
+
let pending = false;
|
|
628
|
+
for (const [sessionId, entry] of Object.entries(registry.sessions || {})) {
|
|
629
|
+
const attempt = entry?.last_memory_attempt;
|
|
630
|
+
const eventIds = Array.isArray(attempt?.event_ids) ? [...attempt.event_ids] : [];
|
|
631
|
+
if (attempt?.memory_mode !== 'v2' || attempt.disposition !== 'applied'
|
|
632
|
+
|| !['enqueued', 'degraded'].includes(attempt.state)) continue;
|
|
633
|
+
pending = true;
|
|
634
|
+
if (!eventIds.length || new Set(eventIds).size !== eventIds.length
|
|
635
|
+
|| eventIds.some((eventId) => typeof eventId !== 'string' || !eventId)
|
|
636
|
+
|| sessionId !== attempt.canonical_session_id
|
|
637
|
+
|| !ownsAttemptContext(entry, attempt)) continue;
|
|
638
|
+
const outboxEvents = readAttemptOutboxEvents(vault, eventIds);
|
|
639
|
+
if (!outboxEvents
|
|
640
|
+
|| outboxEvents.some((event) => !attemptEventMatches(event, attempt, expectedProjectId))) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
frozen.push({
|
|
644
|
+
sessionId,
|
|
645
|
+
projectId: expectedProjectId,
|
|
646
|
+
eventIds,
|
|
647
|
+
eventFingerprints: new Map(outboxEvents.map((event) => [
|
|
648
|
+
event.event_id, hash(canonicalMemoryJson(event)),
|
|
649
|
+
])),
|
|
650
|
+
attemptFingerprint: attemptFingerprint(attempt),
|
|
651
|
+
checkpointFingerprint: memoryCheckpointFingerprint(entry),
|
|
652
|
+
authorityFingerprint: attemptAuthorityFingerprint(entry, attempt),
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (pending) {
|
|
656
|
+
const ledger = readLedgerForValidation(vault, { projectId: expectedProjectId });
|
|
657
|
+
if (!ledger.ok) {
|
|
658
|
+
throw new Error(`Ledger físico inválido para acknowledgement de repair: ${(ledger.errors || []).join(' ')}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return { attempts: frozen, pending };
|
|
662
|
+
}, options.memoryLock || {});
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function publishedProjectionMatches(vault, events, projection) {
|
|
667
|
+
const prepared = prepareMemoryProjection(vault, events);
|
|
668
|
+
if (!sameCheckpoint(prepared.checkpoint, projection?.checkpoint)) return false;
|
|
669
|
+
const shared = readVaultFile(
|
|
670
|
+
vault, brainPath(vault, SHARED), 'utf8', 'projeção SHARED_MEMORY.md',
|
|
671
|
+
);
|
|
672
|
+
const candidates = readVaultFile(
|
|
673
|
+
vault, brainPath(vault, CANDIDATES), 'utf8', 'projeção MEMORY_CANDIDATES.jsonl',
|
|
674
|
+
);
|
|
675
|
+
return shared === prepared.sharedContent && candidates === prepared.candidatesContent;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function acknowledgeRepairAttempts(vault, frozen, projection, options = {}) {
|
|
679
|
+
const receipt = new Set(Array.isArray(projection?.consumedEventIds)
|
|
680
|
+
? projection.consumedEventIds
|
|
681
|
+
: []);
|
|
682
|
+
const covered = frozen.filter((item) => item.eventIds.every((eventId) => receipt.has(eventId)));
|
|
683
|
+
if (!covered.length) {
|
|
684
|
+
return {
|
|
685
|
+
status: 'unchanged', eligible: frozen.length, acknowledged: 0, stale: frozen.length,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const outcome = withMemoryLock(vault, () => {
|
|
690
|
+
if (options.beforeAttemptAcknowledgement) options.beforeAttemptAcknowledgement();
|
|
691
|
+
const ledger = readMemoryLedger(vault);
|
|
692
|
+
if (ledger.status !== 'ok' || !publishedProjectionMatches(vault, ledger.events, projection)) {
|
|
693
|
+
return {
|
|
694
|
+
status: 'attention', eligible: frozen.length, acknowledged: 0, stale: frozen.length,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const byId = new Map(ledger.events.map((event) => [event.event_id, event]));
|
|
698
|
+
const valid = covered.filter((item) => {
|
|
699
|
+
if (projectId(vault) !== item.projectId || !targetOutboxIsAbsent(vault, item.eventIds)) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
return item.eventIds.every((eventId) => {
|
|
703
|
+
const event = byId.get(eventId);
|
|
704
|
+
return event
|
|
705
|
+
&& item.eventFingerprints.get(eventId) === hash(canonicalMemoryJson(event));
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
if (!valid.length) {
|
|
709
|
+
return {
|
|
710
|
+
status: 'attention', eligible: frozen.length, acknowledged: 0, stale: frozen.length,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const acknowledged = mutateSessionRegistry(vault, (registry) => {
|
|
715
|
+
const sessionIds = [];
|
|
716
|
+
for (const item of valid) {
|
|
717
|
+
const entry = registry.sessions?.[item.sessionId];
|
|
718
|
+
const attempt = entry?.last_memory_attempt;
|
|
719
|
+
if (!attempt || attemptFingerprint(attempt) !== item.attemptFingerprint
|
|
720
|
+
|| memoryCheckpointFingerprint(entry) !== item.checkpointFingerprint
|
|
721
|
+
|| attemptAuthorityFingerprint(entry, attempt) !== item.authorityFingerprint
|
|
722
|
+
|| !ownsAttemptContext(entry, attempt)
|
|
723
|
+
|| item.eventIds.some((eventId) => !attemptEventMatches(
|
|
724
|
+
byId.get(eventId), attempt, item.projectId,
|
|
725
|
+
))) continue;
|
|
726
|
+
attempt.state = 'projected';
|
|
727
|
+
attempt.checkpoint = cloneJson(projection.checkpoint);
|
|
728
|
+
entry.memory_status = 'projected';
|
|
729
|
+
entry.memory_checkpoint = cloneJson(projection.checkpoint);
|
|
730
|
+
sessionIds.push(item.sessionId);
|
|
731
|
+
}
|
|
732
|
+
return sessionIds;
|
|
733
|
+
});
|
|
734
|
+
return {
|
|
735
|
+
status: acknowledged.length === valid.length ? 'acknowledged' : 'attention',
|
|
736
|
+
eligible: frozen.length,
|
|
737
|
+
acknowledged: acknowledged.length,
|
|
738
|
+
stale: frozen.length - acknowledged.length,
|
|
739
|
+
sessionIds: acknowledged,
|
|
740
|
+
};
|
|
741
|
+
}, options.memoryLock || {});
|
|
742
|
+
return outcome === MEMORY_LOCK_BUSY
|
|
743
|
+
? {
|
|
744
|
+
status: 'busy', eligible: frozen.length, acknowledged: 0, stale: frozen.length,
|
|
745
|
+
}
|
|
746
|
+
: outcome;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function normalizeProjectedAttemptRecoveryRequest({ sessionId } = {}) {
|
|
750
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
751
|
+
if (!normalizedSessionId) {
|
|
752
|
+
throw new TypeError('sessionId é obrigatório para recuperar attempt projetado.');
|
|
753
|
+
}
|
|
754
|
+
return { sessionId: normalizedSessionId };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function readStrictSessionRegistry(vault) {
|
|
758
|
+
const path = registryPath(vault);
|
|
759
|
+
const checked = checkedVaultFile(vault, path, 'SESSION_REGISTRY da recuperação', {
|
|
760
|
+
allowMissing: false,
|
|
761
|
+
});
|
|
762
|
+
const generationBefore = filesystemGeneration(checked.target);
|
|
763
|
+
let bytes;
|
|
764
|
+
try {
|
|
765
|
+
bytes = readVaultFile(vault, checked.target, undefined, 'SESSION_REGISTRY da recuperação');
|
|
766
|
+
} catch (error) {
|
|
767
|
+
throw new Error(`SESSION_REGISTRY ausente ou ilegível para recuperação: ${error?.message || error}`);
|
|
768
|
+
}
|
|
769
|
+
const generationAfter = filesystemGeneration(checked.target);
|
|
770
|
+
if (canonicalMemoryJson(generationBefore) !== canonicalMemoryJson(generationAfter)) {
|
|
771
|
+
throw new Error('SESSION_REGISTRY mudou durante o preflight da recuperação targeted.');
|
|
772
|
+
}
|
|
773
|
+
let registry;
|
|
774
|
+
try {
|
|
775
|
+
registry = JSON.parse(bytes.toString('utf8'));
|
|
776
|
+
} catch (error) {
|
|
777
|
+
throw new Error(`SESSION_REGISTRY contém JSON inválido para recuperação: ${error?.message || error}`);
|
|
778
|
+
}
|
|
779
|
+
if (!registry || typeof registry !== 'object' || Array.isArray(registry)
|
|
780
|
+
|| !Number.isInteger(registry.version) || registry.version < 2
|
|
781
|
+
|| !registry.sessions || typeof registry.sessions !== 'object'
|
|
782
|
+
|| Array.isArray(registry.sessions)) {
|
|
783
|
+
throw new Error('SESSION_REGISTRY inválido para recuperação targeted.');
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
registry,
|
|
787
|
+
registryHash: hash(canonicalMemoryJson(registry)),
|
|
788
|
+
registryGeneration: generationAfter,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function filesystemGeneration(path) {
|
|
793
|
+
const stat = statSync(path, { bigint: true });
|
|
794
|
+
return {
|
|
795
|
+
dev: String(stat.dev),
|
|
796
|
+
ino: String(stat.ino),
|
|
797
|
+
size: String(stat.size),
|
|
798
|
+
mtimeNs: String(stat.mtimeNs),
|
|
799
|
+
ctimeNs: String(stat.ctimeNs),
|
|
800
|
+
birthtimeNs: String(stat.birthtimeNs),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function projectedRecoveryOutboxProof(vault) {
|
|
805
|
+
const path = brainPath(vault, 'memory-outbox');
|
|
806
|
+
const checked = assertVaultPathSafe(vault, path, {
|
|
807
|
+
allowMissing: true,
|
|
808
|
+
expectedType: 'directory',
|
|
809
|
+
label: 'diretório memory-outbox da recuperação',
|
|
810
|
+
});
|
|
811
|
+
if (!checked.exists) return { exists: false, entries: [] };
|
|
812
|
+
|
|
813
|
+
const namesBefore = readdirSync(checked.target).sort();
|
|
814
|
+
const entries = namesBefore.map((name) => {
|
|
815
|
+
const memberPath = join(checked.target, name);
|
|
816
|
+
const member = checkedVaultFile(
|
|
817
|
+
vault, memberPath, `membro ${name} da memory-outbox`, { allowMissing: false },
|
|
818
|
+
);
|
|
819
|
+
const bytes = readVaultFile(
|
|
820
|
+
vault, member.target, undefined, `membro ${name} da memory-outbox`,
|
|
821
|
+
);
|
|
822
|
+
let event;
|
|
823
|
+
try {
|
|
824
|
+
event = JSON.parse(bytes.toString('utf8'));
|
|
825
|
+
} catch (error) {
|
|
826
|
+
throw new Error(`Membro ${name} da memory-outbox contém JSON inválido: ${error?.message || error}`);
|
|
827
|
+
}
|
|
828
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)
|
|
829
|
+
|| typeof event.event_id !== 'string' || !event.event_id.trim()) {
|
|
830
|
+
throw new Error(`Membro ${name} da memory-outbox não contém event_id válido.`);
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
name,
|
|
834
|
+
eventId: event.event_id,
|
|
835
|
+
generation: filesystemGeneration(member.target),
|
|
836
|
+
hash: byteHash(bytes),
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
const namesAfter = readdirSync(checked.target).sort();
|
|
840
|
+
if (canonicalMemoryJson(namesBefore) !== canonicalMemoryJson(namesAfter)) {
|
|
841
|
+
throw new Error('memory-outbox mudou durante o preflight da recuperação targeted.');
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
exists: true,
|
|
845
|
+
generation: filesystemGeneration(checked.target),
|
|
846
|
+
entries,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function recoveryContextFingerprint(entry, attempt) {
|
|
851
|
+
return hash(canonicalMemoryJson({
|
|
852
|
+
status: fieldSnapshot(entry, 'status'),
|
|
853
|
+
session_id: fieldSnapshot(entry, 'session_id'),
|
|
854
|
+
activation_id: fieldSnapshot(entry, 'activation_id'),
|
|
855
|
+
active_activation_id: fieldSnapshot(entry, 'active_activation_id'),
|
|
856
|
+
activation_epoch: fieldSnapshot(entry, 'activation_epoch'),
|
|
857
|
+
last_turn_id: fieldSnapshot(entry, 'last_turn_id'),
|
|
858
|
+
last_turn_sequence: fieldSnapshot(entry, 'last_turn_sequence'),
|
|
859
|
+
turn_sequences: cloneJson(entry?.turn_sequences || null),
|
|
860
|
+
activation: cloneJson(entry?.activations?.[attempt?.activation_id] || null),
|
|
861
|
+
memory_status: fieldSnapshot(entry, 'memory_status'),
|
|
862
|
+
memory_activation_id: fieldSnapshot(entry, 'memory_activation_id'),
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function recoveryRegistryProof(entry, attempt) {
|
|
867
|
+
return {
|
|
868
|
+
attemptFingerprint: attemptFingerprint(attempt),
|
|
869
|
+
contextFingerprint: recoveryContextFingerprint(entry, attempt),
|
|
870
|
+
checkpointFingerprint: memoryCheckpointFingerprint(entry),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function validStoredProjectedCheckpoint(vault, authority, attempt, entry) {
|
|
875
|
+
if (!checkpointShape(attempt?.checkpoint)
|
|
876
|
+
|| !sameCheckpoint(attempt.checkpoint, entry?.memory_checkpoint)) return false;
|
|
877
|
+
const cursorIndex = authority.ledgerEvents
|
|
878
|
+
.findIndex((event) => event.event_id === attempt.checkpoint.event_cursor);
|
|
879
|
+
if (cursorIndex < 0) return false;
|
|
880
|
+
const prefix = authority.ledgerEvents.slice(0, cursorIndex + 1);
|
|
881
|
+
const prefixIds = new Set(prefix.map((event) => event.event_id));
|
|
882
|
+
if (attempt.event_ids.some((eventId) => !prefixIds.has(eventId))) return false;
|
|
883
|
+
return sameCheckpoint(
|
|
884
|
+
attempt.checkpoint,
|
|
885
|
+
prepareMemoryProjection(vault, prefix).checkpoint,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function prepareProjectedAttemptRecovery(vault, rawRequest) {
|
|
890
|
+
const request = normalizeProjectedAttemptRecoveryRequest(rawRequest);
|
|
891
|
+
const authority = readMemoryAuthority(vault);
|
|
892
|
+
const registrySnapshot = readStrictSessionRegistry(vault);
|
|
893
|
+
const { registry } = registrySnapshot;
|
|
894
|
+
const outboxProof = projectedRecoveryOutboxProof(vault);
|
|
895
|
+
const entry = registry.sessions[request.sessionId];
|
|
896
|
+
if (!entry) throw new Error(`Sessão não encontrada para recuperação: ${request.sessionId}`);
|
|
897
|
+
|
|
898
|
+
const attempt = entry.last_memory_attempt;
|
|
899
|
+
const eventIds = Array.isArray(attempt?.event_ids) ? [...attempt.event_ids] : [];
|
|
900
|
+
if (attempt?.memory_mode !== 'v2' || attempt?.disposition !== 'applied'
|
|
901
|
+
|| !['enqueued', 'degraded', 'projected'].includes(attempt?.state)) {
|
|
902
|
+
throw new Error(`Attempt da sessão ${request.sessionId} não é v2/applied recuperável.`);
|
|
903
|
+
}
|
|
904
|
+
if (!eventIds.length
|
|
905
|
+
|| eventIds.some((eventId) => typeof eventId !== 'string' || !eventId.trim())
|
|
906
|
+
|| new Set(eventIds).size !== eventIds.length) {
|
|
907
|
+
throw new Error('Attempt recuperável deve possuir event_ids não vazios e únicos.');
|
|
908
|
+
}
|
|
909
|
+
if (request.sessionId !== attempt.canonical_session_id
|
|
910
|
+
|| (Object.prototype.hasOwnProperty.call(entry, 'session_id')
|
|
911
|
+
&& entry.session_id !== request.sessionId)
|
|
912
|
+
|| !ownsAttemptContext(entry, attempt)) {
|
|
913
|
+
throw new Error('Contexto causal do attempt não pertence mais à sessão ativa.');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const eventIndexes = eventIds.map((eventId) => (
|
|
917
|
+
authority.ledgerEvents.findIndex((event) => event.event_id === eventId)
|
|
918
|
+
));
|
|
919
|
+
if (eventIndexes.some((index) => index < 0)) {
|
|
920
|
+
throw new Error('Attempt recuperável referencia event_ids ausentes do ledger.');
|
|
921
|
+
}
|
|
922
|
+
for (const eventId of eventIds) {
|
|
923
|
+
if (!attemptEventMatches(authority.ledgerById.get(eventId), attempt, authority.projectId)) {
|
|
924
|
+
throw new Error(`Identidade causal inválida no evento ${eventId}.`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const lastAttemptEventIndex = Math.max(...eventIndexes);
|
|
928
|
+
if (authority.ledgerEvents.slice(lastAttemptEventIndex + 1)
|
|
929
|
+
.some((event) => event.canonical_session_id === request.sessionId)) {
|
|
930
|
+
throw new Error('Existe evento posterior da mesma sessão; attempt histórico não pode ser recuperado.');
|
|
931
|
+
}
|
|
932
|
+
if (!targetOutboxIsAbsent(vault, eventIds)
|
|
933
|
+
|| outboxProof.entries.some((item) => eventIds.includes(item.eventId))) {
|
|
934
|
+
throw new Error('A outbox ainda contém evento alvo; recuperação targeted recusada.');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const projection = prepareMemoryProjection(vault, authority.ledgerEvents);
|
|
938
|
+
const sharedBytes = readVaultFile(
|
|
939
|
+
vault, brainPath(vault, SHARED), undefined, 'projeção SHARED_MEMORY.md',
|
|
940
|
+
);
|
|
941
|
+
const candidatesBytes = readVaultFile(
|
|
942
|
+
vault, brainPath(vault, CANDIDATES), undefined, 'projeção MEMORY_CANDIDATES.jsonl',
|
|
943
|
+
);
|
|
944
|
+
if (!sharedBytes.equals(Buffer.from(projection.sharedContent))
|
|
945
|
+
|| !candidatesBytes.equals(Buffer.from(projection.candidatesContent))) {
|
|
946
|
+
throw new Error('SHARED/candidates divergem da autoridade integral do ledger.');
|
|
947
|
+
}
|
|
948
|
+
assertAuthorityMatches(authority, readMemoryAuthority(vault));
|
|
949
|
+
|
|
950
|
+
const alreadyProjected = attempt.state === 'projected';
|
|
951
|
+
if (alreadyProjected && !validStoredProjectedCheckpoint(vault, authority, attempt, entry)) {
|
|
952
|
+
throw new Error('Attempt projected não possui checkpoint armazenado válido.');
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
request,
|
|
957
|
+
eligible: !alreadyProjected,
|
|
958
|
+
alreadyProjected,
|
|
959
|
+
checkpoint: cloneJson(alreadyProjected ? attempt.checkpoint : projection.checkpoint),
|
|
960
|
+
proof: {
|
|
961
|
+
projectId: authority.projectId,
|
|
962
|
+
projectHash: authority.projectHash,
|
|
963
|
+
coreHash: authority.coreHash,
|
|
964
|
+
ledgerHash: authority.ledgerHash,
|
|
965
|
+
sharedHash: byteHash(sharedBytes),
|
|
966
|
+
candidatesHash: byteHash(candidatesBytes),
|
|
967
|
+
outboxProof,
|
|
968
|
+
registryHash: registrySnapshot.registryHash,
|
|
969
|
+
registryGeneration: registrySnapshot.registryGeneration,
|
|
970
|
+
eventIds,
|
|
971
|
+
eventFingerprints: eventIds.map((eventId) => (
|
|
972
|
+
hash(canonicalMemoryJson(authority.ledgerById.get(eventId)))
|
|
973
|
+
)),
|
|
974
|
+
...recoveryRegistryProof(entry, attempt),
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function projectedAttemptRecoveryResult(prepared, status) {
|
|
980
|
+
return {
|
|
981
|
+
status,
|
|
982
|
+
eligible: prepared.eligible,
|
|
983
|
+
sessionId: prepared.request.sessionId,
|
|
984
|
+
checkpoint: cloneJson(prepared.checkpoint),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function assertProjectedAttemptRecoveryProof(expected, actual) {
|
|
989
|
+
if (canonicalMemoryJson(expected.proof) !== canonicalMemoryJson(actual.proof)
|
|
990
|
+
|| expected.alreadyProjected !== actual.alreadyProjected
|
|
991
|
+
|| !sameCheckpoint(expected.checkpoint, actual.checkpoint)) {
|
|
992
|
+
throw new Error('CAS perdido: autoridade, attempt, contexto causal ou checkpoint mudou.');
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export function inspectProjectedAttemptRecovery(vault, { sessionId } = {}) {
|
|
997
|
+
const prepared = prepareProjectedAttemptRecovery(vault, { sessionId });
|
|
998
|
+
return projectedAttemptRecoveryResult(
|
|
999
|
+
prepared, prepared.alreadyProjected ? 'unchanged' : 'eligible',
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function recoverProjectedAttempt(vault, {
|
|
1004
|
+
sessionId,
|
|
1005
|
+
apply = false,
|
|
1006
|
+
beforeRegistryMutation,
|
|
1007
|
+
memoryLock = {},
|
|
1008
|
+
} = {}) {
|
|
1009
|
+
const prepared = prepareProjectedAttemptRecovery(vault, { sessionId });
|
|
1010
|
+
if (prepared.alreadyProjected) {
|
|
1011
|
+
return projectedAttemptRecoveryResult(prepared, 'unchanged');
|
|
1012
|
+
}
|
|
1013
|
+
if (!apply) return projectedAttemptRecoveryResult(prepared, 'dry-run');
|
|
1014
|
+
|
|
1015
|
+
const outcome = withMemoryLock(vault, () => {
|
|
1016
|
+
const locked = prepareProjectedAttemptRecovery(vault, prepared.request);
|
|
1017
|
+
assertProjectedAttemptRecoveryProof(prepared, locked);
|
|
1018
|
+
if (beforeRegistryMutation) beforeRegistryMutation();
|
|
1019
|
+
const current = prepareProjectedAttemptRecovery(vault, prepared.request);
|
|
1020
|
+
assertProjectedAttemptRecoveryProof(locked, current);
|
|
1021
|
+
|
|
1022
|
+
mutateSessionRegistry(vault, (registry) => {
|
|
1023
|
+
const entry = registry.sessions?.[current.request.sessionId];
|
|
1024
|
+
const attempt = entry?.last_memory_attempt;
|
|
1025
|
+
const expectedRegistryProof = {
|
|
1026
|
+
attemptFingerprint: current.proof.attemptFingerprint,
|
|
1027
|
+
contextFingerprint: current.proof.contextFingerprint,
|
|
1028
|
+
checkpointFingerprint: current.proof.checkpointFingerprint,
|
|
1029
|
+
};
|
|
1030
|
+
if (!entry || !attempt
|
|
1031
|
+
|| hash(canonicalMemoryJson(registry)) !== current.proof.registryHash
|
|
1032
|
+
|| canonicalMemoryJson(filesystemGeneration(registryPath(vault)))
|
|
1033
|
+
!== canonicalMemoryJson(current.proof.registryGeneration)
|
|
1034
|
+
|| canonicalMemoryJson(recoveryRegistryProof(entry, attempt))
|
|
1035
|
+
!== canonicalMemoryJson(expectedRegistryProof)) {
|
|
1036
|
+
throw new Error('CAS perdido: registry mudou antes do acknowledgement targeted.');
|
|
1037
|
+
}
|
|
1038
|
+
attempt.state = 'projected';
|
|
1039
|
+
attempt.checkpoint = cloneJson(current.checkpoint);
|
|
1040
|
+
entry.memory_status = 'projected';
|
|
1041
|
+
entry.memory_activation_id = attempt.activation_id;
|
|
1042
|
+
entry.memory_checkpoint = cloneJson(current.checkpoint);
|
|
1043
|
+
});
|
|
1044
|
+
return projectedAttemptRecoveryResult(current, 'applied');
|
|
1045
|
+
}, memoryLock);
|
|
1046
|
+
|
|
1047
|
+
if (outcome === MEMORY_LOCK_BUSY) {
|
|
1048
|
+
const error = new Error('MEMORY.lock indisponível; recuperação targeted não foi aplicada.');
|
|
1049
|
+
error.code = 'WENDKEEP_MEMORY_LOCK_BUSY';
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
return outcome;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
460
1055
|
function matchingAppliedReconciliation(entry, request) {
|
|
461
1056
|
const attempt = entry?.last_memory_attempt;
|
|
462
1057
|
if (attempt?.memory_mode !== 'v2' || attempt?.state !== 'skipped' || attempt?.disposition !== 'superseded') return null;
|
|
@@ -932,6 +1527,67 @@ function historicalAssertOnlyCheckpoint(vault, attempt, authority, checkpoint) {
|
|
|
932
1527
|
return currentPrefix.checkpoint;
|
|
933
1528
|
}
|
|
934
1529
|
|
|
1530
|
+
function deferredAssertReplayCheckpointMigration(
|
|
1531
|
+
sessionId, entry, authority, legacyReplay, fullReplay,
|
|
1532
|
+
) {
|
|
1533
|
+
const attempt = entry?.last_memory_attempt;
|
|
1534
|
+
const checkpoint = attempt?.checkpoint;
|
|
1535
|
+
if (attempt?.memory_mode !== 'v2' || attempt?.state !== 'projected'
|
|
1536
|
+
|| attempt?.disposition !== 'applied' || !checkpointShape(checkpoint)) return null;
|
|
1537
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'memory_checkpoint')
|
|
1538
|
+
|| !sameCheckpoint(entry.memory_checkpoint, checkpoint)
|
|
1539
|
+
|| !sameCheckpoint(checkpoint, legacyReplay.checkpoint)
|
|
1540
|
+
|| sameCheckpoint(checkpoint, fullReplay.checkpoint)) return null;
|
|
1541
|
+
|
|
1542
|
+
const requiredEventIds = Array.isArray(attempt.event_ids) ? [...attempt.event_ids] : [];
|
|
1543
|
+
if (!requiredEventIds.length || new Set(requiredEventIds).size !== requiredEventIds.length) return null;
|
|
1544
|
+
const current = fullReplay.records?.['handoff.latest']?.source;
|
|
1545
|
+
if (!current?.event_id || !requiredEventIds.includes(current.event_id)) return null;
|
|
1546
|
+
const relevantLegacyCandidate = legacyReplay.candidates.some(
|
|
1547
|
+
(candidate) => candidate.memory_key === 'handoff.latest'
|
|
1548
|
+
&& candidate.event_ids.includes(current.event_id),
|
|
1549
|
+
);
|
|
1550
|
+
const relevantCandidateStillReal = fullReplay.candidates.some(
|
|
1551
|
+
(candidate) => candidate.memory_key === 'handoff.latest',
|
|
1552
|
+
);
|
|
1553
|
+
if (!relevantLegacyCandidate || relevantCandidateStillReal) return null;
|
|
1554
|
+
|
|
1555
|
+
const currentIdentity = {
|
|
1556
|
+
canonical_session_id: current.canonical_session_id,
|
|
1557
|
+
activation_id: current.activation_id,
|
|
1558
|
+
activation_epoch: current.activation_epoch,
|
|
1559
|
+
source_turn_id: current.source_turn_id,
|
|
1560
|
+
turn_sequence: current.turn_sequence,
|
|
1561
|
+
};
|
|
1562
|
+
if (currentIdentity.canonical_session_id !== sessionId
|
|
1563
|
+
|| attempt.canonical_session_id !== currentIdentity.canonical_session_id
|
|
1564
|
+
|| attempt.activation_id !== currentIdentity.activation_id
|
|
1565
|
+
|| attempt.activation_epoch !== currentIdentity.activation_epoch
|
|
1566
|
+
|| attempt.turn_id !== currentIdentity.source_turn_id
|
|
1567
|
+
|| attempt.turn_sequence !== currentIdentity.turn_sequence) return null;
|
|
1568
|
+
|
|
1569
|
+
const newerAttemptEvent = authority.ledgerEvents.some((event) => (
|
|
1570
|
+
event.canonical_session_id === currentIdentity.canonical_session_id
|
|
1571
|
+
&& event.activation_id === currentIdentity.activation_id
|
|
1572
|
+
&& event.activation_epoch === currentIdentity.activation_epoch
|
|
1573
|
+
&& Number.isInteger(event.turn_sequence)
|
|
1574
|
+
&& event.turn_sequence > currentIdentity.turn_sequence
|
|
1575
|
+
));
|
|
1576
|
+
if (newerAttemptEvent) return null;
|
|
1577
|
+
|
|
1578
|
+
const proof = validateSuccessorProof({ bySessionId: sessionId }, attempt, authority);
|
|
1579
|
+
return {
|
|
1580
|
+
sessionId,
|
|
1581
|
+
expectedFingerprint: attemptFingerprint(attempt),
|
|
1582
|
+
expectedMemoryCheckpointFingerprint: memoryCheckpointFingerprint(entry),
|
|
1583
|
+
originalCheckpoint: cloneJson(checkpoint),
|
|
1584
|
+
checkpoint: cloneJson(fullReplay.checkpoint),
|
|
1585
|
+
proof,
|
|
1586
|
+
migrationType: 'deferred_assert_replay_migrated',
|
|
1587
|
+
eventIds: requiredEventIds,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
935
1591
|
function legacyCheckpointMigration(vault, sessionId, entry, authority, fullReplay) {
|
|
936
1592
|
const attempt = entry?.last_memory_attempt;
|
|
937
1593
|
const checkpoint = attempt?.checkpoint;
|
|
@@ -961,6 +1617,8 @@ function legacyCheckpointMigration(vault, sessionId, entry, authority, fullRepla
|
|
|
961
1617
|
originalCheckpoint: cloneJson(checkpoint),
|
|
962
1618
|
checkpoint: cloneJson(nextCheckpoint),
|
|
963
1619
|
proof,
|
|
1620
|
+
migrationType: 'legacy_causal_checkpoint_migrated',
|
|
1621
|
+
eventIds: requiredEventIds,
|
|
964
1622
|
};
|
|
965
1623
|
}
|
|
966
1624
|
|
|
@@ -972,11 +1630,14 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
972
1630
|
const authority = readMemoryAuthority(vault);
|
|
973
1631
|
assertAuthorityMatches(expectedAuthority, authority);
|
|
974
1632
|
const inspected = readSessionRegistry(vault);
|
|
1633
|
+
const legacyReplay = deriveMemoryProjection(vault, authority.ledgerEvents, {
|
|
1634
|
+
resolveDeferredAsserts: false,
|
|
1635
|
+
});
|
|
975
1636
|
const fullReplay = deriveMemoryProjection(vault, authority.ledgerEvents);
|
|
976
1637
|
const plans = Object.entries(inspected.sessions || {})
|
|
977
|
-
.map(([sessionId, entry]) =>
|
|
978
|
-
|
|
979
|
-
))
|
|
1638
|
+
.map(([sessionId, entry]) => deferredAssertReplayCheckpointMigration(
|
|
1639
|
+
sessionId, entry, authority, legacyReplay, fullReplay,
|
|
1640
|
+
) || legacyCheckpointMigration(vault, sessionId, entry, authority, fullReplay))
|
|
980
1641
|
.filter(Boolean);
|
|
981
1642
|
assertAuthorityMatches(expectedAuthority, readMemoryAuthority(vault));
|
|
982
1643
|
if (!plans.length) return {
|
|
@@ -1015,14 +1676,20 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
1015
1676
|
|
|
1016
1677
|
for (const plan of plans) {
|
|
1017
1678
|
const entry = registry.sessions[plan.sessionId];
|
|
1018
|
-
const
|
|
1679
|
+
const reconciliationSeed = plan.migrationType === 'legacy_causal_checkpoint_migrated'
|
|
1680
|
+
? `${plan.sessionId}\0${plan.expectedFingerprint}\0${plan.expectedMemoryCheckpointFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`
|
|
1681
|
+
: `${plan.migrationType}\0${plan.sessionId}\0${plan.expectedFingerprint}\0${plan.expectedMemoryCheckpointFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`;
|
|
1682
|
+
const reconciliationId = `memcp-${hash(reconciliationSeed).slice(0, 20)}`;
|
|
1019
1683
|
entry.memory_reconciliations = [
|
|
1020
1684
|
...(Array.isArray(entry.memory_reconciliations) ? entry.memory_reconciliations : []),
|
|
1021
1685
|
{
|
|
1022
1686
|
v: 1,
|
|
1023
1687
|
reconciliation_id: reconciliationId,
|
|
1024
|
-
type:
|
|
1688
|
+
type: plan.migrationType,
|
|
1025
1689
|
reconciled_at: now,
|
|
1690
|
+
...(plan.migrationType === 'deferred_assert_replay_migrated'
|
|
1691
|
+
? { event_ids: [...plan.eventIds] }
|
|
1692
|
+
: {}),
|
|
1026
1693
|
causal_proof: cloneJson(plan.proof),
|
|
1027
1694
|
original_checkpoint: cloneJson(plan.originalCheckpoint),
|
|
1028
1695
|
checkpoint: cloneJson(plan.checkpoint),
|
|
@@ -1064,13 +1731,28 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
1064
1731
|
}
|
|
1065
1732
|
|
|
1066
1733
|
export function repairMemory(vault, options = {}) {
|
|
1067
|
-
|
|
1734
|
+
if (options.beforeAttemptFreeze) options.beforeAttemptFreeze();
|
|
1735
|
+
const acknowledgementPreflight = freezeRepairAttemptAcknowledgements(vault, options);
|
|
1736
|
+
if (acknowledgementPreflight === MEMORY_LOCK_BUSY) {
|
|
1737
|
+
return { status: 'busy', stage: 'attempt-freeze' };
|
|
1738
|
+
}
|
|
1739
|
+
const frozenAttempts = acknowledgementPreflight.attempts;
|
|
1740
|
+
const repaired = acknowledgementPreflight.pending
|
|
1741
|
+
? { status: 'unchanged', repairedLines: 0, backupPath: null }
|
|
1742
|
+
: repairMemoryLedger(vault);
|
|
1068
1743
|
if (repaired.status === 'busy') return repaired;
|
|
1069
1744
|
const projection = projectMemoryOutbox(vault);
|
|
1070
1745
|
if (projection.status === 'busy') return { status: 'busy', repaired, projection };
|
|
1746
|
+
const attemptAcknowledgements = acknowledgeRepairAttempts(
|
|
1747
|
+
vault, frozenAttempts, projection, options,
|
|
1748
|
+
);
|
|
1071
1749
|
const checkpointMigration = migrateLegacyMemoryCheckpoints(vault, options);
|
|
1072
1750
|
return {
|
|
1073
|
-
status: 'repaired',
|
|
1751
|
+
status: 'repaired',
|
|
1752
|
+
repaired,
|
|
1753
|
+
projection,
|
|
1754
|
+
attemptAcknowledgements,
|
|
1755
|
+
checkpointMigration,
|
|
1074
1756
|
};
|
|
1075
1757
|
}
|
|
1076
1758
|
|
|
@@ -1164,9 +1846,61 @@ function parseReconcileArgs(argv) {
|
|
|
1164
1846
|
};
|
|
1165
1847
|
}
|
|
1166
1848
|
|
|
1849
|
+
function parseRecoverAttemptArgs(argv) {
|
|
1850
|
+
const positionals = [];
|
|
1851
|
+
const seen = new Set();
|
|
1852
|
+
let apply = false;
|
|
1853
|
+
let vault = '';
|
|
1854
|
+
|
|
1855
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
1856
|
+
const token = argv[index];
|
|
1857
|
+
if (!token.startsWith('--')) {
|
|
1858
|
+
positionals.push(token);
|
|
1859
|
+
if (positionals.length > 1) {
|
|
1860
|
+
throw memoryUsageError(`memory recover-attempt recebeu argumento posicional extra: ${token}.`);
|
|
1861
|
+
}
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const equalAt = token.indexOf('=');
|
|
1866
|
+
const name = equalAt >= 0 ? token.slice(0, equalAt) : token;
|
|
1867
|
+
if (name !== '--apply' && name !== '--vault') {
|
|
1868
|
+
throw memoryUsageError(`memory recover-attempt recebeu opção desconhecida: ${name}.`);
|
|
1869
|
+
}
|
|
1870
|
+
if (seen.has(name)) {
|
|
1871
|
+
throw memoryUsageError(`memory recover-attempt recebeu opção duplicada: ${name}.`);
|
|
1872
|
+
}
|
|
1873
|
+
seen.add(name);
|
|
1874
|
+
|
|
1875
|
+
if (name === '--apply') {
|
|
1876
|
+
if (equalAt >= 0) throw memoryUsageError('--apply não aceita valor.');
|
|
1877
|
+
apply = true;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const value = equalAt >= 0 ? token.slice(equalAt + 1) : argv[index + 1];
|
|
1882
|
+
if (!value || !value.trim() || value.startsWith('--')) {
|
|
1883
|
+
throw memoryUsageError('--vault requer valor não vazio que não comece com --.');
|
|
1884
|
+
}
|
|
1885
|
+
vault = value;
|
|
1886
|
+
if (equalAt < 0) index += 1;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
if (positionals.length !== 1) {
|
|
1890
|
+
throw memoryUsageError('memory recover-attempt requer exatamente uma sessão obrigatória.');
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
return {
|
|
1894
|
+
sessionId: positionals[0],
|
|
1895
|
+
apply,
|
|
1896
|
+
vault,
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1167
1900
|
export function runMemory(argv) {
|
|
1168
1901
|
const [sub, positional] = argv;
|
|
1169
1902
|
let reconcileArgs = null;
|
|
1903
|
+
let recoverAttemptArgs = null;
|
|
1170
1904
|
if (sub === 'reconcile') {
|
|
1171
1905
|
try {
|
|
1172
1906
|
reconcileArgs = parseReconcileArgs(argv);
|
|
@@ -1176,7 +1910,18 @@ export function runMemory(argv) {
|
|
|
1176
1910
|
return;
|
|
1177
1911
|
}
|
|
1178
1912
|
}
|
|
1179
|
-
|
|
1913
|
+
if (sub === 'recover-attempt') {
|
|
1914
|
+
try {
|
|
1915
|
+
recoverAttemptArgs = parseRecoverAttemptArgs(argv);
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
process.stderr.write(`wendkeep memory: ${error.message}\n`);
|
|
1918
|
+
process.exitCode = error.code === 'WENDKEEP_MEMORY_USAGE' ? 2 : 1;
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
const vault = (
|
|
1923
|
+
recoverAttemptArgs?.vault || reconcileArgs?.vault || option(argv, '--vault')
|
|
1924
|
+
) || process.env.OBSIDIAN_VAULT_PATH;
|
|
1180
1925
|
if (!vault) { process.stderr.write('wendkeep memory: passe --vault <path>.\n'); process.exitCode = 2; return; }
|
|
1181
1926
|
if (!existsSync(vault)) { process.stderr.write(`wendkeep memory: not found: ${vault}\n`); process.exitCode = 2; return; }
|
|
1182
1927
|
try {
|
|
@@ -1192,6 +1937,12 @@ export function runMemory(argv) {
|
|
|
1192
1937
|
apply: reconcileArgs.apply,
|
|
1193
1938
|
});
|
|
1194
1939
|
}
|
|
1940
|
+
else if (sub === 'recover-attempt') {
|
|
1941
|
+
result = recoverProjectedAttempt(vault, {
|
|
1942
|
+
sessionId: recoverAttemptArgs.sessionId,
|
|
1943
|
+
apply: recoverAttemptArgs.apply,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1195
1946
|
else if (sub === 'promote' || sub === 'reject') {
|
|
1196
1947
|
const eventId = option(argv, '--event');
|
|
1197
1948
|
if (sub === 'reject' && eventId) throw memoryUsageError('--event é permitido somente em memory promote.');
|
|
@@ -1199,7 +1950,7 @@ export function runMemory(argv) {
|
|
|
1199
1950
|
action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
|
|
1200
1951
|
});
|
|
1201
1952
|
}
|
|
1202
|
-
else { process.stderr.write('wendkeep memory: use status | migrate [--apply] | repair | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
1953
|
+
else { process.stderr.write('wendkeep memory: use status | migrate [--apply] | repair | recover-attempt <session> [--apply] | reconcile <session> --by-session <session> --reason <text> [--apply] | promote <candidate> [--event <event-id>] | reject <candidate>.\n'); process.exitCode = 2; return; }
|
|
1203
1954
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1204
1955
|
if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
|
|
1205
1956
|
else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;
|