Skip to content

fix: three setup-run bugs — degenerate causal edges, SQLite grooming surface, legacy memory import - #163

Merged
cdeust merged 4 commits into
mainfrom
fix/setup-run-bugs
Jul 22, 2026
Merged

fix: three setup-run bugs — degenerate causal edges, SQLite grooming surface, legacy memory import#163
cdeust merged 4 commits into
mainfrom
fix/setup-run-bugs

Conversation

@cdeust

@cdeust cdeust commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Three bugs hit by a real setup run on a large store (30k memories / 23k entities), each fixed at root cause with a repro test that failed pre-fix:

1. causal_graph.py unpack crashentities.name has no UNIQUE constraint, so duplicate names reach pc_skeleton, where frozenset((a, b)) collapses a duplicated pair to a 1-element edge; X-vs-X is never independent so the degenerate edge survives to a, b = sorted(edge). Fixed at the source: discover_causal_edges establishes PC's distinct-variables precondition (order-preserving dedup) before skeleton learning — duplicates were also fabricating unshielded (X, X, Z) triples for collider orientation.

2. SqliteMemoryStore grooming surfaceget_grooming_ages existed only on the PG stats mixin; on the (now default) SQLite backend memory_stats and get_grooming_health crashed. New SQLite implementations translate the jsonb tag queries via json_each; wiki age is an honest documented degradation (always None — SQLite has no wiki table, and None already means "never recorded" to every consumer). The sqlite_store.py docstring falsely claiming full PG parity is corrected with the measured 18-method gap inventory.

3. Legacy memory import silently brokenscanner._parse_memory_file indexed the FrontmatterResult NamedTuple as a dict (parsed["meta"]), a Node.js-port drift: every projects/*/memory/*.md parse raised TypeError, swallowed per-file, so setup imported zero legacy memory files while appearing to succeed.

Known follow-up (out of scope, inventoried): lesson_promotion handler still PG-only on SQLite.

Test plan

  • Repro tests added for all three (verified failing pre-fix): causal duplicate-names pair, 10-test SQLite grooming suite, SQLite memory_stats, legacy scanner fixtures
  • tests_py/core 2787 passed · infrastructure 550 passed · handlers/hooks 115 passed (live PG + model)
  • ruff check + format clean (972 files)
  • CI green

🤖 Generated with Claude Code

cdeust and others added 4 commits July 22, 2026 21:21
…e unpack crash

Symptom: 'ValueError: not enough values to unpack (expected 2, got 1)'
at causal_graph.py:166 (a, b = sorted(edge)) during a real setup run on
a 23k-entity store.

Root cause: entities.name carries no UNIQUE constraint (pg_schema.py /
sqlite_schema.py), so cls.py's row-to-name flattening can hand
discover_causal_edges the same name twice. pc_skeleton seeds the
complete graph with frozenset((a, b)) over combinations(variables, 2);
a duplicated name collapses to a degenerate 1-element frozenset that
survives skeleton learning (X vs X is never marginally independent) and
crashes the 2-tuple unpack in the consumption loop. Duplicates also
hand orient_v_structures fake unshielded (X, X, Z) triples.

Fix at the source: establish the PC distinct-variables precondition
once, at the discover_causal_edges boundary (order-preserving
dict.fromkeys), before either pc_skeleton or orient_v_structures sees
the list — no guard at the unpack site. pc_skeleton's docstring now
states the precondition.

Repro test: duplicated name over an exact-independence grid (the config
that lets the degenerate edge survive to the unpack) + a regression
guard that dedup keeps genuine edges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ite backend

Symptom: 'AttributeError: SqliteMemoryStore has no attribute
get_grooming_ages' on a real 30k-memory setup run — get_grooming_ages
existed only on PgStatsMixin, so on the SQLite backend (the plugin
default) memory_stats crashed outright and get_grooming_health crashed
twice (ages, then the PG-only jsonb promotion count against the compat
connection).

Fix:
- sqlite_store_grooming.py (new): SqliteGroomingMixin.get_grooming_ages,
  same contract as the PG twin. distillation/promotion are the honest
  json_each translation of the jsonb tag queries; wiki is a documented
  degradation (always None — the tended index, wiki.pages, is PG-only
  and the SQLite schema has no wiki table; None already means
  never-recorded to every consumer).
- sqlite_store_lesson_promotion.py (new): SQLite twin of the promotion
  eligibility count (jsonb operators have no mechanical translation in
  sqlite_compat.py, so the dialect needs its own module).
- get_grooming_health.py: composition root now dispatches the promotion
  count on store type instead of running the PG SQL unconditionally.
- sqlite_store.py: wire the mixin; correct the stale 'all 89 methods'
  docstring claim with measured numbers (117 shared / 18 PG-only,
  inspect diff 2026-07-22).

Call-site status: memory_stats and get_grooming_health now work
end-to-end on SQLite; hooks/session_start needed no change (its SQLite
banner path deliberately skips grooming staleness, and the PG path
already degrades to [] on error).

Repro tests: memory_stats handler on an in-memory SqliteMemoryStore
(pre-fix AttributeError) + grooming-age/count contract tests +
handler dispatch test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es unreadable

Symptom: '[methodology-agent] Failed to read <...>/memory/<file>.md:
tuple indices must be integers or slices, not str' for every legacy
memory .md file during setup/import.

Root cause: shared/yaml_parser.parse_yaml_frontmatter returns a
FrontmatterResult NamedTuple, but scanner._parse_memory_file indexed it
with string keys (parsed["meta"] / parsed["body"] — drift from the
Node.js port, where that was object access). Every parse raised
TypeError; discover_all_memories' per-file except swallowed it to
stderr, so setup runs silently imported zero memory files. The other
consumer (wiki_purge) already uses attribute access.

Fix at the consumption site to honor the declared NamedTuple contract:
parsed.meta / parsed.body.

Repro tests: synthetic legacy memory fixture under
projects/<p>/memory/ with and without YAML frontmatter — pre-fix both
return [] (error swallowed), post-fix the parsed record round-trips
name/description/type/body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e_store docstring shift

The 03e8ff6 docstring correction added 6 lines above the three known
heat_base UPDATE sites in sqlite_store.py; the writers are unchanged,
only their coordinates moved (441/471/535 -> 447/477/541). Verified the
new lines are the same bump_heat_raw / update_memories_heat_batch
statements the ADR-cited allowlist covers. Full invariants suite: 39 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cdeust
cdeust merged commit 66218cd into main Jul 22, 2026
14 checks passed
@cdeust
cdeust deleted the fix/setup-run-bugs branch July 22, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant