wendkeep 0.74.0 → 0.75.0

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,41 @@ 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.0] — 2026-08-20
8
+
9
+ ### Added
10
+
11
+ - **Identidades SQL escopadas por projeto.** Sessões, agentes, rollups, chamadas e transcripts
12
+ mantêm chaves internas derivadas de `project_id` + identificador externo, constraints compostas
13
+ e foreign keys que carregam o projeto. Colisões deliberadas entre projetos ficam isoladas.
14
+ - **Migrações comprováveis.** Cada arquivo de schema recebe checksum; uma migração estrutural de
15
+ base existente cria backup consistente antes da transação e pode ser retomada com segurança.
16
+ - **Reconciliação explícita.** `observer reconcile` reserva a varredura integral para bootstrap,
17
+ reparo e comprovação de paridade por hash, localmente ou contra o serviço.
18
+ - **Outbox observável.** `doctor` informa lotes, eventos, bytes e idade; uma lease admite apenas um
19
+ publisher e batches do mesmo escopo são coalescidos.
20
+
21
+ ### Changed
22
+
23
+ - **Publicação incremental nos hooks.** `SessionStart` apenas drena, `Stop` lê uma sessão e
24
+ `SubagentStop` somente o transcript afetado. `note new` e `change archive` enfileiram diretamente
25
+ os documentos que escreveram. O file store antigo deixa de receber publicação viva e permanece
26
+ apenas como fonte de migração.
27
+ - **Schema SQL 5.** O mesmo `session_id`, `agent_id`, `call_id`, `transcript_id` ou `rollup_key`
28
+ pode existir em projetos diferentes sem disputar uma linha global.
29
+
30
+ ### Fixed
31
+
32
+ - **Ingest atômico por evento.** Cada evento usa `SAVEPOINT ingest_one`; qualquer falha reverte o
33
+ registro de ingestão e todas as projeções antes de o batch continuar. Retry permanece idempotente.
34
+ - **Chamadas incrementais estáveis.** Turnos concluídos usam identidade causal estável, evitando
35
+ duplicação quando um transcript cresce após a primeira publicação.
36
+
37
+ ### Performance
38
+
39
+ - O caminho de hook não varre mais o Vault. A fixture de publicação incremental comprova
40
+ `p95 < 200 ms`, uma sessão lida por `Stop` e zero documentos lidos por `SessionStart`.
41
+
7
42
  ## [0.74.0] — 2026-08-20
8
43
 
9
44
  ### 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; search returns the matching passage with provenance and falls back to the local lexical ranker when FTS5 is unavailable. |
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, while `observer reconcile` reserves full scans for explicit recovery. |
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
@@ -115,7 +115,7 @@ npx wendkeep init
115
115
 
116
116
  1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
117
117
  2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
118
- 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. `observer-publish` keeps the sanitized index projection and also sends the local SQL authority with documents, consumption, and transcripts; it does not replace the local lifecycle. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
118
+ 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
119
119
  4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
120
120
  5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
121
121
  - **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
@@ -240,7 +240,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
240
240
  | **Notes and knowledge** | BUG/APR/ADR, repairs, renumbering, lessons, and dashboard | [Notes and knowledge](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/notes-and-knowledge.md) |
241
241
  | **Costs and observability** | safe dry-run, tri-state, aggregation, trends, and historical rebuild | [Costs and observability](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/costs-and-observability.md) |
242
242
  | **Maintenance and diagnostics** | doctor, frontier/manifest freshness, drift, version, and help | [Maintenance and diagnostics](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/maintenance-and-diagnostics.md) |
243
- | **Local Observer** | `observer serve`, registration, snapshots, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
243
+ | **Local Observer** | `observer serve`, registration, incremental publication, `reconcile`, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
244
244
 
245
245
  Operations that deserve step-by-step guidance: [verify and exits 0/1/2](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/verify.md),
246
246
  [legacy-memory migration](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory-migration.md), and
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; search returns the matching passage with provenance and falls back to the local lexical ranker when FTS5 is unavailable. |
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, while `observer reconcile` reserves full scans for explicit recovery. |
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
@@ -115,7 +115,7 @@ npx wendkeep init
115
115
 
116
116
  1. Create the vault folder taxonomy and a templated `README.md` (default vault: `<project>/.<project-name>-vault`, e.g. `.MyApp-vault`; override with `--vault`).
117
117
  2. Write a provider-neutral **`.wendkeep.json`** binding at the project root and a matching `.brain/PROJECT.json` marker in the vault, then merge the session hooks into **`.claude/settings.json`**. The binding is provider-neutral by design: any agent resolves the same vault from its session `cwd`, with no machine-global environment variable. Older registrations already in `.claude/settings.json` are adopted automatically.
