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/CHANGELOG.md +34 -0
- package/README.en.md +13 -8
- package/README.md +13 -8
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/memory.md +31 -2
- package/docs/en/commands/sessions-and-import.md +15 -0
- package/docs/pt-BR/commands/costs-and-observability.md +21 -7
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
- package/docs/pt-BR/commands/memory.md +32 -2
- package/docs/pt-BR/commands/sessions-and-import.md +15 -1
- package/hooks/codex-rollout-meta.mjs +112 -0
- package/hooks/codex-subagent-graph.mjs +903 -0
- package/hooks/harness-doctor.mjs +82 -1
- package/hooks/import-sessions.mjs +185 -50
- package/hooks/session-identity.mjs +40 -5
- package/hooks/session-observability-lifecycle.mjs +129 -0
- package/hooks/session-observability-state.mjs +241 -0
- package/hooks/session-observability-store.mjs +436 -0
- package/hooks/session-observability.mjs +647 -21
- package/hooks/session-stop.mjs +218 -5
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- 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 +5 -0
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/memory.mjs +603 -5
- package/src/rebuild-costs.mjs +220 -34
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
|
-
|
|
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',
|
|
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
|
-
|
|
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;
|