wendkeep 0.66.3 → 0.66.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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.4] — 2026-07-30
8
+
9
+ ### Fixed
10
+
11
+ - **`memory repair` fecha o acknowledgement apenas para a outbox que a própria execução
12
+ consumiu integralmente.** O receipt do projetor preserva idempotência e impede que attempts
13
+ históricos, parciais, causalmente divergentes ou concorrentes sejam reclassificados.
14
+ - **`memory recover-attempt <session>` recupera com segurança o estado já projetado pela
15
+ 0.66.3.** O dry-run é padrão e `--apply` altera somente o registry/checkpoint sob prova física,
16
+ lock e CAS; A→B→A, ledger/sidecars divergentes, outbox pendente e links inseguros falham sem
17
+ escrita. O diagnóstico agora orienta esse comando quando o acknowledgement ficou pendente.
18
+
7
19
  ## [0.66.3] — 2026-07-30
8
20
 
9
21
  ### Fixed
package/README.en.md CHANGED
@@ -287,8 +287,10 @@ lost publication, or mismatched checkpoint blocks. See [migration](docs/en/comma
287
287
  and [diagnostics](docs/en/commands/maintenance-and-diagnostics.md).
288
288
 
289
289
  If status blocks, preserve the evidence and run `wendkeep memory repair --vault <vault>` to back up
290
- the corrupt ledger, retain valid lines, and re-project. Repair never reclassifies attempts. Valid
291
- pre-0.59 causal checkpoints and exactly re-derived assert-only historical prefixes are
290
+ the corrupt ledger, retain valid lines, and re-project. Repair remains structural: its only narrow
291
+ acknowledgement exception covers attempts entirely represented by the outbox consumed by that same
292
+ run; it does not scan or reclassify historical attempts. Valid pre-0.59 causal checkpoints and
293
+ exactly re-derived assert-only historical prefixes are
292
294
  CAS-migrated on both the attempt and `memory_checkpoint` to the correct physical boundary with
293
295
  backup/audit; divergent mirrors fail closed. A demonstrably superseded
294
296
  ambiguity uses `memory reconcile <session> --by-session <successor>
@@ -303,9 +305,12 @@ final modern source: the same session/activation/epoch and a higher turn advance
303
305
  superseded. Repair migrates the checkpoint and mirror only when it proves the exact previous
304
306
  replay, attempt identity, and absence of a real conflict; it creates a backup and audit without
305
307
  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).
308
+ install 0.66.2; use 0.66.3 or later. Decisions survive repair/replay;
309
+ `blocked_by_core` cannot override CORE. Doctor only diagnoses. When status/doctor reports projected
310
+ acknowledgement pending on 0.66.4 or later, first run the targeted dry run
311
+ `memory recover-attempt <session> --vault <vault>`, then authorize `--apply`; it changes only
312
+ registry/checkpoint. See syntax, preconditions, and fail-closed behavior in
313
+ [memory and curation](docs/en/commands/memory.md).
309
314
 
310
315
  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.
311
316
 
package/README.md CHANGED
@@ -287,8 +287,10 @@ lost publication, or mismatched checkpoint blocks. See [migration](docs/en/comma
287
287
  and [diagnostics](docs/en/commands/maintenance-and-diagnostics.md).
288
288
 
289
289
  If status blocks, preserve the evidence and run `wendkeep memory repair --vault <vault>` to back up
290
- the corrupt ledger, retain valid lines, and re-project. Repair never reclassifies attempts. Valid
291
- pre-0.59 causal checkpoints and exactly re-derived assert-only historical prefixes are
290
+ the corrupt ledger, retain valid lines, and re-project. Repair remains structural: its only narrow
291
+ acknowledgement exception covers attempts entirely represented by the outbox consumed by that same
292
+ run; it does not scan or reclassify historical attempts. Valid pre-0.59 causal checkpoints and
293
+ exactly re-derived assert-only historical prefixes are
292
294
  CAS-migrated on both the attempt and `memory_checkpoint` to the correct physical boundary with
293
295
  backup/audit; divergent mirrors fail closed. A demonstrably superseded
294
296
  ambiguity uses `memory reconcile <session> --by-session <successor>
@@ -303,9 +305,12 @@ final modern source: the same session/activation/epoch and a higher turn advance
303
305
  superseded. Repair migrates the checkpoint and mirror only when it proves the exact previous
304
306
  replay, attempt identity, and absence of a real conflict; it creates a backup and audit without
305
307
  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).