118
- 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. `observer-publish` keeps the sanitized index projection and also sends the local SQL authority with documents, consumption, and transcripts; it does not replace the local lifecycle. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
118
+ 3. Wire the Codex hooks in **`.codex/hooks.json`** — twelve compatible entries: `brain-inject` + `session-start` + `observer-publish` on `SessionStart`, `session-ensure` + `evidence-context` + `change-context` on `UserPromptSubmit`, `session-stop` + `observer-publish` + `change-nag` on `Stop`, `subagent-stop` + `observer-publish` on `SubagentStop`, and `change-guard` on `PreToolUse` for `Bash`, `exec_command`, `apply_patch`, and mutable MCP tools, always in the `npx wendkeep hook <name>` form. For the Observer, `SessionStart` only drains the outbox, `Stop` enqueues the changed session, and `SubagentStop` enqueues only the affected transcript; full scanning is explicit through `observer reconcile`. The guard accepts object, raw-string, and argv Codex payloads; before a mutation it compares the session with the project, Git root, remote, branch, and worktree, denying missing or divergent targets. The other four stay out because Codex offers no equivalent payload, tool, or event: `change-warn` (no reliable `tool_input.file_path`), `plan-capture` (no `ExitPlanMode`), `decision-capture` (`AskUserQuestion` is Claude-only), and `task-log` (`TaskCompleted` is not in Codex's event enum). Codex scope blocks use `permissionDecision: "deny"`; `ask` is never emitted in `PreToolUse`. The merge remains non-destructive, preserves third-party hooks, and migrates legacy `timeout` to `timeoutSec`. **Codex enumerates every hook as untrusted and runs none until you approve the “Hooks need review” prompt at startup — `init` cannot pre-approve them**.
119
119
  4. Add the **`wendkeep-vault`** MCP server to `.mcp.json` so the agent can read/write the vault. Skip with `--no-mcp` — e.g. when the agent already has a vault MCP. (`--no-mcp` skips *only wendkeep's own* MCP; companion MCPs still follow `--companions`.)
120
120
  5. Offer to pin **companion** plugins/MCP (multi-choice; **none** pre-checked — wendkeep is a neutral harness and presumes no third-party plugin). Each is wired the most agent-agnostic way it supports:
121
121
  - **`context-mode`** — context optimizer + FTS5 memory, wired as a Claude Code plugin. It ships its own MCP server, so wendkeep deliberately adds no `.mcp.json` entry (registering both cold-started two servers at once). On non-Claude agents, add the MCP by hand: `npx -y context-mode`.
@@ -240,7 +240,7 @@ The README is the map; the guides provide syntax, options, exit codes, examples,
240
240
  | **Notes and knowledge** | BUG/APR/ADR, repairs, renumbering, lessons, and dashboard | [Notes and knowledge](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/notes-and-knowledge.md) |
241
241
  | **Costs and observability** | safe dry-run, tri-state, aggregation, trends, and historical rebuild | [Costs and observability](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/costs-and-observability.md) |
242
242
  | **Maintenance and diagnostics** | doctor, frontier/manifest freshness, drift, version, and help | [Maintenance and diagnostics](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/maintenance-and-diagnostics.md) |
243
- | **Local Observer** | `observer serve`, registration, snapshots, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
243
+ | **Local Observer** | `observer serve`, registration, incremental publication, `reconcile`, outbox, and multi-project index | [Local Observer](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/observer.md) |
244
244
 
245
245
  Operations that deserve step-by-step guidance: [verify and exits 0/1/2](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/verify.md),
246
246
  [legacy-memory migration](https://github.com/rogersialves/wendkeep/blob/main/docs/en/commands/memory-migration.md), and
@@ -34,6 +34,7 @@ loopback reads remain open, while every mutation requires a Bearer token.
34
34
  npx wendkeep observer status --data-dir <directory> --json
35
35
  npx wendkeep observer register --project <project> --vault <vault> --data-dir <directory>
36
36
  npx wendkeep observer publish --project <project> --vault <vault> --data-dir <directory>
37
+ npx wendkeep observer reconcile --project <project> --vault <vault> --data-dir <directory> [--url http://127.0.0.1:8787]
37
38
  npx wendkeep observer memory import --project <project> --vault <vault> --url http://127.0.0.1:8787 --token <token> --json
38
39
  npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory> --token <token>
39
40
  ```
@@ -42,7 +43,7 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <directory>
42
43
 
43
44
  - `--data-dir` selects the local event and index directory; the default is
44
45
  `WENDKEEP_OBSERVER_DATA_DIR` or `~/.wendkeep-observer`.
45
- - `--project` and `--vault` identify a project for `register`, `publish`, and `memory import`.
46
+ - `--project` and `--vault` identify a project for `register`, `publish`, `reconcile`, and `memory import`.
46
47
  - `--host` accepts only `127.0.0.1`, `localhost`, or `::1`; other hosts are rejected before
47
48
  listening.
48
49
  - `--token` or `WENDKEEP_OBSERVER_TOKEN` authenticates mutations; `--allow-non-loopback` fails without one.
@@ -84,8 +85,8 @@ If the browser shows the shell but the list fails, check the service health at
84
85
 
85
86
  ## Expected result
86
87
 
87
- `register` stores `project_id`, name, version, and registration time. `publish` reads the local
88
- vault, produces the snapshot, and sends idempotent events to SQLite containing the complete content
88
+ `register` stores `project_id`, name, version, and registration time. `publish` and `reconcile`
89
+ perform an explicit full-vault scan and send idempotent events to SQLite containing the complete content
89
90
  of sessions, decisions, bugs, learnings, specs, changes, CORE, DIGEST, SHARED_MEMORY, brain state,
90
91
  agent sessions, cost rollups, and calls. Messages and transcripts are only sent by capture levels
91
92
  that explicitly enable them. The container stores everything in
@@ -96,16 +97,26 @@ cost/token total recorded in frontmatter is preserved through an explicit reconc
96
97
  the detailed ledger does not add up; that row does not invent calls. Historical sessions sharing
97
98
  one `session_id` receive a canonical per-file identity so one rollup cannot overwrite the other.
98
99
 
99
- In schema 4, each ingested document is also projected into chunks carrying path, heading,
100
+ In schema 5, sessions, agents, rollups, calls, and transcripts use internal identities derived from
101
+ `project_id` plus the external identifier, with project-scoped constraints and foreign keys. The
102
+ same `session_id`, `agent_id`, `call_id`, or `rollup_key` can exist in separate projects without a
103
+ collision. Every event runs inside its own savepoint; an intermediate failure rolls back the ingest
104
+ row and all projections before the batch continues. Migrations are checksummed and an existing
105
+ database receives a consistent backup before every structural migration.
106
+
107
+ Each ingested document is also projected into chunks carrying path, heading,
100
108
  authority, time, and validity. The Observer feature-probes FTS5 and uses the index when the
101
109
  extension is available; otherwise it preserves the same semantics through a lexical fallback.
102
110
  Search returns the passage containing the match and its provenance rather than only the beginning
103
111
  of the document.
104
112
 
105
113
  `init` projects `observer-publish` into `SessionStart`, `Stop`, and `SubagentStop` after the primary
106
- hooks. When the server is unavailable, it writes snapshots to `.brain/observer-outbox/` and SQL
107
- events to `.brain/observer-sql-outbox/` without blocking the session; a later run retries the
108
- batches. SQL batches use gzip so complete transcripts larger than 64 MB as plain JSON remain within
114
+ hooks. `SessionStart` only drains the queue; `Stop` reads the changed session alone, and
115
+ `SubagentStop` reads only the affected subagent transcript. `note new` and `change archive` enqueue
116
+ the documents they wrote. Events are coalesced by scope and one lease admits a single publisher.
117
+ When the server is unavailable, `.brain/observer-sql-outbox/` remains durable without blocking the
118
+ session; full-vault scanning is reserved for `observer reconcile`. `doctor` reports queue batches,
119
+ events, bytes, and age. SQL batches use gzip so complete transcripts larger than 64 MB as plain JSON remain within
109
120
  the transport limit; the Observer decompresses and validates the body before ingesting it. The
110
121
  outbox is temporary transport, not authority.
111
122
 
@@ -113,12 +124,12 @@ outbox is temporary transport, not authority.
113
124
 
114
125
  - `project_not_registered`: run `observer register` before publishing.
115
126
  - `host loopback`: replace `0.0.0.0` or a LAN address with `127.0.0.1`.
116
- - Pending outbox: the service was unavailable; preserve `.brain/observer-outbox/` and
117
- `.brain/observer-sql-outbox/`, then rerun the publisher. Also ignore
118
- `.brain/observer-sql-state.json` and `.brain/observer-sql-outbox/` in a versioned vault. Do not delete events manually.
127
+ - Pending outbox: the service was unavailable; preserve `.brain/observer-sql-outbox/` and let a
128
+ later hook drain it or run `observer reconcile`. Also ignore
129
+ `.brain/observer-sql-state.json`, `.brain/observer-sql-outbox/`, and `.brain/observer-sql-publisher.lock` in a versioned vault. Do not delete events manually.
119
130
  - `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: run the Observer on Node.js 22.13 or newer.
120
131
  - If memory or usage is incomplete, check the Sync screen, preserve the outbox, and run
121
- `observer memory import` to rebuild the load from the vault.
132
+ `observer reconcile` to rebuild the load and prove hash parity.
122
133
 
123
134
  ## Next steps
124
135
 
@@ -34,6 +34,7 @@ continuam compatíveis com Node.js 18 ou mais recente. Registre explicitamente c
34
34
  npx wendkeep observer status --data-dir <diretório> --json
35
35
  npx wendkeep observer register --project <projeto> --vault <vault> --data-dir <diretório>
36
36
  npx wendkeep observer publish --project <projeto> --vault <vault> --data-dir <diretório>
37
+ npx wendkeep observer reconcile --project <projeto> --vault <vault> --data-dir <diretório> [--url http://127.0.0.1:8787]
37
38
  npx wendkeep observer memory import --project <projeto> --vault <vault> --url http://127.0.0.1:8787 --token <token> --json
38
39
  npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório> --token <token>
39
40
  ```
@@ -42,7 +43,7 @@ npx wendkeep observer serve --host 127.0.0.1 --port 8787 --data-dir <diretório>
42
43
 
43
44
  - `--data-dir` escolhe o diretório local de eventos e índice; o padrão é
44
45
  `WENDKEEP_OBSERVER_DATA_DIR` ou `~/.wendkeep-observer`.
45
- - `--project` e `--vault` identificam o projeto nos comandos `register`, `publish` e `memory import`.
46
+ - `--project` e `--vault` identificam o projeto nos comandos `register`, `publish`, `reconcile` e `memory import`.
46
47
  - `--host` aceita somente `127.0.0.1`, `localhost` ou `::1`; outros hosts são recusados antes do
47
48
  listen.
48
49
  - `--token` ou `WENDKEEP_OBSERVER_TOKEN` autentica mutações; `--allow-non-loopback` falha sem token.
@@ -85,8 +86,8 @@ Se o navegador mostrar a tela mas a lista falhar, confirme a saúde em
85
86
 
86
87
  ## Resultado esperado
87
88
 
88
- `register` grava `project_id`, nome, versão e data de registro. `publish` o vault local,
89
- produz o snapshot e envia eventos idempotentes para o SQLite com o conteúdo integral das sessões,
89
+ `register` grava `project_id`, nome, versão e data de registro. `publish` e `reconcile` fazem a
90
+ varredura integral explícita do vault e enviam eventos idempotentes para o SQLite com o conteúdo integral das sessões,
90
91
  decisões, bugs, aprendizados, specs, changes, CORE, DIGEST, SHARED_MEMORY, estado do brain,
91
92
  sessões de agentes, rollups de custo e chamadas. Mensagens e transcripts só são enviados nos
92
93
  níveis de captura que os habilitam. O container grava tudo em
@@ -98,15 +99,25 @@ reconciliação quando o ledger detalhado não fecha com ele; essa linha não in
98
99
  Sessões históricas com o mesmo `session_id` recebem uma identidade canônica por arquivo para
99
100
  evitar que um rollup sobrescreva o outro.
100
101
 
101
- No schema 4, cada documento ingerido também é projetado em chunks com caminho, heading,
102
+ No schema 5, sessões, agentes, rollups, chamadas e transcripts usam identidades internas derivadas
103
+ de `project_id` + identificador externo, com constraints e foreign keys escopadas. O mesmo
104
+ `session_id`, `agent_id`, `call_id` ou `rollup_key` pode existir em projetos diferentes sem colisão.
105
+ Cada evento é aplicado em um savepoint próprio; falha intermediária reverte ingest e todas as
106
+ projeções antes de o batch continuar. Migrações têm checksum e toda migração estrutural de uma base
107
+ existente cria backup consistente antes da transação.
108
+
109
+ Cada documento ingerido também é projetado em chunks com caminho, heading,
102
110
  autoridade, data e validade. O Observer faz um feature probe de FTS5 e usa o índice quando a
103
111
  extensão está disponível; caso contrário, mantém a mesma semântica por fallback lexical. A busca
104
112
  retorna o trecho em que houve o match e sua proveniência, não apenas o começo do documento.
105
113
 
106
114
  O `init` projeta `observer-publish` para `SessionStart`, `Stop` e `SubagentStop` depois dos hooks
107
- principais. Sem servidor disponível, ele grava snapshots em `.brain/observer-outbox/` e eventos
108
- SQL em `.brain/observer-sql-outbox/`, sem bloquear a sessão; uma execução posterior tenta
109
- reenviar os lotes. Os lotes SQL são enviados com gzip para que transcripts completos maiores que
115
+ principais. `SessionStart` apenas drena a fila; `Stop` somente a sessão alterada e
116
+ `SubagentStop` somente o transcript do subagente afetado. `note new` e `change archive` enfileiram
117
+ diretamente os documentos que escreveram. Os eventos são coalescidos por escopo e uma lease garante
118
+ um único publisher. Sem servidor disponível, a outbox `.brain/observer-sql-outbox/` é preservada
119
+ sem bloquear a sessão; a varredura integral fica reservada a `observer reconcile`. O `doctor`
120
+ mostra lotes, eventos, bytes e idade da fila. Os lotes SQL são enviados com gzip para que transcripts completos maiores que
110
121
  64 MB em JSON puro continuem dentro do limite do transporte; o Observer descomprime e valida o
111
122
  corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
112
123
 
@@ -114,12 +125,12 @@ corpo antes de ingerir. O outbox é transporte temporário, não autoridade.
114
125
 
115
126
  - `project_not_registered`: rode `observer register` antes de publicar.
116
127
  - `host loopback`: troque `0.0.0.0` ou endereço LAN por `127.0.0.1`.
117
- - Outbox pendente: o serviço estava indisponível; preserve `.brain/observer-outbox/` e
118
- `.brain/observer-sql-outbox/` e repita o publisher. Adicione também
119
- `.brain/observer-sql-state.json` e `.brain/observer-sql-outbox/` ao ignore do Vault. Não apague eventos manualmente.
128
+ - Outbox pendente: o serviço estava indisponível; preserve `.brain/observer-sql-outbox/` e deixe
129
+ um hook posterior drená-la ou rode `observer reconcile`. Adicione também
130
+ `.brain/observer-sql-state.json`, `.brain/observer-sql-outbox/` e `.brain/observer-sql-publisher.lock` ao ignore do Vault. Não apague eventos manualmente.
120
131
  - `WENDKEEP_OBSERVER_NODE_UNSUPPORTED`: execute o Observer em Node.js 22.13 ou mais recente.
121
132
  - Se a memória ou o consumo ficarem incompletos, verifique a tela Sincronização, preserve o
122
- outbox e rode `observer memory import` para reconstruir a carga a partir do vault.
133
+ outbox e rode `observer reconcile` para reconstruir e comprovar paridade por hash.
123
134
 
124
135
  ## Próximos passos
125
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
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": [
@@ -49,7 +49,7 @@ Usage:
49
49
  cannot replace itself. · --vault P · --profile <name> · --yes.
50
50
 
51
51
  wendkeep doctor [--vault P] Health check. --scope core|runtime · --strict for CI/release.
52
- wendkeep observer <sub> Local multi-project Observer: serve | register | publish | status.
52
+ wendkeep observer <sub> Local multi-project Observer: serve | register | publish | reconcile | status.
53
53
  wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
54
54
  status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
55
55
  archive exige verdict (rode verify --deep); abandon descarta sem ADR.
@@ -0,0 +1,217 @@
1
+ -- wendkeep:structural
2
+ -- Rebuild the operational identity tables so external identifiers are scoped by project.
3
+ -- Internal primary keys remain deterministic and opaque to the public API.
4
+
5
+ PRAGMA defer_foreign_keys = ON;
6
+
7
+ CREATE TABLE sessions_v5 (
8
+ session_pk TEXT PRIMARY KEY,
9
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
10
+ session_id TEXT NOT NULL,
11
+ provider TEXT NOT NULL DEFAULT '',
12
+ status TEXT NOT NULL DEFAULT 'unknown',
13
+ summary TEXT NOT NULL DEFAULT '',
14
+ change_slug TEXT NOT NULL DEFAULT '',
15
+ started_at TEXT,
16
+ ended_at TEXT,
17
+ updated_at TEXT NOT NULL,
18
+ metadata_json TEXT NOT NULL DEFAULT '{}',
19
+ UNIQUE(project_id, session_id)
20
+ );
21
+
22
+ INSERT INTO sessions_v5
23
+ SELECT project_id || char(31) || session_id, project_id, session_id, provider, status,
24
+ summary, change_slug, started_at, ended_at, updated_at, metadata_json
25
+ FROM sessions;
26
+
27
+ INSERT OR IGNORE INTO sessions_v5(session_pk, project_id, session_id, updated_at)
28
+ SELECT project_id || char(31) || session_id, project_id, session_id,
29
+ COALESCE(MAX(occurred_at), CURRENT_TIMESTAMP)
30
+ FROM (
31
+ SELECT project_id, session_id, occurred_at FROM usage_rollups
32
+ UNION ALL SELECT project_id, session_id, occurred_at FROM llm_calls
33
+ UNION ALL SELECT project_id, session_id, occurred_at FROM transcripts
34
+ ) GROUP BY project_id, session_id;
35
+
36
+ INSERT OR IGNORE INTO sessions_v5(session_pk, project_id, session_id, updated_at)
37
+ SELECT project_id || char(31) || session_id, project_id, session_id, CURRENT_TIMESTAMP
38
+ FROM agent_runs;
39
+
40
+ CREATE TABLE agent_runs_v5 (
41
+ agent_pk TEXT PRIMARY KEY,
42
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
43
+ agent_id TEXT NOT NULL,
44
+ session_id TEXT NOT NULL,
45
+ parent_agent_id TEXT,
46
+ role TEXT NOT NULL DEFAULT 'main',
47
+ agent_name TEXT NOT NULL DEFAULT '',
48
+ agent_type TEXT NOT NULL DEFAULT '',
49
+ workflow TEXT NOT NULL DEFAULT '',
50
+ status TEXT NOT NULL DEFAULT 'unknown',
51
+ model TEXT NOT NULL DEFAULT '',
52
+ effort TEXT NOT NULL DEFAULT '',
53
+ started_at TEXT,
54
+ ended_at TEXT,
55
+ metadata_json TEXT NOT NULL DEFAULT '{}',
56
+ UNIQUE(project_id, agent_id),
57
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
58
+ FOREIGN KEY(project_id, parent_agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE SET NULL
59
+ );
60
+
61
+ INSERT INTO agent_runs_v5
62
+ SELECT a.project_id || char(31) || a.agent_id, a.project_id, a.agent_id, a.session_id,
63
+ CASE WHEN EXISTS (
64
+ SELECT 1 FROM agent_runs parent
65
+ WHERE parent.project_id = a.project_id AND parent.agent_id = a.parent_agent_id
66
+ ) THEN a.parent_agent_id ELSE NULL END,
67
+ a.role, a.agent_name, a.agent_type, a.workflow, a.status, a.model, a.effort,
68
+ a.started_at, a.ended_at, a.metadata_json
69
+ FROM agent_runs a;
70
+
71
+ INSERT OR IGNORE INTO agent_runs_v5(
72
+ agent_pk, project_id, agent_id, session_id, role, status
73
+ )
74
+ SELECT project_id || char(31) || agent_id, project_id, agent_id, session_id, role, 'unknown'
75
+ FROM (
76
+ SELECT project_id, agent_id, session_id, role FROM usage_rollups
77
+ UNION ALL SELECT project_id, agent_id, session_id, role FROM llm_calls
78
+ UNION ALL SELECT project_id, agent_id, session_id, 'main' AS role FROM transcripts
79
+ );
80
+
81
+ CREATE TABLE usage_rollups_v5 (
82
+ rollup_pk TEXT PRIMARY KEY,
83
+ rollup_key TEXT NOT NULL,
84
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
85
+ session_id TEXT NOT NULL,
86
+ agent_id TEXT NOT NULL,
87
+ role TEXT NOT NULL DEFAULT 'main',
88
+ provider TEXT NOT NULL DEFAULT '',
89
+ model_provider TEXT NOT NULL DEFAULT '',
90
+ model TEXT NOT NULL DEFAULT '',
91
+ effort TEXT NOT NULL DEFAULT '',
92
+ calls INTEGER NOT NULL DEFAULT 0,
93
+ tokens_input INTEGER NOT NULL DEFAULT 0,
94
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
95
+ tokens_cache_read INTEGER NOT NULL DEFAULT 0,
96
+ tokens_output INTEGER NOT NULL DEFAULT 0,
97
+ tokens_reasoning INTEGER NOT NULL DEFAULT 0,
98
+ tokens_total INTEGER NOT NULL DEFAULT 0,
99
+ cost_usd REAL NOT NULL DEFAULT 0,
100
+ cost_status TEXT NOT NULL DEFAULT 'unknown',
101
+ pricing_source TEXT NOT NULL DEFAULT '',
102
+ pricing_version TEXT NOT NULL DEFAULT '',
103
+ wasted_usd REAL NOT NULL DEFAULT 0,
104
+ revision INTEGER NOT NULL DEFAULT 1,
105
+ occurred_at TEXT NOT NULL,
106
+ source_event_id TEXT NOT NULL REFERENCES ingest_events(event_id) ON DELETE CASCADE,
107
+ metadata_json TEXT NOT NULL DEFAULT '{}',
108
+ UNIQUE(project_id, rollup_key),
109
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
110
+ FOREIGN KEY(project_id, agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE CASCADE
111
+ );
112
+
113
+ INSERT INTO usage_rollups_v5
114
+ SELECT project_id || char(31) || rollup_key, rollup_key, project_id, session_id,
115
+ agent_id, role, provider, model_provider, model, effort, calls, tokens_input,
116
+ tokens_cache_write, tokens_cache_read, tokens_output, tokens_reasoning,
117
+ tokens_total, cost_usd, cost_status, pricing_source, pricing_version,
118
+ wasted_usd, revision, occurred_at, source_event_id, metadata_json
119
+ FROM usage_rollups;
120
+
121
+ CREATE TABLE llm_calls_v5 (
122
+ call_pk TEXT PRIMARY KEY,
123
+ call_id TEXT NOT NULL,
124
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
125
+ session_id TEXT NOT NULL,
126
+ agent_id TEXT NOT NULL,
127
+ role TEXT NOT NULL DEFAULT 'main',
128
+ provider TEXT NOT NULL DEFAULT '',
129
+ model_provider TEXT NOT NULL DEFAULT '',
130
+ model TEXT NOT NULL DEFAULT '',
131
+ effort TEXT NOT NULL DEFAULT '',
132
+ sequence INTEGER NOT NULL DEFAULT 0,
133
+ occurred_at TEXT NOT NULL,
134
+ tokens_input INTEGER NOT NULL DEFAULT 0,
135
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
136
+ tokens_cache_read INTEGER NOT NULL DEFAULT 0,
137
+ tokens_output INTEGER NOT NULL DEFAULT 0,
138
+ tokens_reasoning INTEGER NOT NULL DEFAULT 0,
139
+ tokens_total INTEGER NOT NULL DEFAULT 0,
140
+ cost_usd REAL NOT NULL DEFAULT 0,
141
+ cost_status TEXT NOT NULL DEFAULT 'unknown',
142
+ transcript_id TEXT,
143
+ prompt_text TEXT NOT NULL DEFAULT '',
144
+ response_text TEXT NOT NULL DEFAULT '',
145
+ status TEXT NOT NULL DEFAULT 'complete',
146
+ metadata_json TEXT NOT NULL DEFAULT '{}',
147
+ UNIQUE(project_id, call_id),
148
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
149
+ FOREIGN KEY(project_id, agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE CASCADE
150
+ );
151
+
152
+ INSERT INTO llm_calls_v5
153
+ SELECT project_id || char(31) || call_id, call_id, project_id, session_id, agent_id,
154
+ role, provider, model_provider, model, effort, sequence, occurred_at,
155
+ tokens_input, tokens_cache_write, tokens_cache_read, tokens_output,
156
+ tokens_reasoning, tokens_total, cost_usd, cost_status, transcript_id,
157
+ prompt_text, response_text, status, metadata_json
158
+ FROM llm_calls;
159
+
160
+ CREATE TABLE transcripts_v5 (
161
+ transcript_pk TEXT PRIMARY KEY,
162
+ transcript_id TEXT NOT NULL,
163
+ project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
164
+ session_id TEXT NOT NULL,
165
+ agent_id TEXT NOT NULL,
166
+ coverage TEXT NOT NULL DEFAULT 'summary_only',
167
+ codec TEXT NOT NULL DEFAULT 'gzip',
168
+ content_gzip BLOB NOT NULL,
169
+ content_sha256 TEXT NOT NULL,
170
+ original_bytes INTEGER NOT NULL,
171
+ compressed_bytes INTEGER NOT NULL,
172
+ source TEXT NOT NULL DEFAULT '',
173
+ occurred_at TEXT NOT NULL,
174
+ metadata_json TEXT NOT NULL DEFAULT '{}',
175
+ UNIQUE(project_id, transcript_id),
176
+ FOREIGN KEY(project_id, session_id) REFERENCES sessions_v5(project_id, session_id) ON DELETE CASCADE,
177
+ FOREIGN KEY(project_id, agent_id) REFERENCES agent_runs_v5(project_id, agent_id) ON DELETE CASCADE
178
+ );
179
+
180
+ INSERT INTO transcripts_v5
181
+ SELECT project_id || char(31) || transcript_id, transcript_id, project_id, session_id,
182
+ agent_id, coverage, codec, content_gzip, content_sha256, original_bytes,
183
+ compressed_bytes, source, occurred_at, metadata_json
184
+ FROM transcripts;
185
+
186
+ DROP TABLE llm_calls;
187
+ DROP TABLE transcripts;
188
+ DROP TABLE usage_rollups;
189
+ DROP TABLE agent_runs;
190
+ DROP TABLE sessions;
191
+
192
+ ALTER TABLE sessions_v5 RENAME TO sessions;
193
+ ALTER TABLE agent_runs_v5 RENAME TO agent_runs;
194
+ ALTER TABLE usage_rollups_v5 RENAME TO usage_rollups;
195
+ ALTER TABLE llm_calls_v5 RENAME TO llm_calls;
196
+ ALTER TABLE transcripts_v5 RENAME TO transcripts;
197
+
198
+ CREATE INDEX idx_sessions_project_time ON sessions(project_id, started_at, ended_at);
199
+ CREATE INDEX idx_agent_runs_project_session ON agent_runs(project_id, session_id, role);
200
+ CREATE INDEX idx_usage_rollups_project_time ON usage_rollups(project_id, occurred_at);
201
+ CREATE INDEX idx_usage_rollups_project_agent ON usage_rollups(project_id, agent_id, role);
202
+ CREATE INDEX idx_usage_rollups_project_model ON usage_rollups(project_id, model_provider, model);
203
+ CREATE INDEX idx_llm_calls_project_time ON llm_calls(project_id, occurred_at);
204
+ CREATE INDEX idx_llm_calls_project_agent ON llm_calls(project_id, agent_id, sequence);
205
+ CREATE INDEX idx_llm_calls_project_model ON llm_calls(project_id, model_provider, model);
206
+ CREATE INDEX idx_transcripts_project_session ON transcripts(project_id, session_id, occurred_at);
207
+ CREATE INDEX idx_transcripts_project_coverage ON transcripts(project_id, coverage);
208
+
209
+ CREATE TABLE project_snapshots (
210
+ project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
211
+ event_id TEXT NOT NULL,
212
+ captured_at TEXT NOT NULL,
213
+ snapshot_json TEXT NOT NULL,
214
+ UNIQUE(project_id, event_id)
215
+ );
216
+
217
+ CREATE INDEX idx_project_snapshots_captured ON project_snapshots(captured_at);
package/src/change.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // `wendkeep change <sub>` — native change lifecycle CLI (Pilar B).
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
3
  import { isAbsolute, join, resolve } from 'node:path';
4
4
  import {
5
5
  newChange,
@@ -22,6 +22,22 @@ import { evaluateGate, requiredSensors } from '../hooks/sensors-core.mjs';
22
22
  import { buildEffectiveRequirementPackage, evaluateVerdict, formatOrphanReqs, tasksHashOf, parseSpecsList, parseDelta, parseRequirements, applyDelta, validateSpecImpact } from '../hooks/spec-core.mjs';
23
23
  import { getNextAdrNumber, readControl, readSessionRegistry, upsertSessionRegistry } from '../hooks/obsidian-common.mjs';
24
24
  import { getLocale } from '../hooks/locale.mjs';
25
+ import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
26
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
27
+
28
+ function observerMarkdownUnder(vaultBase, relativeRoot) {
29
+ const output = [];
30
+ const walk = (absolute, relative) => {
31
+ for (const entry of readdirSync(absolute, { withFileTypes: true })) {
32
+ const nextAbsolute = join(absolute, entry.name);
33
+ const nextRelative = join(relative, entry.name);
34
+ if (entry.isDirectory()) walk(nextAbsolute, nextRelative);
35
+ else if (entry.isFile() && entry.name.endsWith('.md')) output.push(nextRelative);
36
+ }
37
+ };
38
+ try { walk(join(vaultBase, relativeRoot), relativeRoot); } catch { /* reconcile recupera */ }
39
+ return output;
40
+ }
25
41
 
26
42
  function resolveVault(argv) {
27
43
  let vault;
@@ -299,6 +315,30 @@ export function runChange(argv) {
299
315
  process.stderr.write(`change archive BLOCKED (gate): ${r.failing.join('; ')}\n`);
300
316
  process.exit(1);
301
317
  }
318
+ try {
319
+ const project = readProjectForValidation(vaultBase);
320
+ if (project.ok && project.projectId) {
321
+ const loc = getLocale(vaultBase);
322
+ for (const archivedPath of observerMarkdownUnder(vaultBase, r.archivedRel)) {
323
+ enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath: archivedPath });
324
+ const suffix = archivedPath.slice(String(r.archivedRel).length).replace(/^[\\/]+/, '');
325
+ enqueueObserverDocumentChange({
326
+ vaultBase,
327
+ projectId: project.projectId,
328
+ logicalPath: join(loc.folders.changes, slug, suffix),
329
+ deleted: true,
330
+ });
331
+ }
332
+ if (r.adrRel) enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath: r.adrRel });
333
+ for (const capability of r.promoted || []) {
334
+ enqueueObserverDocumentChange({
335
+ vaultBase,
336
+ projectId: project.projectId,
337
+ logicalPath: join(loc.folders.specs, capability, 'spec.md'),
338
+ });
339
+ }
340
+ }
341
+ } catch { /* Observer é fail-open; reconcile recupera qualquer enqueue perdido. */ }
302
342
  process.stdout.write(`archived: ${r.archivedRel}${r.adrRel ? `; ADR: ${r.adrRel}` : '; GUIDE compacta: sem ADR'}\n`);
303
343
  if (r.promoted && r.promoted.length) process.stdout.write(`specs promovidas: ${r.promoted.join(', ')}\n`);
304
344
  if (r.specWarnings && r.specWarnings.length) for (const w of r.specWarnings) process.stderr.write(` aviso spec: ${w}\n`);
package/src/doctor.mjs CHANGED
@@ -5,6 +5,7 @@ import { checkHarness, checkVaultLinks, checkSessionActivity, checkStackedFrontm
5
5
  import { runVaultHealth } from '../hooks/vault-health.mjs';
6
6
  import { checkSyncDefs } from './sync-defs.mjs';
7
7
  import { resolveProjectVault } from './project-vault.mjs';
8
+ import { inspectObserverSqlOutbox } from './observer-sql-publish.mjs';
8
9
 
9
10
  const healthStatusLabel = (status) => ({
10
11
  healthy: 'saudável', warning: 'atenção', degraded: 'degradada', blocked: 'bloqueada', legacy: 'legado',
@@ -155,6 +156,10 @@ export function runDoctor(argv) {
155
156
  // 3e. Observabilidade materializada: schema vigente não basta sem frontier + manifest frescos.
156
157
  const observability = checkSessionObservability(vaultBase);
157
158
  process.stdout.write(`\n${renderSessionObservabilityLines(observability).join('\n')}\n`);
159
+ const observerOutbox = inspectObserverSqlOutbox(vaultBase);
160
+ const oldestSeconds = Math.round(observerOutbox.oldest_age_ms / 1000);
161
+ process.stdout.write(`[observer] outbox SQL: ${observerOutbox.batches} lote(s) · ${observerOutbox.events} evento(s) · ${observerOutbox.bytes} bytes${observerOutbox.batches ? ` · mais antigo: ${oldestSeconds}s` : ''}\n`);
162
+ if (observerOutbox.batches) process.stdout.write(' → wendkeep observer reconcile --project . (ou inicie o servidor para o hook drenar a fila)\n');
158
163
 
159
164
  // 4. Sessão: não mente "inativa" quando há atividade recente (workflow/subagente em background).
160
165
  const act = checkSessionActivity(vaultBase);
package/src/init.mjs CHANGED
@@ -323,7 +323,7 @@ const MESSAGES = {
323
323
  mcpSkipped: ' [4/5] .mcp.json ignorado (--no-mcp, sem companions MCP)',
324
324
  colorsSkipped: ' [5/5] cores ignoradas (--no-colors)',
325
325
  colors: (r) => ` [5/5] cores: ${r}`,
326
- runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
326
+ runtimeIgnore: ' [!] ignore runtimes locais do wendkeep no Git quando o vault for versionado: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/ .brain/observer-sql-publisher.lock',
327
327
  merged: 'mesclado', created: 'criado', bakSaved: ', .bak salvo',
328
328
  nextSteps: '\nPróximos passos:',
329
329
  step1: (v) => ` 1. Abra o vault no Obsidian: "Abrir pasta como cofre" -> ${v}`,
@@ -352,7 +352,7 @@ const MESSAGES = {
352
352
  mcpSkipped: ' [4/5] .mcp.json skipped (--no-mcp, no MCP companions)',
353
353
  colorsSkipped: ' [5/5] colors skipped (--no-colors)',
354
354
  colors: (r) => ` [5/5] colors: ${r}`,
355
- runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/',
355
+ runtimeIgnore: ' [!] keep local wendkeep runtimes out of Git when the vault is versioned: .brain/.change-* .brain/runtime/flows/ .brain/observer-sql-state.json .brain/observer-sql-outbox/ .brain/observer-sql-publisher.lock',
356
356
  merged: 'merged', created: 'created', bakSaved: ', .bak saved',
357
357
  nextSteps: '\nNext steps:',
358
358
  step1: (v) => ` 1. Open the vault in Obsidian: "Open folder as vault" -> ${v}`,
package/src/note.mjs CHANGED
@@ -17,6 +17,8 @@ import { getLocale } from '../hooks/locale.mjs';
17
17
  import { buildManualBugNote, buildManualLearningNote, relinkDerivedNotes } from '../hooks/linked-notes.mjs';
18
18
  import { repairStackedFrontmatter } from '../hooks/frontmatter-repair.mjs';
19
19
  import { repairDerivedSections } from '../hooks/derived-sections.mjs';
20
+ import { enqueueObserverDocumentChange } from './observer-sql-publish.mjs';
21
+ import { readProjectForValidation } from '../packages/vault/src/validate-memory.mjs';
20
22
 
21
23
  const TYPES = {
22
24
  bug: { folderKey: 'bugs', prefix: 'BUG', build: buildManualBugNote },
@@ -120,6 +122,11 @@ export function runNote(argv) {
120
122
  } catch { /* sem sessão ativa — nota nasce sem backlink */ }
121
123
 
122
124
  writeFileSync(filePath, kind.build(title.trim(), { num, dateStr, sessionRel, localeId: loc.id }), 'utf8');
123
- process.stdout.write(`${toVaultRelative(vaultBase, filePath)}\n`);
125
+ const logicalPath = toVaultRelative(vaultBase, filePath);
126
+ try {
127
+ const project = readProjectForValidation(vaultBase);
128
+ if (project.ok && project.projectId) enqueueObserverDocumentChange({ vaultBase, projectId: project.projectId, logicalPath });
129
+ } catch { /* Observer é fail-open; reconcile recupera qualquer enqueue perdido. */ }
130
+ process.stdout.write(`${logicalPath}\n`);
124
131
  process.exit(0);
125
132
  }