wendkeep 0.66.1 → 0.66.3
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 +24 -0
- package/README.en.md +12 -4
- package/README.md +12 -4
- package/docs/en/commands/memory.md +15 -1
- package/docs/pt-BR/commands/memory.md +14 -0
- package/package.json +1 -1
- package/packages/vault/src/memory-store.mjs +72 -5
- package/src/memory.mjs +160 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.66.3] — 2026-07-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **O replay reavalia candidates transitórios contra a fonte moderna final.** Um Stop da mesma
|
|
12
|
+
sessão/activation/epoch e turno maior agora avança depois que a correção causal já presente se
|
|
13
|
+
torna ativa; turno menor fica superseded e divergências reais continuam para curadoria.
|
|
14
|
+
- **`memory repair` migra o checkpoint antigo somente com prova e CAS.** O repair compara a
|
|
15
|
+
semântica anterior e a atual, faz backup, atualiza attempt e espelho e registra auditoria sem
|
|
16
|
+
reordenar, reescrever ou acrescentar evento ao ledger. Prova incompleta continua bloqueada. A
|
|
17
|
+
0.66.2 não deve ser publicada no npm; publique e instale a 0.66.3.
|
|
18
|
+
|
|
19
|
+
## [0.66.2] — 2026-07-29
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Promoção preserva valor e identidade causal do evento escolhido.** `memory promote` não
|
|
24
|
+
converte mais objetos/arrays JSON em texto e mantém sessão, activation/epoch, turno de origem e
|
|
25
|
+
sequência; o próximo `SessionStop` da mesma cadeia avança o handoff sem recriar candidate.
|
|
26
|
+
- **A recuperação 0.66.1 cobre inversão física/temporal sem ampliar autoridade.** Quando uma
|
|
27
|
+
promoção legada projetada fica fora do candidate por `observed_at`, `memory promote` só a inclui
|
|
28
|
+
em `supersedes` sob prova de ancestralidade e mesma linhagem; fonte moderna ou alheia falha antes
|
|
29
|
+
de anexar. O E2E percorre o dispatcher público e prova Stop posterior e duplicado idempotentes.
|
|
30
|
+
|
|
7
31
|
## [0.66.1] — 2026-07-29
|
|
8
32
|
|
|
9
33
|
### Fixed
|
package/README.en.md
CHANGED
|
@@ -256,7 +256,7 @@ Hot memory now separates human authorship, operational state, and evidence:
|
|
|
256
256
|
- **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
|
|
257
257
|
- **`SHARED_MEMORY.md` is generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
258
258
|
- **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
|
|
259
|
-
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event.
|
|
259
|
+
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
|
|
260
260
|
|
|
261
261
|
Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
|
|
262
262
|
|
|
@@ -295,9 +295,17 @@ ambiguity uses `memory reconcile <session> --by-session <successor>
|
|
|
295
295
|
--reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
|
|
296
296
|
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
|
|
297
297
|
explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
|
|
298
|
-
candidate, while `memory reject <id>` keeps the current value.
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
candidate, while `memory reject <id>` keeps the current value. The decision is idempotent, and a
|
|
299
|
+
new promotion accepts a later Stop from the same session/activation without recreating a conflict.
|
|
300
|
+
A promotion written by 0.66.1 remains historical: if the next Stop forms a new candidate, update
|
|
301
|
+
to 0.66.3 and run `memory repair`. During replay, a transient candidate is re-evaluated against the
|
|
302
|
+
final modern source: the same session/activation/epoch and a higher turn advances; a lower turn is
|
|
303
|
+
superseded. Repair migrates the checkpoint and mirror only when it proves the exact previous
|
|
304
|
+
replay, attempt identity, and absence of a real conflict; it creates a backup and audit without
|
|
305
|
+
appending or rewriting events. Ambiguity stays queued for explicit curation. Do not publish or
|
|
306
|
+
install 0.66.2; use 0.66.3. Decisions survive repair/replay;
|
|
307
|
+
`blocked_by_core` cannot override CORE. Doctor only
|
|
308
|
+
diagnoses. See [memory and curation](docs/en/commands/memory.md).
|
|
301
309
|
|
|
302
310
|
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
303
311
|
|
package/README.md
CHANGED
|
@@ -256,7 +256,7 @@ Hot memory now separates human authorship, operational state, and evidence:
|
|
|
256
256
|
- **`CORE.md` is canonical.** It is the short, hand-curated nucleus for durable preferences, active patterns, and open issues; no projector may infer or overwrite it.
|
|
257
257
|
- **`SHARED_MEMORY.md` is generated operational state.** The `Stop` hook turns the session handoff into sanitized events; the projector deterministically reduces the ledger and publishes a verifiable revision, cursor, and hash. Facts are `verified` only with local evidence; unsupported reports remain `reported`, and disagreements become candidates for human judgment.
|
|
258
258
|
- **`MEMORY_EVENTS.jsonl` is the append-only authority.** `Stop` makes events durable in the outbox before acknowledging the attempt; the projector runs outside the registry lock and retries reuse the same IDs. Repeating an identical `event_id`/payload is a no-op; reusing the ID with different bytes is observable corruption.
|
|
259
|
-
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event.
|
|
259
|
+
- **`MEMORY_CANDIDATES.jsonl` is the curation queue.** Conflicts and legacy content are never silently promoted. `promote` and `reject` record the decision as a new event; promotion preserves the selected event's JSON type, session, activation/epoch, and source turn.
|
|
260
260
|
|
|
261
261
|
Artifacts stay under `.brain/` only. Sanitization strips secrets, tokens, local paths, transcripts, and harness payloads both before persistence and before injection. Events carry a `project_id`, and one vault never accepts another project's events.
|
|
262
262
|
|
|
@@ -295,9 +295,17 @@ ambiguity uses `memory reconcile <session> --by-session <successor>
|
|
|
295
295
|
--reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
|
|
296
296
|
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. Conflicts require
|
|
297
297
|
explicit, durable curation: `memory promote <id> --event <event-id>` selects one event from the
|
|
298
|
-
candidate, while `memory reject <id>` keeps the current value.
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
candidate, while `memory reject <id>` keeps the current value. The decision is idempotent, and a
|
|
299
|
+
new promotion accepts a later Stop from the same session/activation without recreating a conflict.
|
|
300
|
+
A promotion written by 0.66.1 remains historical: if the next Stop forms a new candidate, update
|
|
301
|
+
to 0.66.3 and run `memory repair`. During replay, a transient candidate is re-evaluated against the
|
|
302
|
+
final modern source: the same session/activation/epoch and a higher turn advances; a lower turn is
|
|
303
|
+
superseded. Repair migrates the checkpoint and mirror only when it proves the exact previous
|
|
304
|
+
replay, attempt identity, and absence of a real conflict; it creates a backup and audit without
|
|
305
|
+
appending or rewriting events. Ambiguity stays queued for explicit curation. Do not publish or
|
|
306
|
+
install 0.66.2; use 0.66.3. Decisions survive repair/replay;
|
|
307
|
+
`blocked_by_core` cannot override CORE. Doctor only
|
|
308
|
+
diagnoses. See [memory and curation](docs/en/commands/memory.md).
|
|
301
309
|
|
|
302
310
|
Session notes use one live `## Agentes, tokens e custos` snapshot. Main-agent and subagent hooks recompose it atomically, with costs, token dimensions, reasoning tokens and effort per model/source. Every hook that rewrites a session note takes a per-file lock and writes through a temp file + rename, so the `SubagentStop` fan-out (one hook run per subagent) can never leave a note half-written; a note whose frontmatter reads back damaged is left untouched rather than patched.
|
|
303
311
|
|
|
@@ -70,7 +70,15 @@ npx wendkeep validate-memory --vault <v2-vault>
|
|
|
70
70
|
`blocked_by_core` candidate can only be rejected: promotion first requires canonical CORE
|
|
71
71
|
curation. If the selected event still belongs to the matching latest `projected` attempt,
|
|
72
72
|
promotion also refreshes its checkpoint and mirror causally; JSON reports
|
|
73
|
-
`checkpointRefreshed`, and a newer concurrent attempt remains untouched.
|
|
73
|
+
`checkpointRefreshed`, and a newer concurrent attempt remains untouched. The decision keeps
|
|
74
|
+
the already validated JSON value without string coercion and copies the selected event's
|
|
75
|
+
`canonical_session_id`, activation/epoch, `source_turn_id`, and `turn_sequence`. A later Stop
|
|
76
|
+
from the same session/activation therefore advances the value instead of opening another candidate.
|
|
77
|
+
During replay, a transient candidate is re-evaluated against the final modern source. The same
|
|
78
|
+
session/activation/epoch and a higher turn applies the Stop; a lower turn is superseded. A
|
|
79
|
+
different, incomplete, or ambiguous identity keeps the candidate queued for curation. `memory
|
|
80
|
+
repair` compares the old and current replay and migrates checkpoint+mirror only with exact
|
|
81
|
+
identity, backup, audit, and CAS; it does not reorder, rewrite, or append a ledger event.
|
|
74
82
|
- `validate-memory <CORE.md>` checks the 25-line cap, required sections, and secrets.
|
|
75
83
|
- `validate-memory --vault` requires a complete v2 bundle and is not the legacy-vault gate.
|
|
76
84
|
|
|
@@ -108,6 +116,12 @@ of a global projection that has already advanced with concurrent events.
|
|
|
108
116
|
- `promote` reports that `--event` is required: inspect the candidate `event_ids`, compare their
|
|
109
117
|
provenance/value, and name the winner explicitly. An ID outside the candidate fails without
|
|
110
118
|
mutating the ledger or projections.
|
|
119
|
+
- A promotion made by 0.66.1 followed by a transient candidate: update to 0.66.3, preserve a backup,
|
|
120
|
+
and run `memory repair`. Do not publish or install 0.66.2. Repair migrates the checkpoint only
|
|
121
|
+
when the old replay, attempt identity, and new event match exactly; a real conflict stays blocked
|
|
122
|
+
for a human `promote`/`reject` choice.
|
|
123
|
+
- `promote` says that the candidate no longer matches the causal projection: no event was appended.
|
|
124
|
+
Run `memory status`, inspect the current candidate again, and do not force a different lineage.
|
|
111
125
|
- Missing `event_cursor` or mismatched v2 hash: preserve the bundle and assess `memory repair`.
|
|
112
126
|
- `validate-memory --vault` fails on legacy: validate CORE only or migrate first.
|
|
113
127
|
|
|
@@ -68,6 +68,14 @@ npx wendkeep validate-memory --vault <cofre-v2>
|
|
|
68
68
|
promover exige antes alterar CORE pela curadoria canônica. Se o evento escolhido ainda pertence
|
|
69
69
|
ao último attempt `projected` correspondente, a promoção também atualiza causalmente checkpoint
|
|
70
70
|
e espelho; o JSON retorna `checkpointRefreshed`, e um attempt concorrente mais novo não é tocado.
|
|
71
|
+
A decisão conserva, sem coerção para string, o valor JSON já validado e copia do evento escolhido
|
|
72
|
+
`canonical_session_id`, activation/epoch, `source_turn_id` e `turn_sequence`. Por isso, um Stop
|
|
73
|
+
posterior da mesma sessão/activation avança o valor em vez de abrir outro candidate. Durante o
|
|
74
|
+
replay, um candidate transitório é reavaliado contra a fonte moderna final. Mesma
|
|
75
|
+
sessão/activation/epoch e turno maior aplica o Stop; turno menor fica superseded. Identidade
|
|
76
|
+
divergente, incompleta ou ambígua mantém o candidate para curadoria. `memory repair` compara o
|
|
77
|
+
replay anterior e o atual e só migra checkpoint+espelho com identidade exata, backup, audit e
|
|
78
|
+
CAS; ele não reordena, reescreve nem acrescenta evento ao ledger.
|
|
71
79
|
- `validate-memory <CORE.md>` valida cap de 25 linhas, seções e segredos.
|
|
72
80
|
- `validate-memory --vault` exige bundle v2 completo; não é o gate correto para vault legado.
|
|
73
81
|
|
|
@@ -104,6 +112,12 @@ prefixo válido de uma projeção global que já avançou com eventos concorrent
|
|
|
104
112
|
- `promote` informa que `--event` é obrigatório: leia os `event_ids` do candidate, compare a
|
|
105
113
|
proveniência/valor e indique explicitamente o vencedor. ID que não pertence ao candidate falha
|
|
106
114
|
sem mutar ledger ou projeções.
|
|
115
|
+
- Promoção feita pela 0.66.1 seguida de candidate transitório: atualize para a 0.66.3, preserve um
|
|
116
|
+
backup e rode `memory repair`. Não publique nem instale a 0.66.2. O repair só migra o checkpoint
|
|
117
|
+
quando o replay anterior, a identidade do attempt e o evento novo coincidem exatamente; conflito
|
|
118
|
+
real continua bloqueado para escolha humana com `promote`/`reject`.
|
|
119
|
+
- `promote` informa que o candidate não corresponde mais à projeção causal: nenhum evento foi
|
|
120
|
+
anexado. Rode `memory status`, releia o candidate atual e não force uma linhagem diferente.
|
|
107
121
|
- `event_cursor` ausente ou hash divergente em v2: preserve o bundle e avalie `memory repair`.
|
|
108
122
|
- `validate-memory --vault` falha no legado: valide apenas CORE ou migre primeiro.
|
|
109
123
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.3",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -344,6 +344,22 @@ function sameCausalActivation(left, right) {
|
|
|
344
344
|
&& left.activation_id === right?.activation_id;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
function hasCompleteCausalIdentity(event) {
|
|
348
|
+
return Boolean(event?.canonical_session_id)
|
|
349
|
+
&& Boolean(event?.activation_id)
|
|
350
|
+
&& Boolean(event?.source_turn_id)
|
|
351
|
+
&& Number.isInteger(event?.activation_epoch)
|
|
352
|
+
&& Number.isInteger(event?.turn_sequence);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function sameCompleteCausalLineage(left, right) {
|
|
356
|
+
return hasCompleteCausalIdentity(left)
|
|
357
|
+
&& hasCompleteCausalIdentity(right)
|
|
358
|
+
&& left.canonical_session_id === right.canonical_session_id
|
|
359
|
+
&& left.activation_id === right.activation_id
|
|
360
|
+
&& left.activation_epoch === right.activation_epoch;
|
|
361
|
+
}
|
|
362
|
+
|
|
347
363
|
function comparable(left, right) {
|
|
348
364
|
if (sameCausalActivation(left, right)) return true;
|
|
349
365
|
const leftSupersedes = left.supersedes_event_id || left.supersedes;
|
|
@@ -447,7 +463,9 @@ function isCausallyOlder(event, current) {
|
|
|
447
463
|
* Pure deterministic reducer. It pre-detects incomparable scalar siblings so replay order
|
|
448
464
|
* never turns one concurrent writer into an accidental winner.
|
|
449
465
|
*/
|
|
450
|
-
export function reduceMemoryEvents(inputEvents = [], {
|
|
466
|
+
export function reduceMemoryEvents(inputEvents = [], {
|
|
467
|
+
coreInvariants = new Map(), resolveDeferredAsserts = true,
|
|
468
|
+
} = {}) {
|
|
451
469
|
const protectedValues = coreInvariants instanceof Map
|
|
452
470
|
? coreInvariants
|
|
453
471
|
: new Map(Object.entries(coreInvariants || {}));
|
|
@@ -495,6 +513,8 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
495
513
|
const records = new Map();
|
|
496
514
|
const tombstones = new Map();
|
|
497
515
|
const candidates = [];
|
|
516
|
+
const pendingAssertConflicts = [];
|
|
517
|
+
const resolvedCandidateIds = new Set();
|
|
498
518
|
const emittedGroups = new Set();
|
|
499
519
|
const appliedEventIds = [];
|
|
500
520
|
const superseded = [];
|
|
@@ -543,7 +563,9 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
543
563
|
appliedEventIds.push(item.event_id);
|
|
544
564
|
continue;
|
|
545
565
|
}
|
|
546
|
-
|
|
566
|
+
const candidate = conflictCandidate(item.memory_key, [currentEventFromRecord(current), item]);
|
|
567
|
+
candidates.push(candidate);
|
|
568
|
+
pendingAssertConflicts.push({ candidate, event: item });
|
|
547
569
|
continue;
|
|
548
570
|
}
|
|
549
571
|
if (!current) {
|
|
@@ -616,6 +638,46 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
616
638
|
appliedEventIds.push(item.event_id);
|
|
617
639
|
}
|
|
618
640
|
|
|
641
|
+
if (resolveDeferredAsserts) {
|
|
642
|
+
// A physically late assert can sort before a corrective promotion because effective time
|
|
643
|
+
// precedes CLI decision time. Revisit only scalar assert conflicts left without an explicit
|
|
644
|
+
// decision, against the final complete causal source; the ledger and global ordering stay put.
|
|
645
|
+
const deferredAsserts = pendingAssertConflicts
|
|
646
|
+
.filter((pending) => !candidateDecisions.has(pending.candidate.candidate_id))
|
|
647
|
+
.sort((left, right) => String(left.event.memory_key).localeCompare(String(right.event.memory_key))
|
|
648
|
+
|| String(left.event.canonical_session_id || '').localeCompare(String(right.event.canonical_session_id || ''))
|
|
649
|
+
|| String(left.event.activation_id || '').localeCompare(String(right.event.activation_id || ''))
|
|
650
|
+
|| Number(left.event.activation_epoch ?? -1) - Number(right.event.activation_epoch ?? -1)
|
|
651
|
+
|| Number(left.event.turn_sequence ?? -1) - Number(right.event.turn_sequence ?? -1)
|
|
652
|
+
|| eventOrder(left.event, right.event));
|
|
653
|
+
let advanced = true;
|
|
654
|
+
while (advanced) {
|
|
655
|
+
advanced = false;
|
|
656
|
+
for (const pending of deferredAsserts) {
|
|
657
|
+
if (resolvedCandidateIds.has(pending.candidate.candidate_id)) continue;
|
|
658
|
+
const current = records.get(pending.event.memory_key);
|
|
659
|
+
const currentSource = current?.source;
|
|
660
|
+
if (!sameCompleteCausalLineage(pending.event, currentSource)) continue;
|
|
661
|
+
if (pending.event.turn_sequence > currentSource.turn_sequence) {
|
|
662
|
+
records.set(pending.event.memory_key, {
|
|
663
|
+
value: pending.event.value,
|
|
664
|
+
revision: current.revision + 1,
|
|
665
|
+
source: pending.event,
|
|
666
|
+
});
|
|
667
|
+
tombstones.delete(pending.event.memory_key);
|
|
668
|
+
superseded.push({ event_id: currentSource.event_id, by_event_id: pending.event.event_id });
|
|
669
|
+
revision += 1;
|
|
670
|
+
appliedEventIds.push(pending.event.event_id);
|
|
671
|
+
resolvedCandidateIds.add(pending.candidate.candidate_id);
|
|
672
|
+
advanced = true;
|
|
673
|
+
} else if (pending.event.turn_sequence < currentSource.turn_sequence) {
|
|
674
|
+
superseded.push({ event_id: pending.event.event_id, by_event_id: currentSource.event_id });
|
|
675
|
+
resolvedCandidateIds.add(pending.candidate.candidate_id);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
619
681
|
const stateEntries = [...records].map(([key, record]) => [key, record.value]);
|
|
620
682
|
const recordEntries = [...records].map(([key, record]) => [key, record]);
|
|
621
683
|
const tombstoneEntries = [...tombstones];
|
|
@@ -623,7 +685,8 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
623
685
|
const recordObject = sortedObject(recordEntries);
|
|
624
686
|
const tombstoneObject = sortedObject(tombstoneEntries);
|
|
625
687
|
const unresolvedCandidates = candidates
|
|
626
|
-
.filter((item) => !candidateDecisions.has(item.candidate_id)
|
|
688
|
+
.filter((item) => !candidateDecisions.has(item.candidate_id)
|
|
689
|
+
&& !resolvedCandidateIds.has(item.candidate_id));
|
|
627
690
|
unresolvedCandidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
|
|
628
691
|
superseded.sort((left, right) => left.event_id.localeCompare(right.event_id));
|
|
629
692
|
const activeEvents = Object.entries(recordObject).map(([memoryKey, record]) => ({
|
|
@@ -655,8 +718,12 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
655
718
|
* `eventCursor` is the reducer's deterministic causal cursor; `ledgerCursor` is the
|
|
656
719
|
* physical prefix boundary used by durable checkpoints.
|
|
657
720
|
*/
|
|
658
|
-
export function deriveMemoryProjection(vaultBase, inputEvents = []
|
|
659
|
-
|
|
721
|
+
export function deriveMemoryProjection(vaultBase, inputEvents = [], {
|
|
722
|
+
resolveDeferredAsserts = true,
|
|
723
|
+
} = {}) {
|
|
724
|
+
const reduced = reduceMemoryEvents(inputEvents, {
|
|
725
|
+
coreInvariants: readCoreInvariants(vaultBase), resolveDeferredAsserts,
|
|
726
|
+
});
|
|
660
727
|
const ledgerCursor = inputEvents.at(-1)?.event_id || 'none';
|
|
661
728
|
const checkpoint = {
|
|
662
729
|
revision: reduced.revision,
|
package/src/memory.mjs
CHANGED
|
@@ -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);
|
|
@@ -932,6 +1013,67 @@ function historicalAssertOnlyCheckpoint(vault, attempt, authority, checkpoint) {
|
|
|
932
1013
|
return currentPrefix.checkpoint;
|
|
933
1014
|
}
|
|
934
1015
|
|
|
1016
|
+
function deferredAssertReplayCheckpointMigration(
|
|
1017
|
+
sessionId, entry, authority, legacyReplay, fullReplay,
|
|
1018
|
+
) {
|
|
1019
|
+
const attempt = entry?.last_memory_attempt;
|
|
1020
|
+
const checkpoint = attempt?.checkpoint;
|
|
1021
|
+
if (attempt?.memory_mode !== 'v2' || attempt?.state !== 'projected'
|
|
1022
|
+
|| attempt?.disposition !== 'applied' || !checkpointShape(checkpoint)) return null;
|
|
1023
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'memory_checkpoint')
|
|
1024
|
+
|| !sameCheckpoint(entry.memory_checkpoint, checkpoint)
|
|
1025
|
+
|| !sameCheckpoint(checkpoint, legacyReplay.checkpoint)
|
|
1026
|
+
|| sameCheckpoint(checkpoint, fullReplay.checkpoint)) return null;
|
|
1027
|
+
|
|
1028
|
+
const requiredEventIds = Array.isArray(attempt.event_ids) ? [...attempt.event_ids] : [];
|
|
1029
|
+
if (!requiredEventIds.length || new Set(requiredEventIds).size !== requiredEventIds.length) return null;
|
|
1030
|
+
const current = fullReplay.records?.['handoff.latest']?.source;
|
|
1031
|
+
if (!current?.event_id || !requiredEventIds.includes(current.event_id)) return null;
|
|
1032
|
+
const relevantLegacyCandidate = legacyReplay.candidates.some(
|
|
1033
|
+
(candidate) => candidate.memory_key === 'handoff.latest'
|
|
1034
|
+
&& candidate.event_ids.includes(current.event_id),
|
|
1035
|
+
);
|
|
1036
|
+
const relevantCandidateStillReal = fullReplay.candidates.some(
|
|
1037
|
+
(candidate) => candidate.memory_key === 'handoff.latest',
|
|
1038
|
+
);
|
|
1039
|
+
if (!relevantLegacyCandidate || relevantCandidateStillReal) return null;
|
|
1040
|
+
|
|
1041
|
+
const currentIdentity = {
|
|
1042
|
+
canonical_session_id: current.canonical_session_id,
|
|
1043
|
+
activation_id: current.activation_id,
|
|
1044
|
+
activation_epoch: current.activation_epoch,
|
|
1045
|
+
source_turn_id: current.source_turn_id,
|
|
1046
|
+
turn_sequence: current.turn_sequence,
|
|
1047
|
+
};
|
|
1048
|
+
if (currentIdentity.canonical_session_id !== sessionId
|
|
1049
|
+
|| attempt.canonical_session_id !== currentIdentity.canonical_session_id
|
|
1050
|
+
|| attempt.activation_id !== currentIdentity.activation_id
|
|
1051
|
+
|| attempt.activation_epoch !== currentIdentity.activation_epoch
|
|
1052
|
+
|| attempt.turn_id !== currentIdentity.source_turn_id
|
|
1053
|
+
|| attempt.turn_sequence !== currentIdentity.turn_sequence) return null;
|
|
1054
|
+
|
|
1055
|
+
const newerAttemptEvent = authority.ledgerEvents.some((event) => (
|
|
1056
|
+
event.canonical_session_id === currentIdentity.canonical_session_id
|
|
1057
|
+
&& event.activation_id === currentIdentity.activation_id
|
|
1058
|
+
&& event.activation_epoch === currentIdentity.activation_epoch
|
|
1059
|
+
&& Number.isInteger(event.turn_sequence)
|
|
1060
|
+
&& event.turn_sequence > currentIdentity.turn_sequence
|
|
1061
|
+
));
|
|
1062
|
+
if (newerAttemptEvent) return null;
|
|
1063
|
+
|
|
1064
|
+
const proof = validateSuccessorProof({ bySessionId: sessionId }, attempt, authority);
|
|
1065
|
+
return {
|
|
1066
|
+
sessionId,
|
|
1067
|
+
expectedFingerprint: attemptFingerprint(attempt),
|
|
1068
|
+
expectedMemoryCheckpointFingerprint: memoryCheckpointFingerprint(entry),
|
|
1069
|
+
originalCheckpoint: cloneJson(checkpoint),
|
|
1070
|
+
checkpoint: cloneJson(fullReplay.checkpoint),
|
|
1071
|
+
proof,
|
|
1072
|
+
migrationType: 'deferred_assert_replay_migrated',
|
|
1073
|
+
eventIds: requiredEventIds,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
935
1077
|
function legacyCheckpointMigration(vault, sessionId, entry, authority, fullReplay) {
|
|
936
1078
|
const attempt = entry?.last_memory_attempt;
|
|
937
1079
|
const checkpoint = attempt?.checkpoint;
|
|
@@ -961,6 +1103,8 @@ function legacyCheckpointMigration(vault, sessionId, entry, authority, fullRepla
|
|
|
961
1103
|
originalCheckpoint: cloneJson(checkpoint),
|
|
962
1104
|
checkpoint: cloneJson(nextCheckpoint),
|
|
963
1105
|
proof,
|
|
1106
|
+
migrationType: 'legacy_causal_checkpoint_migrated',
|
|
1107
|
+
eventIds: requiredEventIds,
|
|
964
1108
|
};
|
|
965
1109
|
}
|
|
966
1110
|
|
|
@@ -972,11 +1116,14 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
972
1116
|
const authority = readMemoryAuthority(vault);
|
|
973
1117
|
assertAuthorityMatches(expectedAuthority, authority);
|
|
974
1118
|
const inspected = readSessionRegistry(vault);
|
|
1119
|
+
const legacyReplay = deriveMemoryProjection(vault, authority.ledgerEvents, {
|
|
1120
|
+
resolveDeferredAsserts: false,
|
|
1121
|
+
});
|
|
975
1122
|
const fullReplay = deriveMemoryProjection(vault, authority.ledgerEvents);
|
|
976
1123
|
const plans = Object.entries(inspected.sessions || {})
|
|
977
|
-
.map(([sessionId, entry]) =>
|
|
978
|
-
|
|
979
|
-
))
|
|
1124
|
+
.map(([sessionId, entry]) => deferredAssertReplayCheckpointMigration(
|
|
1125
|
+
sessionId, entry, authority, legacyReplay, fullReplay,
|
|
1126
|
+
) || legacyCheckpointMigration(vault, sessionId, entry, authority, fullReplay))
|
|
980
1127
|
.filter(Boolean);
|
|
981
1128
|
assertAuthorityMatches(expectedAuthority, readMemoryAuthority(vault));
|
|
982
1129
|
if (!plans.length) return {
|
|
@@ -1015,14 +1162,20 @@ export function migrateLegacyMemoryCheckpoints(vault, {
|
|
|
1015
1162
|
|
|
1016
1163
|
for (const plan of plans) {
|
|
1017
1164
|
const entry = registry.sessions[plan.sessionId];
|
|
1018
|
-
const
|
|
1165
|
+
const reconciliationSeed = plan.migrationType === 'legacy_causal_checkpoint_migrated'
|
|
1166
|
+
? `${plan.sessionId}\0${plan.expectedFingerprint}\0${plan.expectedMemoryCheckpointFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`
|
|
1167
|
+
: `${plan.migrationType}\0${plan.sessionId}\0${plan.expectedFingerprint}\0${plan.expectedMemoryCheckpointFingerprint}\0${canonicalMemoryJson(plan.checkpoint)}`;
|
|
1168
|
+
const reconciliationId = `memcp-${hash(reconciliationSeed).slice(0, 20)}`;
|
|
1019
1169
|
entry.memory_reconciliations = [
|
|
1020
1170
|
...(Array.isArray(entry.memory_reconciliations) ? entry.memory_reconciliations : []),
|
|
1021
1171
|
{
|
|
1022
1172
|
v: 1,
|
|
1023
1173
|
reconciliation_id: reconciliationId,
|
|
1024
|
-
type:
|
|
1174
|
+
type: plan.migrationType,
|
|
1025
1175
|
reconciled_at: now,
|
|
1176
|
+
...(plan.migrationType === 'deferred_assert_replay_migrated'
|
|
1177
|
+
? { event_ids: [...plan.eventIds] }
|
|
1178
|
+
: {}),
|
|
1026
1179
|
causal_proof: cloneJson(plan.proof),
|
|
1027
1180
|
original_checkpoint: cloneJson(plan.originalCheckpoint),
|
|
1028
1181
|
checkpoint: cloneJson(plan.checkpoint),
|