308
+ install 0.66.2; use 0.66.3 or later. Decisions survive repair/replay;
309
+ `blocked_by_core` cannot override CORE. Doctor only diagnoses. When status/doctor reports projected
310
+ acknowledgement pending on 0.66.4 or later, first run the targeted dry run
311
+ `memory recover-attempt <session> --vault <vault>`, then authorize `--apply`; it changes only
312
+ registry/checkpoint. See syntax, preconditions, and fail-closed behavior in
313
+ [memory and curation](docs/en/commands/memory.md).
309
314
 
310
315
  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.
311
316
 
@@ -25,6 +25,7 @@ Pass the vault explicitly in automation. Preserve backups and evidence before re
25
25
  ```bash
26
26
  npx wendkeep memory status [--gate] --vault <vault>
27
27
  npx wendkeep memory repair --vault <vault>
28
+ npx wendkeep memory recover-attempt <session> [--apply] --vault <vault>
28
29
  npx wendkeep memory reconcile <ambiguous-session> --by-session <successor-session> --reason <reason> [--apply] --vault <vault>
29
30
  npx wendkeep memory promote <candidate> [--event <event-id>] --vault <vault>
30
31
  npx wendkeep memory reject <candidate> --vault <vault>
@@ -51,8 +52,23 @@ npx wendkeep validate-memory --vault <v2-vault>
51
52
  historical prefix only when revision, cursor, hash, identity, turns, and the
52
53
  `memory_checkpoint` mirror exactly reproduce the old semantics; the target is the current replay
53
54
  of that prefix, without absorbing later events. Both paths CAS-check the attempt and mirror and
54
- record backup/audit. Repair never reclassifies registry attempts or accepts a tuple, operation,
55
- or mirror that cannot be fully re-derived.
55
+ record backup/audit. The only narrow acknowledgement exception covers `enqueued`/`degraded`
56
+ attempts whose outbox was frozen and whose event IDs that same repair run consumed in full;
57
+ partial coverage does not change the attempt. Repair does not scan or reclassify historical
58
+ attempts and does not accept a tuple, operation, or mirror that cannot be fully re-derived.
59
+ - As of 0.66.4, `memory recover-attempt` targets one session and is a dry run by default. The
60
+ session must exist in the registry and its latest attempt must be `v2`, `applied`, and `enqueued`
61
+ or `degraded`, with non-empty, unique `event_ids`. Every event must be present in the ledger and
62
+ belong to the attempt's project/session/activation/epoch/turn; no later event from that session
63
+ or target event still in the outbox may exist, and SHARED/candidates must byte-for-byte reproduce
64
+ the full ledger projection. An already `projected` attempt is accepted only with a valid
65
+ checkpoint and returns `unchanged`.
66
+ - With `--apply`, `memory recover-attempt` changes only `SESSION_REGISTRY`: it marks
67
+ `last_memory_attempt`/`memory_status` as `projected` and stores the same checkpoint in the
68
+ attempt and `memory_checkpoint`. Ledger, CORE, SHARED, candidates, outbox, and notes remain
69
+ byte-identical. The command validates all authority again under `MEMORY.lock`, CAS-checks the
70
+ attempt, activation, epoch, turn, and checkpoint, and fails closed if any byte/context changes.
71
+ A busy lock is not reaped; retry after application returns `unchanged` without writing.
56
72
  - `memory reconcile` is a dry run by default. `--apply` requires two named sessions plus a reason,
57
73
  CAS-checks the exact attempt, backs up the registry, and limits mutation to the ambiguous attempt
58
74
  and its successor. Replay is CORE-aware, checkpoints use the physical ledger cursor, and the
@@ -81,11 +97,16 @@ npx wendkeep validate-memory --vault <v2-vault>
81
97
  identity, backup, audit, and CAS; it does not reorder, rewrite, or append a ledger event.
82
98
  - `validate-memory <CORE.md>` checks the 25-line cap, required sections, and secrets.
83
99
  - `validate-memory --vault` requires a complete v2 bundle and is not the legacy-vault gate.
100
+ - For `recover-attempt`, exit `0` means a valid dry run/apply, including `unchanged`; exit `1`
101
+ means a precondition, authority, CAS, topology, or lock check failed; exit `2` means a missing
102
+ session/`--vault`, unknown or duplicate option, extra argument, or invalid value.
84
103
 
85
104
  ## Examples
86
105
 
87
106
  ```bash
