fix(memory): persist decision content (B-MCP-4 critical data loss)#20
Merged
fix(memory): persist decision content (B-MCP-4 critical data loss)#20
Conversation
Issue #3 (CRITICAL data loss). The wire schema in docs/02 §4.4 documented `content: string` as the canonical full-text body for every mem.remember kind, but the `decisions` table had no `content` column. The RememberFacadeAdapter routed `input.content` into `rationale` (when rationale was absent) and silently dropped it otherwise. Recall returned `rationale` in the wire `content` field to compound the confusion. Decision: Option B from the comparative ADR — preserve the documented wire contract by adding the column. Stability of the public protocol outweighs the cost of a one-time schema migration on existing workspaces (see feedback memory: "Always prioritize stability over velocity"). Schema: - migration 008__decisions-content.sql adds `content TEXT NOT NULL DEFAULT ''`, backfills existing rows with `content = rationale` (preserves searchability across legacy data), drops/recreates the decisions_fts virtual table to include the new column, and reindexes from the base table. Triggers updated to keep the FTS mirror in sync; carries forward migration 007's column-scoped UPDATE OF optimization. Domain: - New `DecisionContent` VO extending `NonEmptyString`; max 50,000 chars (well above Rationale's 5,000 — content is the long-form body where rationale is the short why). - Decision aggregate gains `content: DecisionContent`. Both factories (`record`, `rehydrate`) accept the new field. New `getContent()` accessor. Application: - RecordDecision port adds optional `content`; the use case falls back to `rationale` when absent (defensive default for internal workflows that bypass the wire). - ImportHandoffUseCase + JsonMemoryImporter mirror the same fallback so handoff/JSON importers from pre-B-MCP-4 snapshots rehydrate cleanly. Infrastructure: - SqliteDecisionRepository writes/reads the new column. Zod schema updated; UPSERT and SELECT statements include `content`. - SqliteMemoryProjectionRepository (recall side) selects `content` and uses it as the preview returned in the wire response. Falls back to rationale if the row predates migration 008. - JsonMemoryExporter serializes `content` so round-trip exports/imports stay lossless. Composition: - RememberFacadeAdapter passes `input.content` through to the use case for kind=decision. The previous code path silently dropped the field. Tests: - N-decision-content-roundtrip.test.ts (NEW, 2 cases): VALUES validation per Phase-9 lesson. Insert via mem.remember with rationale != content, assert SQL row has both intact, recall a token that appears ONLY in content, assert the wire response surfaces the actual content (not rationale). Plus the rationale-fallback default for content-less internal calls. - value-objects.test.ts (+5 cases): DecisionContent invariants. - memory-database fixture: applies migration 008 alongside 000/004/005. - 6 existing test files updated: Decision builders inject content field; importer/exporter tests assert content round-trips; smoke test asserts versions [0..8]. - Audit of turns/tasks confirmed they have their own dedicated text columns (summary/description) so the wire content field routes correctly there — only decisions had the silent-drop defect. 5/5 EXIT=0: - typecheck, lint, lint:tests, validate:modules, build, test - 2519 tests passing in 208 files (was 2512 in 207) Closes #3 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
h2devx
added a commit
that referenced
this pull request
May 2, 2026
## Que cambia Cuts `v0.1.2-beta.3` consolidando los 4 fixes de Phase-11 que cierran los 4 bugs de dogfood reportados en Phase-9 (B-MCP-2/3/4/5). ## Por que `@netzi/recall@0.1.2-beta.0` (en npm beta channel) tenia 4 issues abiertos que rompian la promesa central del producto (semantic recall, mem.health diagnostics, decision content storage). Phase-11 los cerro todos via PRs squash-mergeados a `develop`. Esta release branch consolida los version bumps + release notes + HANDOFF update para promover beta.3 a `main` y publish a npm. ## Tipo de cambio - [x] chore — release (no code change beyond version bumps) ## Cambios incluidos (commits ya en develop, no parte de este PR) | Issue | Tag | PR | Commit en develop | |---|---|---|---| | [#2](#2) | B-MCP-3 critical | [#17](#17) | `229e7cd` | | [#1](#1) | B-MCP-2 high | [#18](#18) | `05b6731` | | [#4](#4) | B-MCP-5 low | [#19](#19) | `c4a2d1d` | | [#3](#3) | B-MCP-4 critical (data loss) | [#20](#20) | `52fbfd9` | ## Cambios de este PR - `code/package.json`: `0.1.2-beta.0` → `0.1.2-beta.3` - `code/src/bootstrap/composition-root.ts`: default `serverInfo.version` actualizado - `code/sonar-project.properties`: `projectVersion` actualizado - `docs/RELEASE-NOTES-v0.1.2-beta.3.md`: NUEVO, documentando los 4 fixes + migration safety + engineering metrics + outstanding caveats + path a v0.1.2 stable - `HANDOFF.md`: §0 actualizado al estado post-Phase-11; §6.16 nueva con la cronologia completa, 10 decisiones del orquestador (D-1101..D-1110), y 6 hallazgos durables ## Checklist - [x] `npm run typecheck` EXIT=0 - [x] `npm run lint` y `npm run lint:tests` EXIT=0 - [x] `npm run validate:modules` EXIT=0 - [x] `npm run build` EXIT=0 - [x] `npm run test` EXIT=0 — 2519 tests passing en 208 archivos - [x] Cero `any`, cero `as any`, cero `// @ts-ignore` - [x] HANDOFF.md actualizado (§0 + §6.16 nueva) - [x] Release notes consolidadas en `docs/RELEASE-NOTES-v0.1.2-beta.3.md` ## E2E que validan VALORES - [x] N/A — release branch sin cambios de codigo nuevos. Los 3 archivos test value-validation (L/M/N) ya estan en develop via PRs #17/#18/#20. ## Plan post-merge ```bash git checkout main && git pull git tag -a v0.1.2-beta.3 -m "v0.1.2-beta.3" git push origin v0.1.2-beta.3 gh release create v0.1.2-beta.3 --prerelease --notes-file docs/RELEASE-NOTES-v0.1.2-beta.3.md # Usuario ejecuta el publish: cd code && npm publish --tag beta --auth-type=web # Merge-back develop: git checkout develop && git merge --no-ff main && git push origin develop # Dogfood real: npm install -g @netzi/recall@beta && recall mcp serve # o el comando equivalente ``` Si el dogfood post-publish surface nuevos defectos → cortar `beta.4`. Si pasa limpio → cortar `release/0.1.2` (stable) + promover `latest` desde 0.1.1 → 0.1.2 + hard-deprecate 0.1.1. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks
h2devx
added a commit
that referenced
this pull request
May 2, 2026
#23) ## Que cambia Hotfix que actualiza los docs publicos (README.md raiz, code/README.md shipped en npm tarball, SECURITY.md, CONTRIBUTING.md) para reflejar que los 4 bugs B-MCP-2..5 quedaron cerrados en \`v0.1.2-beta.3\`. ## Por que PR #21 cerro el release v0.1.2-beta.3 pero solo actualizo: - bumps de version (package.json, composition-root, sonar) - HANDOFF.md (§0 + §6.16) - docs/RELEASE-NOTES-v0.1.2-beta.3.md (nuevo) **Olvidamos los public-facing docs**. Cuando el usuario tomo el codigo a publish, el README.md root y el code/README.md (shipped en npm tarball como package README) seguian diciendo: - "trabajo activo en 0.1.2-beta.x" - "4 issues abiertos hoy (B-MCP-2..5)" - "npm install -g @netzi/recall" (defaults a latest=0.1.1 deprecada) Sin este hotfix, la pagina del paquete en npmjs.com mostraria informacion incorrecta sobre el estado del producto justo despues del publish. ## Tipo de cambio - [x] docs — solo documentacion publica - [x] hotfix — fix sobre main (perdimos esto en el release; sin esto el npm publish sale con package.json correcto pero README stale) ## Updates - **README.md** (root): banner refleja "v0.1.2-beta.3 cierra los 4 bugs"; quick start nota canal beta; "Issues" reads "0 issues abiertos al cierre de Phase-11 (cerrados via PRs #17/#18/#19/#20)". - **code/README.md** (shipped en npm tarball como package README): install command recomienda `@beta` con nota sobre por que; CTA apunta a beta.3 en vez de latest=0.1.1 deprecada. - **SECURITY.md**: tabla de versiones soportadas incluye `0.1.2-beta.3` (active), marca `0.1.2-beta.0` como superseded, nota que latest=0.1.1 sigue deprecada hasta que 0.1.2 stable promote. - **CONTRIBUTING.md**: "Issues y bugs" reads "0 issues abiertos al cierre de Phase-11" con pointers a §6.16 + release notes. ## Artefactos historicos intencionalmente sin tocar - `docs/RELEASE-NOTES-v0.1.2-beta.0.md` — snapshot de un release anterior; release notes son inmutables por convencion. - `CONTRIBUTING.md` line 139 commit-format example mencionando beta.0 — illustrative, no truth claim. ## Checklist - [x] `npm run typecheck` EXIT=0 - [x] `npm run lint` y `npm run lint:tests` EXIT=0 - [x] `npm run validate:modules` EXIT=0 - [x] `npm run build` EXIT=0 - [x] `npm run test` EXIT=0 (no source files touched, suite previa sigue valida) - [ ] N/A — wire/protocolo MCP no cambia - [ ] N/A — no introduce ADR ## Plan post-merge Per CONTRIBUTING.md hotfix flow: tras merge a main, hacer merge-back a develop (PR separado). Adicionalmente, **actualizar el tag v0.1.2-beta.3** para que apunte al commit con los doc fixes incluidos antes del npm publish: \`\`\`bash git checkout main && git pull git tag -d v0.1.2-beta.3 git push origin :refs/tags/v0.1.2-beta.3 git tag -a v0.1.2-beta.3 -m "v0.1.2-beta.3" git push origin v0.1.2-beta.3 gh release edit v0.1.2-beta.3 --target main # apunta el release al nuevo SHA cd code && npm publish --tag beta --auth-type=web \`\`\` (El tag v0.1.2-beta.3 actualmente apunta a \`a826ef0\`, sin los doc fixes. Re-tagging es seguro porque el GitHub release de beta.3 aun no tiene downloads y npm publish todavia no se completo.) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 tasks
h2devx
added a commit
that referenced
this pull request
May 3, 2026
## Summary **First stable release of `@netzi/recall`.** Channel promotion from `0.1.2-beta.6` (commit `f3aca46`) to the `latest` dist-tag. **No code changes** — same binary bit-for-bit modulo the version string. Decision: skip the soak 24-48h post-beta.6 (justified by fresh smoke 10/10 PASS + 0 issues + cycle of 7 betas that already closed 8 bugs vinculated to real use). The cycle's bug discovery rate has been "1 bug per beta surfaced by dogfood" — staying on beta.6 longer would not surface new information. | Item | Value | |---|---| | Tag | `v0.1.2` (no suffix) | | Channel | `latest` (was `beta` for the entire `0.1.2-beta.*` cycle) | | Tests | **2560 passing** in 212 files (no change vs beta.6) | | Coverage SonarQube | overall 96.4%, ratings A/A/A | | Issues at release | **0** open | ## What 0.1.2 stable consolidates The `0.1.2-beta.*` cycle ran from 2026-04-28 (beta.0) to 2026-05-03 (beta.6). Each release surfaced exactly one bug from real-world dogfood that the previous release exposed: | Beta | Date | Bugs closed | PR | |---|---|---|---| | `0.1.2-beta.0` | 2026-04-28 | preventive cut after Phase-9 dogfood discovery | n/a | | `0.1.2-beta.3` | 2026-05-01 | **B-MCP-2** + **B-MCP-3** + **B-MCP-4** + **B-MCP-5** | [#17](#17), [#18](#18), [#19](#19), [#20](#20) | | `0.1.2-beta.4` | 2026-05-02 | **B-MCP-7** | [#27](#27) | | `0.1.2-beta.5` | 2026-05-02 | **B-MCP-8** | [#33](#33) | | `0.1.2-beta.6` | 2026-05-03 | **carryover** `serverInfo.version` | [#37](#37) | | **`0.1.2` (this)** | 2026-05-03 | channel promotion + npm deprecation of 0.1.0/0.1.1 | this PR | Plus B-MCP-1 closed in v0.1.1 (Phase-8 same-day patch). Total: 8 bugs closed end-to-end via dogfood + smoke loop. ## Files in this release branch | Capa | Archivos | |---|---| | Code | (cero cambios — channel promotion sin cambios de logica) | | Release tooling | `code/package.json` + `code/sonar-project.properties` (bump 0.1.2-beta.6 → 0.1.2) | | Release notes | `docs/RELEASE-NOTES-v0.1.2.md` (NEW, ~250 LOC consolidating the full cycle + migration guide) | | Public docs | `README.md`, `code/README.md`, `SECURITY.md` (banner "stable", install command without @beta, version table updated) | | HANDOFF | `HANDOFF.md` (§0 + new §6.21 Phase-16 + footer) | ## Validation | Check | Result | |---|---| | `npm run typecheck` | EXIT=0 | | `npm run lint` (max-warnings 0) | EXIT=0 | | `npm run lint:tests` (max-warnings 0) | EXIT=0 | | `npm run validate:modules` | PASS — no module violations | | `npm run build` (tsup) | EXIT=0 | | `npm test` | **2560 passing** in 212 files | ## Test plan - [x] 5+1 EXIT=0 over the release branch. - [x] PR #38 (release v0.1.2-beta.6) merged to main. - [x] Fresh smoke 10/10 PASS against npx-installed beta.6 in fresh workspace (this is the same binary). - [ ] CI green on this PR. - [ ] Squash-merge to main. - [ ] Tag `v0.1.2` annotated to merge commit + push (`git switch --detach v0.1.2` because of protected-branch hook). - [ ] `gh release create v0.1.2 --target main --notes-file docs/RELEASE-NOTES-v0.1.2.md` — **NO `--prerelease`**. - [ ] `cd code && npm publish --auth-type=web` — **NO `--tag beta`** → publishes directly to `latest`. - [ ] `npm deprecate @netzi/recall@0.1.0 "deprecated due to bugs B-MCP-1..8 (closed in 0.1.2). Use @netzi/recall@latest"`. - [ ] `npm deprecate @netzi/recall@0.1.1 "deprecated due to bugs B-MCP-2..8 (closed in 0.1.2). Use @netzi/recall@latest"`. - [ ] `npm view @netzi/recall dist-tags` should return `{ latest: '0.1.2', beta: '0.1.2-beta.6' }`. - [ ] `npx --yes @netzi/recall@latest --help` should execute 0.1.2 (not 0.1.1). - [ ] Merge-back develop ← main via `chore/sync-develop-after-0.1.2`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Que cambia
Cierra B-MCP-4 (issue #3, critical, data loss) via Option B
(decision tomada en el cuadro comparativo previo, alineada con la
regla durable "siempre priorizar la estabilidad"):
decisions.content TEXT NOT NULL DEFAULT '', backfillcontent = rationalepara filas existentes,rebuild de
decisions_ftsvirtual table incluyendo la nuevacolumna, triggers actualizados con UPDATE OF column-scope.
DecisionContent(max 50K chars vs 5K deRationale). Aggregate
Decisionlleva el campo enrecordyrehydrate.RecordDecisionport + use case aceptan contentoptional con fallback a rationale (defensa para flujos internos
no-wire). Importer de handoff + JSON tambien.
SqliteDecisionRepositorylee/escribe lacolumna;
SqliteMemoryProjectionRepository(lado recall) lasurface en el wire response.
JsonMemoryExporterla incluye pararound-trip lossless.
RememberFacadeAdapterya no descartasilenciosamente
input.contentparakind=decision— lo pasaintacto al use case.
Por que
Fixes #3. Wire schema documentaba
contentcomo campo top-levelpara todas las kinds desde v0.1.0; la tabla
decisionsno tenia lacolumna; la facade descartaba el valor sin error. Recall retornaba
rationaleen el campo wirecontentpara empeorar la confusion.Eleccion entre Option A (drop
contentdel wire) y Option B(agregar columna + migracion): Option B porque preservar el
contrato publico documentado tiene prioridad sobre la velocidad.
Audit
turns/tasksconfirmado: ambos tienen sus columnasdedicadas (
summary/description) que recibencontentcorrectamente.Solo
decisionstenia el silent-drop. Sin scope creep.Tipo de cambio
Checklist
npm run typecheckEXIT=0npm run lintynpm run lint:testsEXIT=0npm run validate:modulesEXIT=0npm run buildEXIT=0npm run testEXIT=0 — 2519 tests passing en 208 archivos (was 2512 in 207)any, ceroas any, cero// @ts-ignoredocs/02 §4.4(sin cambio — el contrato existente AHORA se honra)E2E que validan VALORES
tests/integration/N-decision-content-roundtrip.test.ts(2 casos)sigue la metodologia de Phase-9:
mem.remember(kind=decision)conrationaleSHORT ycontentLONG, distintos.SELECT content FROM decisions WHERE id=?debe contener el content largo, NO el rationale.
mem.recallcon un token que aparece SOLOen content (no en title ni rationale) — debe traer el hit, y
result.contentdebe ser el content largo (no el rationale,que era el comportamiento pre-fix).
contentdebe persistircontent = rationale.Pre-fix:
result.content === rationale(siempre). Post-fix:result.content === content(lo que el cliente envio).Notas para el reviewer
Migracion 008 sobre datos existentes:
content = rationale(no empty string) parapreservar searchability via FTS5 sobre la dogfood DB del usuario
(27 decisions reales). Documentado en el SQL header.
decisions_ftsnecesario porque FTS5 nosoporta ALTER. Operacion idempotente porque la migration runner
honra
_meta-based applied versions (la migracion no se reaplica).UPDATE OF title, rationale, contentpara preservar la optimizacion de migration 007 (no reindexar FTS
en updates de
confidence).Forward-compat de exports legacy:
JsonMemoryImporter.DecisionSchemamarcacontentcomooptionalpara consumir snapshots v0.1.0/v0.1.1 que no lo tienen.Cuando absent → buildDecision usa rationale.
SqliteMemoryProjectionRepository.DecisionRowSchemaigual:content: z.string().optional()con fallback a rationale en elpreview.
Por que NO se toca
turnsnitasks:summary: input.content→ tiene su columnasummary.description: input.content→ tiene su columnadescription.decisionstenia el bug.