wendkeep 0.75.0 → 0.75.2
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 +26 -0
- package/README.en.md +8 -5
- package/README.md +8 -5
- package/docs/en/commands/maintenance-and-diagnostics.md +4 -0
- package/docs/en/commands/memory.md +13 -9
- package/docs/en/commands/observer.md +7 -2
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +4 -0
- package/docs/pt-BR/commands/memory.md +13 -9
- package/docs/pt-BR/commands/observer.md +5 -0
- package/hooks/vault-health.mjs +26 -1
- package/package.json +2 -2
- package/packages/cli/src/index.mjs +3 -2
- package/packages/vault/src/memory-candidate-policy.mjs +47 -0
- package/src/doctor.mjs +2 -1
- package/src/memory-curate.mjs +97 -3
- package/src/memory.mjs +92 -23
- package/src/observer-sql-publish.mjs +27 -13
- package/src/observer.mjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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.75.2] — 2026-08-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Handoffs legados reescopáveis.** `memory rescope` agora separa individualmente eventos
|
|
12
|
+
`handoff.latest` ambíguos quando a sessão de origem é comprovável. A migração permanece
|
|
13
|
+
append-only, não escolhe vencedor e conserva ambiguidades reais dentro da mesma work session.
|
|
14
|
+
- **Curadoria proporcional.** `memory curate` mostra conflitos acionáveis por padrão; `--all`
|
|
15
|
+
inclui handoffs de sessões encerradas com status/change sanitizados e permite `H` para encerrar
|
|
16
|
+
em lote somente recomendações seguras, sempre com confirmação e releitura entre decisões.
|
|
17
|
+
- **Diagnóstico sem falso bloqueio.** O doctor separa conflitos acionáveis de handoffs históricos
|
|
18
|
+
reparáveis no mesmo snapshot. Dívida histórica isolada gera atenção e orienta `memory rescope`,
|
|
19
|
+
sem degradar a memória nem ocultar a contagem restante.
|
|
20
|
+
|
|
21
|
+
## [0.75.1] — 2026-08-20
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Coalescência sem perda.** A outbox seleciona o identificador próprio de cada tipo de evento;
|
|
26
|
+
chamadas, agentes, rollups e transcripts distintos da mesma sessão não disputam mais a mesma chave.
|
|
27
|
+
- **Reconciliação realmente integral.** `observer reconcile --url` e `observer memory import`
|
|
28
|
+
ignoram o cursor incremental local, consultam as revisões remotas e regeneram documentos,
|
|
29
|
+
consumo, chamadas e transcripts mesmo após restauração ou troca do Observer.
|
|
30
|
+
- **Leases compatíveis com o transporte.** Locks de batch e publisher permanecem válidos além do
|
|
31
|
+
timeout máximo de request e só podem ser liberados pelo proprietário que os adquiriu.
|
|
32
|
+
|
|
7
33
|
## [0.75.0] — 2026-08-20
|
|
8
34
|
|
|
9
35
|
### Added
|
package/README.en.md
CHANGED
|
@@ -88,7 +88,7 @@ Decisions, dead ends, the reason you chose X over Y — gone next session. The p
|
|
|
88
88
|
| **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault; research previews without a final rate remain unestimated. |
|
|
89
89
|
| **Multi‑agent** — one vault, both agents | `init` wires the session hooks into `.claude/settings.json` *and* `.codex/hooks.json`, and every note is tagged with the agent that wrote it: Claude Code is detected from its environment, anything else is recorded as Codex. One shared graph, whichever agent you are in. |
|
|
90
90
|
| **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
|
|
91
|
-
| **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite. Identities and foreign keys are project-scoped; each event is atomic. Hooks publish only what changed
|
|
91
|
+
| **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite. Identities and foreign keys are project-scoped; each event is atomic. Hooks publish only what changed; `observer reconcile --url` ignores the incremental cursor to regenerate the complete projection while preserving local/remote revision baselines. |
|
|
92
92
|
|
|
93
93
|
During historical migration, the Observer preserves differences between frontmatter totals and the
|
|
94
94
|
ledger as explicit reconciliation rows, and disambiguates duplicate `session_id` values per file
|
|
@@ -413,10 +413,13 @@ CAS-migrated on both the attempt and `memory_checkpoint` to the correct physical
|
|
|
413
413
|
backup/audit; divergent mirrors fail closed. A demonstrably superseded
|
|
414
414
|
ambiguity uses `memory reconcile <session> --by-session <successor>
|
|
415
415
|
--reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
|
|
416
|
-
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
416
|
+
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. `memory rescope`
|
|
417
|
+
append-only separates legacy handoffs with proven identity even when they already participate in
|
|
418
|
+
candidates; it never selects a winner. Actionable conflicts require explicit, durable curation.
|
|
419
|
+
Start with `memory curate --vault <vault>`: the menu shows actionable conflicts only, groups them
|
|
420
|
+
under friendly names, shows sanitized previews, and confirms every write with default `no`.
|
|
421
|
+
`memory curate --all` includes proven historical handoffs and lets `H` batch-close them after
|
|
422
|
+
confirmation. In a non-TTY environment, use
|
|
420
423
|
`memory candidates --active --vault <vault>` to list only safe IDs and metadata in read-only mode —
|
|
421
424
|
it does not expose memory values or content. After human review,
|
|
422
425
|
`memory promote <id> --event <event-id>` selects one event from the candidate, while
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Decisions, dead ends, the reason you chose X over Y — gone next session. The p
|
|
|
88
88
|
| **Cost** — what it all cost | Per‑model, cache‑aware token pricing per session — plus `cost --trend` with a run‑rate projection across the whole vault; research previews without a final rate remain unestimated. |
|
|
89
89
|
| **Multi‑agent** — one vault, both agents | `init` wires the session hooks into `.claude/settings.json` *and* `.codex/hooks.json`, and every note is tagged with the agent that wrote it: Claude Code is detected from its environment, anything else is recorded as Codex. One shared graph, whichever agent you are in. |
|
|
90
90
|
| **Local‑first** — no cloud, no account | Everything is plain Markdown on your disk. An optional MCP server (`@bitbonsai/mcpvault`) lets the agent read/write the vault. |
|
|
91
|
-
| **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite. Identities and foreign keys are project-scoped; each event is atomic. Hooks publish only what changed
|
|
91
|
+
| **Local Observer** — many projects, one view | `wendkeep observer` keeps documents, FTS5 chunks, sessions, agents, tokens, costs, calls, and transcripts in SQLite. Identities and foreign keys are project-scoped; each event is atomic. Hooks publish only what changed; `observer reconcile --url` ignores the incremental cursor to regenerate the complete projection while preserving local/remote revision baselines. |
|
|
92
92
|
|
|
93
93
|
During historical migration, the Observer preserves differences between frontmatter totals and the
|
|
94
94
|
ledger as explicit reconciliation rows, and disambiguates duplicate `session_id` values per file
|
|
@@ -413,10 +413,13 @@ CAS-migrated on both the attempt and `memory_checkpoint` to the correct physical
|
|
|
413
413
|
backup/audit; divergent mirrors fail closed. A demonstrably superseded
|
|
414
414
|
ambiguity uses `memory reconcile <session> --by-session <successor>
|
|
415
415
|
--reason <reason>` as a dry run and requires `--apply`; the decision is backed up and audited
|
|
416
|
-
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
416
|
+
without rewriting ledger, CORE, or notes. Run `status --gate` again afterwards. `memory rescope`
|
|
417
|
+
append-only separates legacy handoffs with proven identity even when they already participate in
|
|
418
|
+
candidates; it never selects a winner. Actionable conflicts require explicit, durable curation.
|
|
419
|
+
Start with `memory curate --vault <vault>`: the menu shows actionable conflicts only, groups them
|
|
420
|
+
under friendly names, shows sanitized previews, and confirms every write with default `no`.
|
|
421
|
+
`memory curate --all` includes proven historical handoffs and lets `H` batch-close them after
|
|
422
|
+
confirmation. In a non-TTY environment, use
|
|
420
423
|
`memory candidates --active --vault <vault>` to list only safe IDs and metadata in read-only mode —
|
|
421
424
|
it does not expose memory values or content. After human review,
|
|
422
425
|
`memory promote <id> --event <event-id>` selects one event from the candidate, while
|
|
@@ -104,6 +104,10 @@ dry-run path before any write.
|
|
|
104
104
|
the evidence, and then use
|
|
105
105
|
`memory promote <candidate-id> --event <event-id> --vault <vault>` to select an event or
|
|
106
106
|
`memory reject <candidate-id> --vault <vault>` to keep the current operational value.
|
|
107
|
+
- Historical handoffs from closed sessions are repairable debt, not actionable conflicts. Run
|
|
108
|
+
`memory rescope --vault <vault>` first and review the dry run, then apply with `--apply`. If debt
|
|
109
|
+
remains, `memory curate --all --vault <vault>` shows context and offers `H` to batch-close safe
|
|
110
|
+
recommendations only, always after confirmation.
|
|
107
111
|
- `legacy`/`degraded`/`stale`/`manifest-unproven` observability: run
|
|
108
112
|
`npx --no-install wendkeep cost rebuild --session <id> --json --vault <vault>`, review diagnostics,
|
|
109
113
|
and only then authorize the second variant with `--apply`.
|
|
@@ -28,7 +28,7 @@ Pass the vault explicitly in automation. Preserve backups and evidence before re
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npx wendkeep memory status [--gate] --vault <vault>
|
|
31
|
-
npx wendkeep memory curate --vault <vault>
|
|
31
|
+
npx wendkeep memory curate [--all] --vault <vault>
|
|
32
32
|
npx wendkeep memory candidates [--active] --vault <vault>
|
|
33
33
|
npx wendkeep memory rescope [--apply] --vault <vault>
|
|
34
34
|
npx wendkeep memory repair --vault <vault>
|
|
@@ -43,12 +43,14 @@ npx wendkeep validate-memory --vault <v2-vault>
|
|
|
43
43
|
## Options and exit codes
|
|
44
44
|
|
|
45
45
|
- `memory status` is read-only; `--gate` exits `1` only for blocking state.
|
|
46
|
-
- `memory curate` is the recommended human path
|
|
47
|
-
|
|
48
|
-
`P` to skip, `R` to reject, `D` for technical details,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
- `memory curate` is the recommended human path and shows actionable conflicts only by default.
|
|
47
|
+
In an interactive terminal it groups conflicts under friendly names, shows sanitized previews
|
|
48
|
+
and context, and offers numbered choices, `P` to skip, `R` to reject, `D` for technical details,
|
|
49
|
+
and `Q` to quit. `--all` includes handoffs from proven closed sessions; for those, `H` batch-closes
|
|
50
|
+
only safe recommendations while reloading authority between decisions. Every write asks for
|
|
51
|
+
confirmation with default `no`: Enter or `N` does not write. Skip or quit preserves the
|
|
52
|
+
remaining conflicts so a later run can resume them.
|
|
53
|
+
- The assistant accepts `--vault` and `--all`: there is no `--yes` or `--apply`. In a
|
|
52
54
|
non-TTY environment it exits `2` without changing bytes and recommends the advanced fallback
|
|
53
55
|
`memory candidates --active`.
|
|
54
56
|
- `memory candidates` is read-only and prints deterministic JSON containing only `candidate_id`,
|
|
@@ -98,8 +100,10 @@ npx wendkeep validate-memory --vault <v2-vault>
|
|
|
98
100
|
decision is idempotent.
|
|
99
101
|
- `memory rescope` is a dry run by default and lists only planned IDs, keys, and scopes. With
|
|
100
102
|
`--apply`, it appends explicit project, work-session, change, branch, or worktree events while
|
|
101
|
-
preserving historic bytes as the ledger prefix.
|
|
102
|
-
|
|
103
|
+
preserving historic bytes as the ledger prefix. Legacy `handoff.latest` events inside candidates
|
|
104
|
+
are also rescoped individually when they carry proven session identity, separating independent
|
|
105
|
+
workflows without selecting a winner. Ambiguities that remain inside one scope stay under human
|
|
106
|
+
curation; retry returns `unchanged`.
|
|
103
107
|
- Registers such as `git.local-head`, `handoff.latest`, `quality.latest-*`, and
|
|
104
108
|
`change.<slug>.status` compete only inside the same scope. Automatic resolution still requires
|
|
105
109
|
the same project and causal lineage; incompatible decisions, constraints, and blockers remain
|
|
@@ -92,8 +92,13 @@ agent sessions, cost rollups, and calls. Messages and transcripts are only sent
|
|
|
92
92
|
that explicitly enable them. The container stores everything in
|
|
93
93
|
`/data/observer.sqlite`; it does not mount `C:\GitHub` or any `.WendKeep-vault`. Markdown is only
|
|
94
94
|
the text held in SQL and is recreated as files only by an explicit read-only export.
|
|
95
|
-
`memory import` performs the initial load and returns file/hash parity.
|
|
96
|
-
|
|
95
|
+
`memory import` performs the initial load and returns file/hash parity. The remote recovery
|
|
96
|
+
commands `observer reconcile --url` and `observer memory import` do not use the
|
|
97
|
+
incremental cursor to decide what to omit: they regenerate documents, sessions, usage, calls, and
|
|
98
|
+
transcripts. The cursor still supplies the local revision baseline and the remote tree, when
|
|
99
|
+
available, supplies the highest known baseline; a transient read failure never lowers the locally
|
|
100
|
+
persisted revision. During migration, the cost/token total recorded in frontmatter is preserved
|
|
101
|
+
through an explicit reconciliation row when
|
|
97
102
|
the detailed ledger does not add up; that row does not invent calls. Historical sessions sharing
|
|
98
103
|
one `session_id` receive a canonical per-file identity so one rollup cannot overwrite the other.
|
|
99
104
|
|
|
@@ -102,6 +102,10 @@ ou sem manifest comprovado e oferece um caminho dry-run antes de qualquer escrit
|
|
|
102
102
|
liste os IDs seguros com `memory candidates --active --vault <cofre>`, revise a evidência e use
|
|
103
103
|
`memory promote <candidate-id> --event <event-id> --vault <cofre>` para selecionar um evento ou
|
|
104
104
|
`memory reject <candidate-id> --vault <cofre>` para manter o valor operacional atual.
|
|
105
|
+
- Handoffs históricos de sessões encerradas são dívida reparável, não conflito acionável. Rode
|
|
106
|
+
primeiro `memory rescope --vault <cofre>` e revise o dry-run; aplique com `--apply`. Se restar
|
|
107
|
+
dívida, `memory curate --all --vault <cofre>` mostra o contexto e oferece `H` para encerrar em
|
|
108
|
+
lote somente as recomendações seguras, sempre com confirmação.
|
|
105
109
|
- Observabilidade `legacy`/`degraded`/`stale`/`manifest-unproven`: rode
|
|
106
110
|
`npx --no-install wendkeep cost rebuild --session <id> --json --vault <cofre>`, revise diagnostics
|
|
107
111
|
e só então autorize a segunda variante com `--apply`.
|
|
@@ -28,7 +28,7 @@ Informe o vault explicitamente em automações. Preserve backups e evidências a
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
npx wendkeep memory status [--gate] --vault <cofre>
|
|
31
|
-
npx wendkeep memory curate --vault <cofre>
|
|
31
|
+
npx wendkeep memory curate [--all] --vault <cofre>
|
|
32
32
|
npx wendkeep memory candidates [--active] --vault <cofre>
|
|
33
33
|
npx wendkeep memory rescope [--apply] --vault <cofre>
|
|
34
34
|
npx wendkeep memory repair --vault <cofre>
|
|
@@ -43,12 +43,14 @@ npx wendkeep validate-memory --vault <cofre-v2>
|
|
|
43
43
|
## Opções e códigos de saída
|
|
44
44
|
|
|
45
45
|
- `memory status` é read-only; `--gate` retorna exit `1` apenas para estado bloqueante.
|
|
46
|
-
- `memory curate` é o caminho recomendado para pessoas:
|
|
47
|
-
|
|
48
|
-
`P` para pular, `R` para rejeitar, `D` para detalhes
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
- `memory curate` é o caminho recomendado para pessoas: por padrão mostra somente conflitos
|
|
47
|
+
acionáveis. Em um terminal interativo, agrupa por nome amigável, mostra previews e contexto
|
|
48
|
+
sanitizados e oferece escolhas numeradas, `P` para pular, `R` para rejeitar, `D` para detalhes
|
|
49
|
+
técnicos e `Q` para sair. `--all` inclui handoffs de sessões comprovadamente encerradas; nesses
|
|
50
|
+
casos, `H` encerra em lote somente as recomendações seguras, relendo a autoridade entre cada
|
|
51
|
+
decisão. Toda escrita pede confirmação com padrão negativo: Enter ou `N` não grava. Pular ou
|
|
52
|
+
sair preserva o restante para retomar em outra execução.
|
|
53
|
+
- O assistente aceita `--vault` e `--all`: não há `--yes` nem `--apply`. Em ambiente
|
|
52
54
|
não-TTY/terminal não interativo, ele retorna exit `2` sem alterar bytes e orienta usar o fallback
|
|
53
55
|
avançado `memory candidates --active`.
|
|
54
56
|
- `memory candidates` é read-only e imprime JSON determinístico com somente `candidate_id`,
|
|
@@ -98,8 +100,10 @@ npx wendkeep validate-memory --vault <cofre-v2>
|
|
|
98
100
|
notas, nem consome a outbox. Repetir a mesma decisão aplicada é idempotente.
|
|
99
101
|
- `memory rescope` é dry-run por padrão e lista somente IDs, chaves e escopos planejados. Com
|
|
100
102
|
`--apply`, anexa eventos explícitos de projeto, work session, change, branch ou worktree e mantém
|
|
101
|
-
os bytes históricos como prefixo do ledger.
|
|
102
|
-
|
|
103
|
+
os bytes históricos como prefixo do ledger. Eventos legados de `handoff.latest` que participam
|
|
104
|
+
de candidates também são reescopados individualmente quando possuem identidade de sessão
|
|
105
|
+
comprovável: isso separa workflows independentes sem selecionar vencedor. Ambiguidades que
|
|
106
|
+
permanecem no mesmo escopo continuam sob curadoria; uma repetição retorna `unchanged`.
|
|
103
107
|
- Registradores como `git.local-head`, `handoff.latest`, `quality.latest-*` e
|
|
104
108
|
`change.<slug>.status` só competem dentro do mesmo escopo. Resolução automática ainda exige o
|
|
105
109
|
mesmo projeto e linhagem causal; decisões, constraints e blockers incompatíveis permanecem sob
|
|
@@ -94,6 +94,11 @@ níveis de captura que os habilitam. O container grava tudo em
|
|
|
94
94
|
`/data/observer.sqlite`; não monta `C:\GitHub` nem qualquer `.WendKeep-vault`. Markdown é aceito
|
|
95
95
|
somente como conteúdo de uma coluna SQL e volta a existir como arquivo apenas pela exportação
|
|
96
96
|
read-only sob demanda. `memory import` faz a carga inicial e retorna a paridade por arquivo e hash.
|
|
97
|
+
Na recuperação remota, `observer reconcile --url` e `observer memory import` não usam o cursor
|
|
98
|
+
incremental para decidir o que omitir: regeneram documentos, sessões, consumo, chamadas e
|
|
99
|
+
transcripts. O cursor continua fornecendo o baseline local de revisão e a árvore remota, quando
|
|
100
|
+
disponível, fornece o maior baseline conhecido; uma falha temporária nessa leitura nunca reduz a
|
|
101
|
+
revisão persistida localmente.
|
|
97
102
|
Na migração, o total de custo/token registrado no frontmatter é preservado por uma linha de
|
|
98
103
|
reconciliação quando o ledger detalhado não fecha com ele; essa linha não inventa chamadas.
|
|
99
104
|
Sessões históricas com o mesmo `session_id` recebem uma identidade canônica por arquivo para
|
package/hooks/vault-health.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { detectMemoryMode, LEGACY_MEMORY_WARNING } from './memory-mode.mjs';
|
|
|
18
18
|
import { deriveMemoryProjection } from './memory-store.mjs';
|
|
19
19
|
import { assertVaultPathSafe, assertVaultPathsSafe } from './vault-path-safety.mjs';
|
|
20
20
|
import { validateMemoryBundle } from '../src/validate-memory.mjs';
|
|
21
|
+
import { isHistoricalHandoffCandidate } from '../packages/vault/src/memory-candidate-policy.mjs';
|
|
21
22
|
|
|
22
23
|
const DEFAULT_PENDING_PATTERNS = [
|
|
23
24
|
/^- \[ \] Revisar resumo da sessão$/i,
|
|
@@ -115,6 +116,12 @@ const memoryCandidatesCommand = (vaultBase) => (
|
|
|
115
116
|
const memoryCurateCommand = (vaultBase) => (
|
|
116
117
|
`${WENDKEEP_COMMAND} memory curate --vault ${quoteCommandArgument(vaultBase)}`
|
|
117
118
|
);
|
|
119
|
+
const memoryCurateAllCommand = (vaultBase) => (
|
|
120
|
+
`${WENDKEEP_COMMAND} memory curate --all --vault ${quoteCommandArgument(vaultBase)}`
|
|
121
|
+
);
|
|
122
|
+
const memoryRescopeCommand = (vaultBase, { apply = false } = {}) => (
|
|
123
|
+
`${WENDKEEP_COMMAND} memory rescope${apply ? ' --apply' : ''} --vault ${quoteCommandArgument(vaultBase)}`
|
|
124
|
+
);
|
|
118
125
|
|
|
119
126
|
const MEMORY_KEY_PURPOSES = new Map([
|
|
120
127
|
['handoff.latest', 'próximo handoff'],
|
|
@@ -231,6 +238,7 @@ function memoryMetrics() {
|
|
|
231
238
|
pendingOutbox: 0,
|
|
232
239
|
candidates: 0,
|
|
233
240
|
activeConflicts: 0,
|
|
241
|
+
repairableHandoffs: 0,
|
|
234
242
|
semanticStatus: null,
|
|
235
243
|
semanticCode: null,
|
|
236
244
|
semanticActiveKeys: [],
|
|
@@ -497,7 +505,12 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
|
497
505
|
warnings.push(...lifecycle.warnings);
|
|
498
506
|
|
|
499
507
|
const unresolved = candidates.items.filter((item) => !['resolved', 'rejected', 'superseded'].includes(item?.status));
|
|
500
|
-
const
|
|
508
|
+
const allConflicts = unresolved.filter((item) => item?.reason === 'conflict');
|
|
509
|
+
const historicalHandoffs = allConflicts.filter(
|
|
510
|
+
(item) => isHistoricalHandoffCandidate(item, effectiveRegistry),
|
|
511
|
+
);
|
|
512
|
+
const historicalIds = new Set(historicalHandoffs.map((item) => item.candidate_id));
|
|
513
|
+
const activeConflicts = allConflicts.filter((item) => !historicalIds.has(item.candidate_id));
|
|
501
514
|
const ordinaryCandidates = unresolved.filter((item) => item?.reason !== 'conflict');
|
|
502
515
|
if (activeConflicts.length) {
|
|
503
516
|
const label = activeConflicts.length === 1
|
|
@@ -506,6 +519,17 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
|
506
519
|
const purposes = groupConflictPurposes(activeConflicts).join('; ');
|
|
507
520
|
warnings.push(`${label} (${purposes}). Existem versões concorrentes e nenhum dado foi escolhido automaticamente. Conflito semântico degrada somente as chaves afetadas e exige curadoria humana; memory repair não escolhe vencedor. Próximo passo: ${memoryCurateCommand(vaultBase)}. Inventário avançado: ${memoryCandidatesCommand(vaultBase)}.`);
|
|
508
521
|
}
|
|
522
|
+
if (historicalHandoffs.length) {
|
|
523
|
+
const label = historicalHandoffs.length === 1
|
|
524
|
+
? '1 handoff histórico reparável'
|
|
525
|
+
: `${historicalHandoffs.length} handoffs históricos reparáveis`;
|
|
526
|
+
warnings.push(
|
|
527
|
+
`${label}; sessões de origem estão encerradas e nenhuma versão precisa virar estado atual. `
|
|
528
|
+
+ `Execute primeiro o dry-run: ${memoryRescopeCommand(vaultBase)}. `
|
|
529
|
+
+ `Se a prévia estiver correta: ${memoryRescopeCommand(vaultBase, { apply: true })}. `
|
|
530
|
+
+ `Dívida restante: ${memoryCurateAllCommand(vaultBase)}.`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
509
533
|
if (outbox.count) warnings.push(`${outbox.count} evento(s) pendente(s) na outbox; execute o projector quando seguro.`);
|
|
510
534
|
if (ordinaryCandidates.length) warnings.push(`${ordinaryCandidates.length} candidate(s) aguardando curadoria humana.`);
|
|
511
535
|
for (const warning of bundle.warnings || []) warnings.push(warning);
|
|
@@ -526,6 +550,7 @@ export function checkMemoryBundle(vaultBase, { registry } = {}) {
|
|
|
526
550
|
pendingOutbox: outbox.count,
|
|
527
551
|
candidates: candidates.items.length,
|
|
528
552
|
activeConflicts: activeConflicts.length,
|
|
553
|
+
repairableHandoffs: historicalHandoffs.length,
|
|
529
554
|
semanticStatus: semantic.status ?? null,
|
|
530
555
|
semanticCode: semantic.code ?? null,
|
|
531
556
|
semanticActiveKeys: semantic.activeKeys || [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.75.
|
|
3
|
+
"version": "0.75.2",
|
|
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": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
44
|
+
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
45
45
|
"test": "node --test --test-concurrency=2",
|
|
46
46
|
"test:core": "node scripts/run-scope.mjs core",
|
|
47
47
|
"release": "node scripts/release.mjs",
|
|
@@ -102,8 +102,9 @@ Usage:
|
|
|
102
102
|
in session notes from the linked derived notes — the body used to lag
|
|
103
103
|
behind the closing block. Dry-run by default · --apply · --json.
|
|
104
104
|
wendkeep lesson add "t" "l" Record a project-local lesson (injected at SessionStart).
|
|
105
|
-
wendkeep memory curate Guide
|
|
106
|
-
|
|
105
|
+
wendkeep memory curate Guide actionable semantic conflicts in an interactive terminal.
|
|
106
|
+
--all includes historical handoffs and enables confirmed safe batch close;
|
|
107
|
+
every promote/reject requires confirmation. --vault P.
|
|
107
108
|
wendkeep memory <sub> Shared memory v2: status | candidates [--active] | curate | migrate [--apply] | rescope [--apply] | repair |
|
|
108
109
|
recover-attempt <session> [--apply] |
|
|
109
110
|
reconcile <session> --by-session <session> --reason <text> [--apply] |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const TERMINAL_SESSION_STATUSES = new Set([
|
|
2
|
+
'done',
|
|
3
|
+
'superseded',
|
|
4
|
+
'completed',
|
|
5
|
+
'closed',
|
|
6
|
+
'inactive',
|
|
7
|
+
'abandoned',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function sessionEntry(registry, event) {
|
|
11
|
+
const sessionId = String(event?.canonical_session_id || '').trim();
|
|
12
|
+
return sessionId ? registry?.sessions?.[sessionId] || null : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Pure, fail-closed policy shared by doctor and interactive curation. */
|
|
16
|
+
export function classifyMemoryCandidate(candidate, registry = { sessions: {} }) {
|
|
17
|
+
if (candidate?.reason !== 'conflict' || candidate?.memory_key !== 'handoff.latest') {
|
|
18
|
+
return { classification: 'actionable' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const events = Array.isArray(candidate.events) ? candidate.events : [];
|
|
22
|
+
if (!events.length) return { classification: 'unknown' };
|
|
23
|
+
const entries = events.map((event) => sessionEntry(registry, event));
|
|
24
|
+
if (entries.some((entry) => !entry)) return { classification: 'unknown' };
|
|
25
|
+
if (entries.some((entry) => String(entry.status || '').trim() === 'active')) {
|
|
26
|
+
return { classification: 'actionable' };
|
|
27
|
+
}
|
|
28
|
+
if (entries.every((entry) => TERMINAL_SESSION_STATUSES.has(String(entry.status || '').trim()))) {
|
|
29
|
+
return { classification: 'historical', recommended_action: 'reject' };
|
|
30
|
+
}
|
|
31
|
+
return { classification: 'unknown' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function candidateSessionContext(event, registry = { sessions: {} }) {
|
|
35
|
+
const entry = sessionEntry(registry, event);
|
|
36
|
+
if (!entry) return {};
|
|
37
|
+
const status = String(entry.status || '').trim();
|
|
38
|
+
const changeSlug = String(entry.change_slug || entry.changeSlug || '').trim();
|
|
39
|
+
return {
|
|
40
|
+
...(status ? { session_status: status } : {}),
|
|
41
|
+
...(changeSlug ? { change_slug: changeSlug } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isHistoricalHandoffCandidate(candidate, registry) {
|
|
46
|
+
return classifyMemoryCandidate(candidate, registry).classification === 'historical';
|
|
47
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -39,7 +39,8 @@ export function renderVaultHealthLines(result) {
|
|
|
39
39
|
lines.push(
|
|
40
40
|
`[memória] ${healthStatusLabel(result.memoryStatus)} — schema: ${metricValue(memory.schemaVersion)} · revisão: ${metricValue(memory.revision)} · cursor: ${metricValue(memory.eventCursor)} · hash: ${metricValue(memory.stateHash)}`,
|
|
41
41
|
);
|
|
42
|
-
|
|
42
|
+
const repairableHandoffs = Number(memory.repairableHandoffs || 0);
|
|
43
|
+
lines.push(` ledger: ${metricValue(memory.ledgerEvents)} evento(s) · outbox: ${metricValue(memory.pendingOutbox)} · candidates: ${metricValue(memory.candidates)} · conflitos: ${metricValue(memory.activeConflicts)}${repairableHandoffs ? ` · handoffs reparáveis: ${repairableHandoffs}` : ''}`);
|
|
43
44
|
const semanticKeys = memory.semanticActiveKeys || [];
|
|
44
45
|
const semanticProjected = memory.semanticProjectedKeys || [];
|
|
45
46
|
const semanticMissing = memory.semanticMissingKeys || [];
|
package/src/memory-curate.mjs
CHANGED
|
@@ -21,10 +21,18 @@ const TEXT = {
|
|
|
21
21
|
observed: 'registrado em',
|
|
22
22
|
newer: 'mais recente',
|
|
23
23
|
actions: '[1-N] Manter uma versão · [P] Pular · [R] Encerrar sem vencedor · [D] Detalhes · [Q] Sair',
|
|
24
|
+
historicalAction: '[H] Encerrar todos os handoffs históricos recomendados',
|
|
25
|
+
terminalContext: 'sessão encerrada',
|
|
26
|
+
activeContext: 'sessão ativa',
|
|
27
|
+
changeContext: (slug) => `change: ${slug}`,
|
|
28
|
+
safeRecommendation: 'Recomendação segura: encerrar sem vencedor; as evidências permanecem no ledger.',
|
|
29
|
+
historicalOmitted: (count) => `${count} handoff(s) histórico(s) reparável(is) foram omitidos. Use --all para revisar ou encerrar em lote.`,
|
|
30
|
+
confirmHistorical: (count) => `Encerrar ${count} handoff(s) histórico(s) sem vencedor? Cada decisão será auditada. [s/N] `,
|
|
31
|
+
historicalClosed: (count) => `${count} handoff(s) histórico(s) encerrado(s) com decisão auditada.`,
|
|
24
32
|
choose: '> ',
|
|
25
33
|
confirmPromote: (number) => `Manter a versão ${number}? Essa decisão será gravada agora. [s/N] `,
|
|
26
34
|
confirmReject: 'Encerrar este conflito sem escolher uma versão? Essa decisão será gravada agora. [s/N] ',
|
|
27
|
-
invalid: 'Opção inválida. Escolha um número exibido, P, R, D ou Q.',
|
|
35
|
+
invalid: 'Opção inválida. Escolha um número exibido, P, R, D, H ou Q.',
|
|
28
36
|
declined: 'Decisão não confirmada; nenhum byte foi alterado.',
|
|
29
37
|
promoted: 'Versão promovida e decisão auditada.',
|
|
30
38
|
rejected: 'Conflito encerrado sem promover uma versão.',
|
|
@@ -47,10 +55,18 @@ const TEXT = {
|
|
|
47
55
|
observed: 'recorded at',
|
|
48
56
|
newer: 'newest timestamp',
|
|
49
57
|
actions: '[1-N] Keep one version · [P] Skip · [R] Close without a winner · [D] Details · [Q] Quit',
|
|
58
|
+
historicalAction: '[H] Close every safely recommended historical handoff',
|
|
59
|
+
terminalContext: 'closed session',
|
|
60
|
+
activeContext: 'active session',
|
|
61
|
+
changeContext: (slug) => `change: ${slug}`,
|
|
62
|
+
safeRecommendation: 'Safe recommendation: close without a winner; evidence remains in the ledger.',
|
|
63
|
+
historicalOmitted: (count) => `${count} repairable historical handoff(s) were omitted. Use --all to review or close them in a batch.`,
|
|
64
|
+
confirmHistorical: (count) => `Close ${count} historical handoff(s) without a winner? Every decision will be audited. [y/N] `,
|
|
65
|
+
historicalClosed: (count) => `${count} historical handoff(s) closed with audited decisions.`,
|
|
50
66
|
choose: '> ',
|
|
51
67
|
confirmPromote: (number) => `Keep version ${number}? This decision will be written now. [y/N] `,
|
|
52
68
|
confirmReject: 'Close this conflict without choosing a version? This decision will be written now. [y/N] ',
|
|
53
|
-
invalid: 'Invalid option. Choose a displayed number, P, R, D, or Q.',
|
|
69
|
+
invalid: 'Invalid option. Choose a displayed number, P, R, D, H, or Q.',
|
|
54
70
|
declined: 'Decision not confirmed; no bytes were changed.',
|
|
55
71
|
promoted: 'Version promoted and decision audited.',
|
|
56
72
|
rejected: 'Conflict closed without promoting a version.',
|
|
@@ -125,13 +141,24 @@ export function renderMemoryConflict(candidate, {
|
|
|
125
141
|
const source = text.sourceKinds[event.source] || text.sourceKinds.unknown;
|
|
126
142
|
lines.push(`[${eventIndex + 1}] ${text.source}: ${source}${recent}`);
|
|
127
143
|
if (event.observed_at) lines.push(` ${text.observed}: ${event.observed_at}`);
|
|
144
|
+
const context = [
|
|
145
|
+
event.session_status === 'active' ? text.activeContext
|
|
146
|
+
: event.session_status ? text.terminalContext : '',
|
|
147
|
+
event.change_slug ? text.changeContext(event.change_slug) : '',
|
|
148
|
+
].filter(Boolean);
|
|
149
|
+
if (context.length) lines.push(` ${context.join(' · ')}`);
|
|
128
150
|
lines.push(` ${event.preview}`);
|
|
129
151
|
});
|
|
152
|
+
if (candidate.classification === 'historical'
|
|
153
|
+
&& candidate.recommended_action === 'reject') {
|
|
154
|
+
lines.push('', text.safeRecommendation);
|
|
155
|
+
}
|
|
130
156
|
if (details) {
|
|
131
157
|
lines.push('', `${text.details}:`, ` candidate: ${candidate.candidate_id}`);
|
|
132
158
|
candidate.events.forEach((event) => lines.push(` event: ${event.event_id}`));
|
|
133
159
|
}
|
|
134
160
|
lines.push('', text.actions);
|
|
161
|
+
if (candidate.classification === 'historical') lines.push(text.historicalAction);
|
|
135
162
|
return `${lines.join('\n')}\n`;
|
|
136
163
|
}
|
|
137
164
|
|
|
@@ -141,6 +168,7 @@ export async function runGuidedMemoryCuration(vault, {
|
|
|
141
168
|
loadCandidates = listMemoryCandidatesForCuration,
|
|
142
169
|
decide = decideMemoryCandidate,
|
|
143
170
|
locale = 'pt-BR',
|
|
171
|
+
omittedHistorical = 0,
|
|
144
172
|
} = {}) {
|
|
145
173
|
if (typeof ask !== 'function' || typeof write !== 'function') {
|
|
146
174
|
throw new TypeError('runGuidedMemoryCuration requer ask e write.');
|
|
@@ -167,6 +195,7 @@ export async function runGuidedMemoryCuration(vault, {
|
|
|
167
195
|
if (first) {
|
|
168
196
|
sessionTotal = loaded.length;
|
|
169
197
|
write(`${text.title}\n${text.intro(loaded.length)}\n`);
|
|
198
|
+
if (omittedHistorical) write(`${text.historicalOmitted(omittedHistorical)}\n`);
|
|
170
199
|
if (loaded.length) write(`${text.categories}\n${groupLines(loaded, id).join('\n')}\n`);
|
|
171
200
|
first = false;
|
|
172
201
|
}
|
|
@@ -207,6 +236,56 @@ export async function runGuidedMemoryCuration(vault, {
|
|
|
207
236
|
}));
|
|
208
237
|
continue;
|
|
209
238
|
}
|
|
239
|
+
if (answer === 'h') {
|
|
240
|
+
const historicalCount = loaded.filter((item) => (
|
|
241
|
+
item.classification === 'historical' && item.recommended_action === 'reject'
|
|
242
|
+
)).length;
|
|
243
|
+
if (!historicalCount) {
|
|
244
|
+
write(`${text.invalid}\n`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const confirmation = await ask(text.confirmHistorical(historicalCount));
|
|
248
|
+
if (!isAffirmative(confirmation)) {
|
|
249
|
+
write(`${text.declined}\n`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
let closed = 0;
|
|
253
|
+
const batchIds = new Set();
|
|
254
|
+
while (true) {
|
|
255
|
+
let refreshed;
|
|
256
|
+
try {
|
|
257
|
+
refreshed = loadCandidates(vault);
|
|
258
|
+
} catch {
|
|
259
|
+
write(`${text.blocked}\n`);
|
|
260
|
+
return { status: 'blocked', decisions, skipped };
|
|
261
|
+
}
|
|
262
|
+
const next = refreshed.find((item) => (
|
|
263
|
+
item.classification === 'historical' && item.recommended_action === 'reject'
|
|
264
|
+
));
|
|
265
|
+
if (!next) break;
|
|
266
|
+
if (batchIds.has(next.candidate_id)) {
|
|
267
|
+
write(`${text.blocked}\n`);
|
|
268
|
+
return { status: 'blocked', decisions, skipped };
|
|
269
|
+
}
|
|
270
|
+
batchIds.add(next.candidate_id);
|
|
271
|
+
let batchResult;
|
|
272
|
+
try {
|
|
273
|
+
batchResult = decide(vault, { action: 'reject', candidateId: next.candidate_id });
|
|
274
|
+
} catch {
|
|
275
|
+
write(`${text.blocked}\n`);
|
|
276
|
+
return { status: 'blocked', decisions, skipped };
|
|
277
|
+
}
|
|
278
|
+
if (batchResult?.status !== 'rejected') {
|
|
279
|
+
write(`${text.blocked}\n`);
|
|
280
|
+
return { status: 'blocked', decisions, skipped };
|
|
281
|
+
}
|
|
282
|
+
decisions += 1;
|
|
283
|
+
closed += 1;
|
|
284
|
+
decidedIds.add(next.candidate_id);
|
|
285
|
+
}
|
|
286
|
+
write(`${text.historicalClosed(closed)}\n`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
210
289
|
|
|
211
290
|
let decision;
|
|
212
291
|
let confirmation;
|
|
@@ -260,11 +339,18 @@ function usageError(message) {
|
|
|
260
339
|
export function parseMemoryCurateArgs(argv) {
|
|
261
340
|
let vault = '';
|
|
262
341
|
let seenVault = false;
|
|
342
|
+
let includeHistorical = false;
|
|
263
343
|
for (let index = 0; index < argv.length; index += 1) {
|
|
264
344
|
const token = argv[index];
|
|
265
345
|
if (!token.startsWith('--')) throw usageError(`argumento posicional inesperado: ${token}.`);
|
|
266
346
|
const equalAt = token.indexOf('=');
|
|
267
347
|
const name = equalAt >= 0 ? token.slice(0, equalAt) : token;
|
|
348
|
+
if (name === '--all') {
|
|
349
|
+
if (equalAt >= 0) throw usageError('--all não aceita valor.');
|
|
350
|
+
if (includeHistorical) throw usageError('--all duplicado.');
|
|
351
|
+
includeHistorical = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
268
354
|
if (name !== '--vault') throw usageError(`opção desconhecida: ${name}.`);
|
|
269
355
|
if (seenVault) throw usageError('--vault duplicado.');
|
|
270
356
|
const value = equalAt >= 0 ? token.slice(equalAt + 1) : argv[index + 1];
|
|
@@ -275,7 +361,7 @@ export function parseMemoryCurateArgs(argv) {
|
|
|
275
361
|
seenVault = true;
|
|
276
362
|
if (equalAt < 0) index += 1;
|
|
277
363
|
}
|
|
278
|
-
return { vault };
|
|
364
|
+
return { vault, ...(includeHistorical ? { includeHistorical: true } : {}) };
|
|
279
365
|
}
|
|
280
366
|
|
|
281
367
|
export async function runMemoryCurateCli(argv, {
|
|
@@ -313,8 +399,16 @@ export async function runMemoryCurateCli(argv, {
|
|
|
313
399
|
|
|
314
400
|
const rl = createInterface({ input, output });
|
|
315
401
|
try {
|
|
402
|
+
const allCandidates = listMemoryCandidatesForCuration(vault, { includeHistorical: true });
|
|
403
|
+
const omittedHistorical = args.includeHistorical
|
|
404
|
+
? 0
|
|
405
|
+
: allCandidates.filter((item) => item.classification === 'historical').length;
|
|
316
406
|
const result = await runGuidedMemoryCuration(vault, {
|
|
317
407
|
locale: getLocale(vault).id,
|
|
408
|
+
omittedHistorical,
|
|
409
|
+
loadCandidates: (base) => listMemoryCandidatesForCuration(base, {
|
|
410
|
+
includeHistorical: Boolean(args.includeHistorical),
|
|
411
|
+
}),
|
|
318
412
|
ask: (question) => rl.question(question),
|
|
319
413
|
write: (value) => output.write(String(value)),
|
|
320
414
|
});
|
package/src/memory.mjs
CHANGED
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
import { validateCore } from './validate-core.mjs';
|
|
22
22
|
import { checkMemoryBundle } from '../hooks/vault-health.mjs';
|
|
23
23
|
import { scopeForMemoryKey } from '../hooks/memory-scope.mjs';
|
|
24
|
+
import {
|
|
25
|
+
candidateSessionContext,
|
|
26
|
+
classifyMemoryCandidate,
|
|
27
|
+
} from '../packages/vault/src/memory-candidate-policy.mjs';
|
|
24
28
|
|
|
25
29
|
const BRAIN = '.brain';
|
|
26
30
|
const LEDGER = 'MEMORY_EVENTS.jsonl';
|
|
@@ -241,24 +245,17 @@ export function planScopedMemoryMigration(vault) {
|
|
|
241
245
|
const projection = deriveMemoryProjection(vault, ledger.events);
|
|
242
246
|
const ambiguous = new Set(projection.candidates.map((candidate) => candidate.record_key || candidate.memory_key));
|
|
243
247
|
const existingIds = new Set(ledger.events.map((event) => event.event_id));
|
|
248
|
+
const alreadyRescopedSourceIds = new Set(ledger.events.flatMap((event) => [
|
|
249
|
+
event.rescopes_event_id,
|
|
250
|
+
...(Array.isArray(event.rescopes_event_ids) ? event.rescopes_event_ids : []),
|
|
251
|
+
]).filter(Boolean));
|
|
244
252
|
const planned = [];
|
|
253
|
+
const plannedSourceIds = new Set();
|
|
245
254
|
|
|
246
|
-
|
|
247
|
-
if (ambiguous.has(recordKey) || record.source?.scope) continue;
|
|
248
|
-
const source = record.source;
|
|
249
|
-
const sameLegacyKey = ledger.events.filter((event) => (
|
|
250
|
-
!event.scope && event.memory_key === source.memory_key && !event.rescopes_event_id
|
|
251
|
-
));
|
|
252
|
-
const scope = scopeForMemoryKey(source.memory_key, {
|
|
253
|
-
...source,
|
|
254
|
-
projectId: source.project_id,
|
|
255
|
-
workSessionId: source.work_session_id,
|
|
256
|
-
branch: source.value?.branch || source.value?.branch_name,
|
|
257
|
-
worktreeId: source.value?.worktree_id,
|
|
258
|
-
repositoryId: source.value?.repository_id,
|
|
259
|
-
});
|
|
255
|
+
const appendRescope = (source, scope, rescopesEventIds = [source.event_id]) => {
|
|
260
256
|
const eventId = rescopeEventId(source.event_id, scope);
|
|
261
|
-
if (existingIds.has(eventId))
|
|
257
|
+
if (existingIds.has(eventId) || alreadyRescopedSourceIds.has(source.event_id)
|
|
258
|
+
|| plannedSourceIds.has(source.event_id)) return false;
|
|
262
259
|
planned.push({
|
|
263
260
|
v: 1,
|
|
264
261
|
event_id: eventId,
|
|
@@ -266,7 +263,7 @@ export function planScopedMemoryMigration(vault) {
|
|
|
266
263
|
memory_key: source.memory_key,
|
|
267
264
|
scope,
|
|
268
265
|
operation: 'assert',
|
|
269
|
-
value:
|
|
266
|
+
value: source.value,
|
|
270
267
|
authority: source.authority,
|
|
271
268
|
canonical_session_id: source.canonical_session_id || 'memory-rescope',
|
|
272
269
|
activation_id: source.activation_id || 'memory-rescope',
|
|
@@ -275,9 +272,58 @@ export function planScopedMemoryMigration(vault) {
|
|
|
275
272
|
source_turn_id: source.source_turn_id || 'memory-rescope',
|
|
276
273
|
observed_at: source.observed_at || new Date(0).toISOString(),
|
|
277
274
|
evidence: [...new Set([...(source.evidence || []), `memory-rescope:${source.event_id}`])],
|
|
275
|
+
...(source.work_session_id ? { work_session_id: source.work_session_id } : {}),
|
|
278
276
|
rescopes_event_id: source.event_id,
|
|
279
|
-
rescopes_event_ids:
|
|
277
|
+
rescopes_event_ids: [...new Set(rescopesEventIds)].sort(),
|
|
280
278
|
});
|
|
279
|
+
plannedSourceIds.add(source.event_id);
|
|
280
|
+
return true;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
for (const [recordKey, record] of Object.entries(projection.records)) {
|
|
284
|
+
if (ambiguous.has(recordKey) || record.source?.scope) continue;
|
|
285
|
+
const source = record.source;
|
|
286
|
+
const sameLegacyKey = ledger.events.filter((event) => (
|
|
287
|
+
!event.scope && event.memory_key === source.memory_key && !event.rescopes_event_id
|
|
288
|
+
));
|
|
289
|
+
const scope = scopeForMemoryKey(source.memory_key, {
|
|
290
|
+
...source,
|
|
291
|
+
projectId: source.project_id,
|
|
292
|
+
workSessionId: source.work_session_id,
|
|
293
|
+
branch: source.value?.branch || source.value?.branch_name,
|
|
294
|
+
worktreeId: source.value?.worktree_id,
|
|
295
|
+
repositoryId: source.value?.repository_id,
|
|
296
|
+
});
|
|
297
|
+
const eventId = rescopeEventId(source.event_id, scope);
|
|
298
|
+
if (existingIds.has(eventId)) continue;
|
|
299
|
+
appendRescope(
|
|
300
|
+
{ ...source, value: record.value },
|
|
301
|
+
scope,
|
|
302
|
+
sameLegacyKey.map((event) => event.event_id),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let rescopableConflicts = 0;
|
|
307
|
+
for (const candidate of projection.candidates) {
|
|
308
|
+
if (candidate.reason !== 'conflict' || candidate.memory_key !== 'handoff.latest') continue;
|
|
309
|
+
const sources = Array.isArray(candidate.events) ? candidate.events : [];
|
|
310
|
+
if (!sources.length || sources.some((source) => (
|
|
311
|
+
source.scope || (!source.work_session_id && !source.canonical_session_id)
|
|
312
|
+
))) continue;
|
|
313
|
+
|
|
314
|
+
const scopedSources = sources.map((source) => ({
|
|
315
|
+
source,
|
|
316
|
+
scope: scopeForMemoryKey(source.memory_key, {
|
|
317
|
+
...source,
|
|
318
|
+
projectId: source.project_id,
|
|
319
|
+
workSessionId: source.work_session_id,
|
|
320
|
+
}),
|
|
321
|
+
}));
|
|
322
|
+
if (scopedSources.some(({ scope }) => scope.type !== 'work_session'
|
|
323
|
+
|| scope.id.startsWith('legacy:'))) continue;
|
|
324
|
+
|
|
325
|
+
rescopableConflicts += 1;
|
|
326
|
+
for (const { source, scope } of scopedSources) appendRescope(source, scope);
|
|
281
327
|
}
|
|
282
328
|
|
|
283
329
|
return {
|
|
@@ -285,6 +331,7 @@ export function planScopedMemoryMigration(vault) {
|
|
|
285
331
|
ledger_events: ledger.events.length,
|
|
286
332
|
planned: planned.length,
|
|
287
333
|
ambiguous: projection.candidates.length,
|
|
334
|
+
rescopable_conflicts: rescopableConflicts,
|
|
288
335
|
events: planned,
|
|
289
336
|
};
|
|
290
337
|
}
|
|
@@ -298,13 +345,18 @@ export function rescopeMemoryEvents(vault, { apply = false } = {}) {
|
|
|
298
345
|
ledger_events: plan.ledger_events,
|
|
299
346
|
planned: plan.planned,
|
|
300
347
|
ambiguous: plan.ambiguous,
|
|
348
|
+
rescopable_conflicts: plan.rescopable_conflicts,
|
|
301
349
|
scopes: plan.events.map((event) => ({
|
|
302
350
|
event_id: event.event_id, memory_key: event.memory_key, scope: event.scope,
|
|
303
351
|
})),
|
|
304
352
|
};
|
|
305
353
|
}
|
|
306
354
|
if (!plan.events.length) {
|
|
307
|
-
return {
|
|
355
|
+
return {
|
|
356
|
+
status: 'unchanged', migrated: 0, ambiguous: plan.ambiguous,
|
|
357
|
+
rescopable_conflicts: plan.rescopable_conflicts,
|
|
358
|
+
remaining_ambiguous: plan.ambiguous,
|
|
359
|
+
};
|
|
308
360
|
}
|
|
309
361
|
for (const event of plan.events) enqueueMemoryEvent(vault, event);
|
|
310
362
|
const projection = projectMemoryOutbox(vault);
|
|
@@ -312,6 +364,10 @@ export function rescopeMemoryEvents(vault, { apply = false } = {}) {
|
|
|
312
364
|
status: projection.status === 'projected' ? 'migrated' : projection.status,
|
|
313
365
|
migrated: plan.events.length,
|
|
314
366
|
ambiguous: plan.ambiguous,
|
|
367
|
+
rescopable_conflicts: plan.rescopable_conflicts,
|
|
368
|
+
remaining_ambiguous: Number.isInteger(projection.candidates)
|
|
369
|
+
? projection.candidates
|
|
370
|
+
: plan.ambiguous,
|
|
315
371
|
checkpoint: projection.checkpoint || null,
|
|
316
372
|
};
|
|
317
373
|
}
|
|
@@ -377,7 +433,7 @@ function curationPreview(value) {
|
|
|
377
433
|
: `${visible.slice(0, CURATION_PREVIEW_CHARS - 1).trimEnd()}…`;
|
|
378
434
|
}
|
|
379
435
|
|
|
380
|
-
function sanitizedCurationEvent(candidate, eventId, candidateIndex) {
|
|
436
|
+
function sanitizedCurationEvent(candidate, eventId, candidateIndex, registry) {
|
|
381
437
|
const events = Array.isArray(candidate.events) ? candidate.events : [];
|
|
382
438
|
const event = events.find((item) => item?.event_id === eventId);
|
|
383
439
|
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
@@ -397,15 +453,24 @@ function sanitizedCurationEvent(candidate, eventId, candidateIndex) {
|
|
|
397
453
|
observed_at: sanitizeMemoryText(event.observed_at || event.effective_at || ''),
|
|
398
454
|
source: sanitizeMemoryText(source),
|
|
399
455
|
preview: curationPreview(event.value),
|
|
456
|
+
...Object.fromEntries(Object.entries(candidateSessionContext(event, registry))
|
|
457
|
+
.map(([key, value]) => [key, sanitizeMemoryText(value)])),
|
|
400
458
|
};
|
|
401
459
|
}
|
|
402
460
|
|
|
403
|
-
export function listMemoryCandidatesForCuration(vault) {
|
|
461
|
+
export function listMemoryCandidatesForCuration(vault, { includeHistorical = false } = {}) {
|
|
462
|
+
const registry = readSessionRegistry(vault);
|
|
404
463
|
return readCandidates(vault)
|
|
405
|
-
.map((candidate, index) => ({
|
|
464
|
+
.map((candidate, index) => ({
|
|
465
|
+
candidate,
|
|
466
|
+
index,
|
|
467
|
+
safe: sanitizedCandidate(candidate, index),
|
|
468
|
+
policy: classifyMemoryCandidate(candidate, registry),
|
|
469
|
+
}))
|
|
406
470
|
.filter(({ safe }) => safe.reason === 'conflict'
|
|
407
471
|
&& !TERMINAL_CANDIDATE_STATUSES.has(safe.status))
|
|
408
|
-
.
|
|
472
|
+
.filter(({ policy }) => includeHistorical || policy.classification !== 'historical')
|
|
473
|
+
.map(({ candidate, index, safe, policy }) => {
|
|
409
474
|
if (!safe.event_ids.length) {
|
|
410
475
|
throw new Error(
|
|
411
476
|
`MEMORY_CANDIDATES.jsonl: candidate ${index + 1} sem eventos elegíveis.`,
|
|
@@ -416,8 +481,12 @@ export function listMemoryCandidatesForCuration(vault) {
|
|
|
416
481
|
reason: safe.reason,
|
|
417
482
|
status: safe.status,
|
|
418
483
|
memory_key: safe.memory_key,
|
|
484
|
+
classification: policy.classification,
|
|
485
|
+
...(policy.recommended_action ? { recommended_action: policy.recommended_action } : {}),
|
|
419
486
|
...(safe.scope ? { scope: safe.scope } : {}),
|
|
420
|
-
events: safe.event_ids.map(
|
|
487
|
+
events: safe.event_ids.map(
|
|
488
|
+
(eventId) => sanitizedCurationEvent(candidate, eventId, index, registry),
|
|
489
|
+
),
|
|
421
490
|
};
|
|
422
491
|
})
|
|
423
492
|
.sort((left, right) => lexicalCompare(left.memory_key, right.memory_key)
|
|
@@ -26,6 +26,7 @@ export const SQL_EVENT_BATCH_SIZE = 64;
|
|
|
26
26
|
export const SQL_EVENT_BATCH_BYTES = 8 * 1024 * 1024;
|
|
27
27
|
const REQUEST_TIMEOUT_MS = 15000;
|
|
28
28
|
const MAX_REQUEST_TIMEOUT_MS = 120000;
|
|
29
|
+
export const SQL_LEASE_STALE_MS = MAX_REQUEST_TIMEOUT_MS + 30000;
|
|
29
30
|
const REQUEST_TIMEOUT_BYTES_STEP = 1024 * 1024;
|
|
30
31
|
const CAPTURE_LEVELS = new Set(['metadata', 'messages', 'full-transcript']);
|
|
31
32
|
|
|
@@ -262,7 +263,7 @@ function dedupeEvents(events) {
|
|
|
262
263
|
});
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {}, captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
|
|
266
|
+
export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {}, captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata', forceFull = false } = {}) {
|
|
266
267
|
if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
|
|
267
268
|
const occurredAt = isoNow(now);
|
|
268
269
|
const resolvedCaptureLevel = normalizeObserverCaptureLevel(captureLevel);
|
|
@@ -289,7 +290,7 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
|
|
|
289
290
|
nextState.files[file.logicalPath] = { content_hash: contentHash, revision: revision || 1 };
|
|
290
291
|
const fm = parseFrontmatter(content);
|
|
291
292
|
if (fm.type === 'session') sessionContexts.push({ file, content, fm, contentHash, revision: revision || 1, sessionId: sessionIdentity.get(file.logicalPath) });
|
|
292
|
-
if (previous?.content_hash === contentHash) continue;
|
|
293
|
+
if (!forceFull && previous?.content_hash === contentHash) continue;
|
|
293
294
|
changed += 1;
|
|
294
295
|
events.push(documentEvent({ projectId, logicalPath: file.logicalPath, content, metadata: fm, revision: revision || 1, occurredAt }));
|
|
295
296
|
if (fm.type === 'session') {
|
|
@@ -309,7 +310,7 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
|
|
|
309
310
|
const content = readFileSync(source.path, 'utf8');
|
|
310
311
|
const fingerprint = hash(content);
|
|
311
312
|
const previousTranscript = state.transcripts?.[source.transcriptId];
|
|
312
|
-
if (previousTranscript?.content_hash === fingerprint && previousTranscript?.coverage === resolvedCaptureLevel) continue;
|
|
313
|
+
if (!forceFull && previousTranscript?.content_hash === fingerprint && previousTranscript?.coverage === resolvedCaptureLevel) continue;
|
|
313
314
|
const complete = completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider, model, source, now: occurredAt, captureLevel: resolvedCaptureLevel });
|
|
314
315
|
nextState.transcripts[source.transcriptId] = { content_hash: complete.fingerprint, coverage: resolvedCaptureLevel };
|
|
315
316
|
const summaryId = complete.transcriptId;
|
|
@@ -467,11 +468,20 @@ function queueOutbox(vaultBase, batch) {
|
|
|
467
468
|
|
|
468
469
|
function eventCoalesceKey(event) {
|
|
469
470
|
const payload = event.payload || {};
|
|
471
|
+
const entityFields = {
|
|
472
|
+
'document.upsert': ['logical_path'],
|
|
473
|
+
'document.delete': ['logical_path'],
|
|
474
|
+
'session.upsert': ['session_id'],
|
|
475
|
+
'agent.upsert': ['agent_id'],
|
|
476
|
+
'usage.rollup': ['rollup_key'],
|
|
477
|
+
llm_call: ['call_id'],
|
|
478
|
+
'transcript.upsert': ['transcript_id'],
|
|
479
|
+
}[event.kind] || [];
|
|
480
|
+
const entityId = entityFields.map((field) => payload[field]).find(Boolean) || event.event_id;
|
|
470
481
|
return [
|
|
471
482
|
event.project_id,
|
|
472
483
|
event.kind,
|
|
473
|
-
|
|
474
|
-
|| payload.call_id || payload.transcript_id || event.event_id,
|
|
484
|
+
entityId,
|
|
475
485
|
].join('\u001f');
|
|
476
486
|
}
|
|
477
487
|
|
|
@@ -480,15 +490,17 @@ function waitSync(milliseconds) { Atomics.wait(WAIT_ARRAY, 0, 0, milliseconds);
|
|
|
480
490
|
|
|
481
491
|
function acquireBatchFileLease(path, waitMs = 0) {
|
|
482
492
|
const lock = `${path}.lock`;
|
|
493
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
483
494
|
const deadline = Date.now() + Math.max(0, waitMs);
|
|
484
495
|
do {
|
|
485
496
|
try {
|
|
486
497
|
mkdirSync(lock);
|
|
487
|
-
|
|
498
|
+
atomicJson(join(lock, 'owner.json'), { token, pid: process.pid, acquired_at: new Date().toISOString() });
|
|
499
|
+
return { path: lock, token };
|
|
488
500
|
} catch (error) {
|
|
489
501
|
if (error?.code !== 'EEXIST') throw error;
|
|
490
502
|
try {
|
|
491
|
-
if (Date.now() - statSync(lock).mtimeMs >
|
|
503
|
+
if (Date.now() - statSync(lock).mtimeMs > SQL_LEASE_STALE_MS) {
|
|
492
504
|
const stale = `${lock}.stale-${process.pid}-${Date.now()}`;
|
|
493
505
|
renameSync(lock, stale);
|
|
494
506
|
rmSync(stale, { recursive: true, force: true });
|
|
@@ -502,8 +514,10 @@ function acquireBatchFileLease(path, waitMs = 0) {
|
|
|
502
514
|
return null;
|
|
503
515
|
}
|
|
504
516
|
|
|
505
|
-
function releaseBatchFileLease(
|
|
506
|
-
if (
|
|
517
|
+
function releaseBatchFileLease(lease) {
|
|
518
|
+
if (!lease) return;
|
|
519
|
+
const owner = readJson(join(lease.path, 'owner.json'), {});
|
|
520
|
+
if (owner.token === lease.token) rmSync(lease.path, { recursive: true, force: true });
|
|
507
521
|
}
|
|
508
522
|
|
|
509
523
|
/** Queue a precise writer batch and replace older pending state for the same logical scope. */
|
|
@@ -625,7 +639,7 @@ function acquirePublisherLease(vaultBase, currentTime = Date.now()) {
|
|
|
625
639
|
if (error?.code !== 'EEXIST') throw error;
|
|
626
640
|
let age = 0;
|
|
627
641
|
try { age = currentTime - statSync(path).mtimeMs; } catch { return null; }
|
|
628
|
-
if (age <=
|
|
642
|
+
if (age <= SQL_LEASE_STALE_MS) return null;
|
|
629
643
|
const stale = `${path}.stale-${token}`;
|
|
630
644
|
try {
|
|
631
645
|
renameSync(path, stale);
|
|
@@ -770,14 +784,14 @@ export async function publishObserverSqlIncremental({
|
|
|
770
784
|
};
|
|
771
785
|
}
|
|
772
786
|
|
|
773
|
-
export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata' } = {}) {
|
|
787
|
+
export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata', forceFull = false } = {}) {
|
|
774
788
|
if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
|
|
775
789
|
const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token });
|
|
776
790
|
const state = readState(vaultBase);
|
|
777
|
-
const remoteDocuments = Object.keys(state.files || {}).length === 0
|
|
791
|
+
const remoteDocuments = forceFull || Object.keys(state.files || {}).length === 0
|
|
778
792
|
? await readRemoteDocuments({ url, projectId, fetchImpl, token })
|
|
779
793
|
: {};
|
|
780
|
-
const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel });
|
|
794
|
+
const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel, forceFull });
|
|
781
795
|
if (!batch.events.length) {
|
|
782
796
|
atomicJson(statePath(vaultBase), batch.nextState);
|
|
783
797
|
return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay };
|
package/src/observer.mjs
CHANGED
|
@@ -147,6 +147,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
147
147
|
projectId: snapshot.project_id,
|
|
148
148
|
url,
|
|
149
149
|
token,
|
|
150
|
+
forceFull: true,
|
|
150
151
|
captureLevel: optionValue(argv, '--capture-level') || process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
|
|
151
152
|
});
|
|
152
153
|
const snapshotResponse = await fetch(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(snapshot.project_id)}/snapshot`, {
|
|
@@ -209,6 +210,7 @@ export async function runObserver(argv = [], { write = (chunk) => process.stdout
|
|
|
209
210
|
projectId: snapshot.project_id,
|
|
210
211
|
url,
|
|
211
212
|
token,
|
|
213
|
+
forceFull: true,
|
|
212
214
|
captureLevel: optionValue(argv, '--capture-level') || process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
|
|
213
215
|
});
|
|
214
216
|
const snapshotResponse = await fetch(`${String(url).replace(/\/$/, '')}/v1/projects/${encodeURIComponent(snapshot.project_id)}/snapshot`, {
|