88
107
  npx wendkeep memory status --gate --vault .MyApp-vault
108
+ npx wendkeep memory recover-attempt session-123 --vault .MyApp-vault
109
+ npx wendkeep memory recover-attempt session-123 --apply --vault .MyApp-vault
89
110
  npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --vault .MyApp-vault
90
111
  npx wendkeep memory reconcile old --by-session current --reason "delivery continued" --apply --vault .MyApp-vault
91
112
  npx wendkeep validate-memory .MyApp-vault/.brain/CORE.md
@@ -107,6 +128,14 @@ of a global projection that has already advanced with concurrent events.
107
128
  repair merely to manufacture the first event.
108
129
  - `degraded` with every event ID present in either the ledger or an intact outbox is recoverable;
109
130
  let idempotent replay finish. An event ID absent from both locations means lost publication.
131
+ - Status/doctor reports `projected acknowledgement pending` and suggests
132
+ `memory recover-attempt <session>`: preserve the artifacts, inspect the dry-run JSON first, and
133
+ use `--apply` only when `eligible: true`. `dry-run` confirms eligibility; `applied` updates
134
+ registry/checkpoint; `unchanged` means the recovery was already applied idempotently.
135
+ - `recover-attempt` rejects a missing/divergent event, a target still in the outbox, stale
136
+ SHARED/candidates, a historical attempt, mismatched session/causal context, invalid checkpoint,
137
+ or busy lock. Do not bypass the gate by editing files: rerun `memory status --gate`, preserve
138
+ evidence, and resolve the divergent authority.
110
139
  - An `ambiguous` attempt, an `applied` attempt without event IDs, a `projected` event found only in
111
140
  the outbox, or a mismatched checkpoint is blocking: preserve the artifacts and investigate
112
141
  before repair. If the ambiguity is demonstrably superseded by a successor session, inspect the
