fix: layer memory_category onto reflection-mapped rows (write-time, read-time, backfill) - #952
Conversation
c6d13e0 to
1790de9
Compare
257fce6 to
aa79cc6
Compare
rwmjhb
left a comment
There was a problem hiding this comment.
Reviewed head aa79cc6. The targeted tests, full suite, and repository CI pass, but the write path violates the primary category-column contract.
index.ts stores smart taxonomy values (preferences, patterns, cases) directly in MemoryEntry.category via a cast, while that column is defined in the legacy storage vocabulary (preference, fact, decision, etc.) and the repository already provides getStorageCategoryForMemoryCategory for this conversion. Direct consumers therefore see an incompatible value. A focused compaction check showed both cases and preferences sources being reconstructed with memory_category: patterns; the new preferences shape also derives a different default layer from an equivalent legacy-backed row. With compaction enabled, correctly stamped source rows can be deleted and replaced by a misclassified merged row.
Please keep entry.category in the legacy storage vocabulary and store the six-category value only in metadata.memory_category. Add production-path tests covering compaction and default-layer parity between newly written and backfilled rows.
Important follow-ups: paginate --categories-only beyond the fixed 10,000-row first page, keep admission and persistence on the same centralized mapping, restrict legacy-decision fallback to identifiable mapped rows, and merge category metadata against the latest locked row to avoid lost updates.
|
Fixed in 69fab12. The write site now derives the stored column through the central smart-to-storage mapping (getStorageCategoryForMemoryCategory via a new getReflectionMappedStorageCategory helper), and the six-category value lives only in metadata.memory_category. Read side, two hardenings for rows already written with six-category column values: reverseMapLegacyCategory reads a six-category value back as itself instead of defaulting to patterns, and the decision-to-cases redirect is now gated on the row's own mapped identity (metadata type), so a bare legacy decision row keeps the canonical decision-to-events mapping your sibling translators use. Layer derivation prefers a valid stamped memory_category, which restores default-layer parity between newly written and backfilled rows. Follow-ups from your review, dispositions:
Validation: new red-proofed cells for the compaction reconstruction (cases and preferences sources no longer collapse to patterns), default-layer parity, the storage-column vocabulary pins, backfill pagination, column repair, and fresh-read patch building (10 cells red pre-fix, all green post). Full suite 454/454, tsc clean, cli-smoke group green, dist rebuilt in the same commit. |
|
Thanks for addressing the primary storage-category blocker in |
reverseMapLegacyCategory routed the legacy "decision" store category through its own branch, defaulting to "events" (append-only). This shields never-migrated reflection-mapped decision rows, and anything else relying on the fallback, from consolidation/dedup even though they are durable operational facts, not one-off occurrences. Merge "decision" into the same branch as "fact" so it resolves to "cases" (or "profile" for personal-identity text) exactly like fact does.
Reflection-mapped rows carried mappedCategory/mappedKind but no memory_category, so readers derived it via reverseMapLegacyCategory from the row-level legacy category alone. mappedKind is known structurally at write time (each kind comes from a fixed reflection section), so stamp memory_category directly from a static mappedKind lookup instead of relying on the read-time fallback: user-model/agent-model -> preferences, lesson/decision -> cases. Zero impact on existing stores; only new rows going forward get the stamp (see the follow-up opt-in `memory-pro upgrade` pass for backfilling existing rows).
Reflection-mapped rows written before write-time memory_category stamping (and any store an operator hasn't re-run since) still lack the stamp entirely. isLegacyMemory()/upgrade() deliberately exclude reflection rows, so the general legacy-upgrade sweep never reaches them either. Add MemoryUpgrader.normalizeMappedRowCategories(): a narrower, one-shot pass that scans for memory-reflection-mapped rows and re-stamps memory_category from the same mappedKind lookup new rows get, touching only that field and only when the stamped value is missing or wrong. Idempotent (a row already correct is left alone, so a second run is a no-op) and dry-run capable. Wired up as `memory-pro upgrade --categories-only [--dry-run] [--scope]`, reusing the existing command's flag conventions.
Wire the three new tests (reverse-map-legacy-category, reflection-mapped-category-stamping, memory-upgrader-category- normalization) into package.json's local test chain and scripts/ci-test-manifest.mjs's storage-and-schema group.
…el rows become patterns The heading->category map now has one source of truth (REFLECTION_MAPPED_MEMORY_CATEGORY, keyed by structural kind): the metadata stamp and the stored row category both read it. Agent self-observations map to patterns instead of polluting user preferences, and mapped rows mint their smart taxonomy category as the row category instead of the legacy preference/fact/decision names. The upgrader's mapped-row normalization re-stamps existing rows through the same map. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A mapped or item row is one distilled line, so the line is its own abstract and content and the section heading forms the overview. Level-less rows fell back to three identical Abstract/Overview/Content lines in every shared pipeline prompt.
…lary The mapped-row persist site cast the six-category taxonomy value straight into the legacy-typed category column, so compaction's plurality vote and the read-time reverse mapping mis-defaulted those rows to patterns, and newly written rows derived a different default layer than equivalent legacy-backed rows. - persist mapped rows through the central smart-to-storage mapping; the six-category value lives only in metadata.memory_category - reverse mapping reads six-category column values back as themselves (tolerance for rows written by earlier builds) and applies the decision-to-cases redirect only to identifiable mapped rows; bare legacy decision rows keep the canonical decision-to-events mapping - layer derivation prefers a valid stamped memory_category, restoring default-layer parity between newly written and backfilled rows - admission scoring reads the same kind-to-category table the persisted stamp uses, so judge register and stored register always agree - the categories-only backfill pages the whole store (no fixed first-page cap), repairs six-category column values back to storage vocabulary, and builds each patch from a fresh per-chunk read so concurrent metadata writes survive
|
Rebased onto current master (post #927/#943/#974 merges). Semantic reconciliation was confined to the reflection persist site: the branch now layers its mappedKind taxonomy and storage-category mapping on top of the merged #943 mirror callback (both behaviors preserved). Everything else was mechanical test-registration unions. Gates green on the new head: typecheck, full suite, manifest verifier, fresh dist. |
69fab12 to
05457c0
Compare
rwmjhb
left a comment
There was a problem hiding this comment.
Re-reviewed rebased head 05457c0. The prior storage-contract blocker is fixed: mapped rows keep the legacy storage vocabulary in entry.category, while the six-category value remains in metadata.memory_category. The rebased persistence path preserves the merged #943 mirror callback, and the updated pagination, admission mapping, legacy-decision fallback, compaction/parity regressions, full suite, packaging checks, and repository CI all pass. I found no remaining HIGH/CRITICAL issue. Approving.
Follow-ups worth addressing: derive unstamped historical agent-model rows from mappedKind; make the category backfill an atomic field-level metadata merge; preserve valid member category stamps during compaction; honor or reject --limit explicitly in --categories-only; document that the command may also repair the storage column; and remove the whole-file index.ts line-ending churn/dead import.
Problem
Reflection-mapped rows (the "User model deltas", "Agent model deltas", "Lessons & pitfalls", and "Decisions (durable)" sections written by the reflection writer) carry a
mappedCategory/mappedKindin their metadata, but never amemory_categoryin the 6-category sense that the rest of the store uses. Readers fall back to deriving one from the row's legacy store category viareverseMapLegacyCategory, and that fallback sends mapped decision rows straight intoevents, an append-only category. Since durable operator/system decisions are not one-off occurrences, they end up permanently shielded from consolidation and dedup for the wrong reason: not because they are append-only in nature, but because of a mapping gap.This PR closes that gap in three layers, following the write-time stamping pattern from #950 and building on the single-sourced category map from #935.
Layer 1: write-time stamping
buildReflectionMappedMetadatanow stampsmemory_categorydirectly from the row'smappedKind, since the kind is known structurally at write time (each one comes from a fixed reflection section) rather than needing to be guessed from text:user-model,agent-model->preferenceslesson,decision->casesThis is forward-only and has zero impact on existing stores; only new rows written after this lands get the stamp.
Layer 2: read-time fallback correction
For rows that never migrate (written before this lands, or from any other source relying on the fallback),
reverseMapLegacyCategory'sdecisioncase now resolves through the same branch asfactinstead of defaulting toevents. This is a behavior change for any pre-existing row that hits this fallback with a legacydecisioncategory: it will now read ascases(orprofilefor personal-identity-shaped text, same heuristicfactalready used) instead ofevents.Layer 3: opt-in backfill
memory-pro upgradegains a--categories-onlyflag that runs a new, narrower one-shot pass (MemoryUpgrader.normalizeMappedRowCategories) to re-stampmemory_categoryon existing reflection-mapped rows using the same mapping as layer 1. It is separate from the general legacy-upgrade sweep (which deliberately excludes reflection rows), touches only that one field, and only when the value is missing or wrong. Idempotent: a row that's already correct is left alone, so a second run makes zero further changes. Supports--dry-runand--scope, matching the existing command's conventions.Testing
test/reverse-map-legacy-category.test.mjstest/reflection-mapped-category-stamping.test.mjstest/memory-upgrader-category-normalization.test.mjsAll three are registered in
package.json's test chain andscripts/ci-test-manifest.mjs'sstorage-and-schemagroup. Full local suite,tsc --noEmit, andnpm run buildall pass clean.Update (2026-07-18)
Rebased onto current master. This revision also single-sources the heading-to-taxonomy map: the metadata stamp and the stored row category both read
REFLECTION_MAPPED_MEMORY_CATEGORY, agent self-observations now map topatternsinstead of polluting userpreferences, and mapped rows mint their smart taxonomy category as the row category rather than the legacypreference/fact/decisionnames. The upgrader's mapped-row normalization re-stamps existing rows through the same map. Verified live on our fleet.