wendkeep 0.66.3 → 0.66.5

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/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';
@@ -538,6 +538,520 @@ function memoryCheckpointFingerprint(entry) {
538
538
  }));
539
539
  }
540
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
+
541
1055
  function matchingAppliedReconciliation(entry, request) {
542
1056
  const attempt = entry?.last_memory_attempt;
543
1057
  if (attempt?.memory_mode !== 'v2' || attempt?.state !== 'skipped' || attempt?.disposition !== 'superseded') return null;
@@ -1217,13 +1731,28 @@ export function migrateLegacyMemoryCheckpoints(vault, {
1217
1731
  }
1218
1732
 
1219
1733
  export function repairMemory(vault, options = {}) {
1220
- const repaired = repairMemoryLedger(vault);
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);
1221
1743
  if (repaired.status === 'busy') return repaired;
1222
1744
  const projection = projectMemoryOutbox(vault);
1223
1745
  if (projection.status === 'busy') return { status: 'busy', repaired, projection };
1746
+ const attemptAcknowledgements = acknowledgeRepairAttempts(
1747
+ vault, frozenAttempts, projection, options,
1748
+ );
1224
1749
  const checkpointMigration = migrateLegacyMemoryCheckpoints(vault, options);
1225
1750
  return {
1226
- status: 'repaired', repaired, projection, checkpointMigration,
1751
+ status: 'repaired',
1752
+ repaired,
1753
+ projection,
1754
+ attemptAcknowledgements,
1755
+ checkpointMigration,
1227
1756
  };
1228
1757
  }
1229
1758
 
@@ -1317,9 +1846,61 @@ function parseReconcileArgs(argv) {
1317
1846
  };
1318
1847
  }
1319
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
+
1320
1900
  export function runMemory(argv) {
1321
1901
  const [sub, positional] = argv;
1322
1902
  let reconcileArgs = null;
1903
+ let recoverAttemptArgs = null;
1323
1904
  if (sub === 'reconcile') {
1324
1905
  try {
1325
1906
  reconcileArgs = parseReconcileArgs(argv);
@@ -1329,7 +1910,18 @@ export function runMemory(argv) {
1329
1910
  return;
1330
1911
  }
1331
1912
  }
1332
- const vault = (reconcileArgs?.vault || option(argv, '--vault')) || process.env.OBSIDIAN_VAULT_PATH;
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;
1333
1925
  if (!vault) { process.stderr.write('wendkeep memory: passe --vault <path>.\n'); process.exitCode = 2; return; }
1334
1926
  if (!existsSync(vault)) { process.stderr.write(`wendkeep memory: not found: ${vault}\n`); process.exitCode = 2; return; }
1335
1927
  try {
@@ -1345,6 +1937,12 @@ export function runMemory(argv) {
1345
1937
  apply: reconcileArgs.apply,
1346
1938
  });
1347
1939
  }
1940
+ else if (sub === 'recover-attempt') {
1941
+ result = recoverProjectedAttempt(vault, {
1942
+ sessionId: recoverAttemptArgs.sessionId,
1943
+ apply: recoverAttemptArgs.apply,
1944
+ });
1945
+ }
1348
1946
  else if (sub === 'promote' || sub === 'reject') {
1349
1947
  const eventId = option(argv, '--event');
1350
1948
  if (sub === 'reject' && eventId) throw memoryUsageError('--event é permitido somente em memory promote.');
@@ -1352,7 +1950,7 @@ export function runMemory(argv) {
1352
1950
  action: sub, candidateId: positional, ...(eventId ? { eventId } : {}),
1353
1951
  });
1354
1952
  }
1355
- 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; }
1356
1954
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1357
1955
  if (sub === 'status' && argv.includes('--gate')) process.exitCode = result.status === 'blocked' ? 1 : 0;
1358
1956
  else if (sub === 'reconcile' && reconcileArgs.apply) process.exitCode = result.health?.status === 'blocked' ? 1 : 0;