@@ -25,6 +25,7 @@ Informe o vault explicitamente em automações. Preserve backups e evidências a
25
25
  ```bash
26
26
  npx wendkeep memory status [--gate] --vault <cofre>
27
27
  npx wendkeep memory repair --vault <cofre>
28
+ npx wendkeep memory recover-attempt <sessão> [--apply] --vault <cofre>
28
29
  npx wendkeep memory reconcile <sessão-ambígua> --by-session <sessão-sucessora> --reason <motivo> [--apply] --vault <cofre>
29
30
  npx wendkeep memory promote <candidate> [--event <event-id>] --vault <cofre>
30
31
  npx wendkeep memory reject <candidate> --vault <cofre>
@@ -51,8 +52,24 @@ npx wendkeep validate-memory --vault <cofre-v2>
51
52
  assert-only somente quando revision, cursor, hash, identidade, turns e o espelho
52
53
  `memory_checkpoint` reproduzem exatamente a semântica antiga; o alvo é o replay atual daquele
53
54
  prefixo, sem absorver eventos posteriores. Ambos os casos fazem CAS do attempt e do espelho e
54
- registram backup/auditoria. O repair nunca reclassifica attempts do registry nem aceita tuple,
55
- operação ou espelho que não seja rederivado integralmente.
55
+ registram backup/auditoria. A única exceção estreita de acknowledgement cobre attempts
56
+ `enqueued`/`degraded` cuja outbox foi congelada e cujos event IDs a mesma execução do repair
57
+ consumiu integralmente; cobertura parcial não altera o attempt. O repair não varre nem
58
+ reclassifica attempts históricos e não aceita tuple, operação ou espelho que não seja
59
+ rederivado integralmente.
60
+ - Desde a 0.66.4, `memory recover-attempt` é dirigido a uma única sessão e faz dry-run por padrão.
61
+ A sessão deve existir no registry e possuir o último attempt `v2`, `applied`, em `enqueued` ou
62
+ `degraded`, com `event_ids` não vazios e únicos. Todos os eventos devem estar integralmente no
63
+ ledger, pertencer ao mesmo projeto/sessão/activation/epoch/turn do attempt, não pode haver evento
64
+ posterior da mesma sessão nem evento alvo restante na outbox, e SHARED/candidates devem
65
+ reproduzir byte a byte a projeção integral do ledger. Um attempt já `projected` só é aceito com
66
+ checkpoint válido e retorna `unchanged`.
67
+ - Com `--apply`, `memory recover-attempt` altera somente `SESSION_REGISTRY`: marca
68
+ `last_memory_attempt`/`memory_status` como `projected` e grava o checkpoint idêntico no attempt e
69
+ em `memory_checkpoint`. Ledger, CORE, SHARED, candidates, outbox e notas permanecem byte-intactos.
70
+ O comando valida novamente toda a autoridade sob `MEMORY.lock`, faz CAS do attempt, activation,
71
+ epoch, turno e checkpoint e falha fechado se qualquer byte/contexto mudar. Lock ocupado não é
72
+ colhido; retry após aplicação retorna `unchanged` sem escrita.
56
73
  - `memory reconcile` é dry-run por padrão. `--apply` exige duas sessões nomeadas e motivo, faz CAS
57
74
  do attempt exato, salva backup do registry e limita a mutação ao attempt ambíguo e à sucessora.
58
75
  O replay é CORE-aware, usa cursor físico do ledger no checkpoint e não reescreve ledger, CORE ou
@@ -78,11 +95,16 @@ npx wendkeep validate-memory --vault <cofre-v2>
78
95
  CAS; ele não reordena, reescreve nem acrescenta evento ao ledger.
79
96
  - `validate-memory <CORE.md>` valida cap de 25 linhas, seções e segredos.
80
97
  - `validate-memory --vault` exige bundle v2 completo; não é o gate correto para vault legado.
98
+ - Para `recover-attempt`, exit `0` indica dry-run/apply válido, inclusive `unchanged`; exit `1`
99
+ indica falha de pré-condição, autoridade, CAS, topologia ou lock; exit `2` indica
100
+ sessão/`--vault` ausente, opção desconhecida/duplicada, argumento extra ou valor inválido.
81
101
 
82
102
  ## Exemplos
83
103
 
84
104
  ```bash
85
105
  npx wendkeep memory status --gate --vault .MeuApp-vault
106
+ npx wendkeep memory recover-attempt sessao-123 --vault .MeuApp-vault
107
+ npx wendkeep memory recover-attempt sessao-123 --apply --vault .MeuApp-vault
86
108
  npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --vault .MeuApp-vault
87
109
  npx wendkeep memory reconcile antiga --by-session atual --reason "entrega continuada" --apply --vault .MeuApp-vault
88
110
  npx wendkeep validate-memory .MeuApp-vault/.brain/CORE.md
@@ -104,6 +126,14 @@ prefixo válido de uma projeção global que já avançou com eventos concorrent
104
126
  fabricar o primeiro evento.
105
127
  - `degraded` com todos os event IDs presentes no ledger ou na outbox íntegra é recuperável; deixe o
106
128
  replay idempotente concluir. Event ID ausente nos dois lugares indica publicação perdida.
129
+ - Status/doctor informa `acknowledgement projetado pendente` e sugere
130
+ `memory recover-attempt <sessão>`: preserve os artefatos, revise primeiro o JSON do dry-run e só
131
+ use `--apply` se `eligible: true`. `dry-run` confirma elegibilidade; `applied` atualiza
132
+ registry/checkpoint; `unchanged` indica que a recuperação já foi aplicada de forma idempotente.
133
+ - `recover-attempt` recusa evento ausente/divergente, outbox alvo ainda presente, SHARED/candidates
134
+ stale, attempt histórico, sessão/contexto causal divergente, checkpoint inválido ou lock ocupado.
135
+ Não tente contornar o gate com edição manual: rode novamente `memory status --gate`, preserve a
136
+ evidência e resolva a autoridade divergente.
107
137
  - Attempt `ambiguous`, attempt `applied` sem event IDs, evento `projected` apenas na outbox ou
108
138
  checkpoint divergente são bloqueantes: preserve os artefatos e investigue antes de repair. Se a
109
139
  ambiguidade for comprovadamente substituída por uma sessão sucessora, revise o dry-run de
@@ -338,6 +338,12 @@ function checkMemoryAttempts(registry, {
338
338
  const missing = eventIds.filter((eventId) => !ledgerEventIds.has(eventId) && !outboxEventIds.has(eventId));
339
339
  if (missing.length) {
340
340
  failures.push(`Attempt v2 perdeu ${missing.length} evento(s): ausentes do ledger e da outbox. Inspecione com: ${MEMORY_STATUS_COMMAND}.`);
341
+ } else if (
342
+ state === 'enqueued'
343
+ && eventIds.every((eventId) => ledgerEventIds.has(eventId))
344
+ && eventIds.every((eventId) => !outboxEventIds.has(eventId))
345
+ ) {
346
+ warnings.push(`Attempt de memória v2 possui acknowledgement projetado pendente. Recupere com: wendkeep memory recover-attempt ${sessionId}.`);
341
347
  } else {
342
348
  warnings.push(`Attempt de memória v2 ${state} permanece recuperável: ${eventIds.length} evento(s) durável(is) no ledger e/ou outbox.`);
343
349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.66.3",
3
+ "version": "0.66.4",
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": [
@@ -100,6 +100,7 @@ Usage:
100
100
  behind the closing block. Dry-run by default · --apply · --json.
101
101
  wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
102
102
  wendkeep memory <sub> Shared memory v2: status | migrate [--apply] | repair |
103
+ recover-attempt <session> [--apply] |
103
104
  reconcile <session> --by-session <session> --reason <text> [--apply] |
104
105
  promote <candidate> [--event <event-id>] | reject <candidate>. --vault P.
105
106
  Reconcile is dry-run by default; the original attempt remains audited.
@@ -313,6 +313,8 @@ export function readMemoryLedger(vaultBase) {
313
313
  if (eventIds.has(parsed.event_id)) {
314
314
  if (eventPayloads.get(parsed.event_id) !== payload) {
315
315
  errors.push(ledgerError(lineNumber, `event_id collision: ${parsed.event_id}`, partial));
316
+ } else {
317
+ errors.push(ledgerError(lineNumber, `duplicate event_id: ${parsed.event_id}`, partial));
316
318
  }
317
319
  return;
318
320
  }
@@ -911,15 +913,18 @@ function projectLocked(vaultBase, { faultAt } = {}) {
911
913
  const projection = publishMemoryProjection(vaultBase, prepared);
912
914
  injectFault(faultAt, 'after-projection');
913
915
 
916
+ const consumedEventIds = [];
914
917
  for (const entry of outbox) {
915
918
  unlinkVaultFile(vaultBase, entry.path, {
916
919
  missingOk: false, label: 'evento consumido do outbox de memória',
917
920
  });
921
+ consumedEventIds.push(entry.event.event_id);
918
922
  }
919
923
  return {
920
924
  status: 'projected',
921
925
  appended: newEvents.length,
922
926
  consumed: outbox.length,
927
+ consumedEventIds,
923
928
  pending: 0,
924
929
  ...projection,
925
930
  };
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;