Skip to content

fix(semantic): key memory_suppressions by (content_hash, scope) (#106) - #144

Merged
devinmlowe merged 3 commits into
mainfrom
fix/issue-106-scope-bypass
Sep 21, 2026
Merged

devinmlowe merged 3 commits into
mainfrom
fix/issue-106-scope-bypass

Conversation

@devinmlowe

Copy link
Copy Markdown
Owner

Fixes #106.

Summary

memory_suppressions was keyed on content_hash alone and every reader matched on the hash alone, so one tenant's forget suppressed re-extraction of the same sentence for every tenant, and any tenant's remember lifted every tenant's suppression.

  • Schema (src/_core/db/schema.ts): new checkpoint suppressions_scope_v1 rebuilds memory_suppressions with PRIMARY KEY (content_hash, scope) in one transaction, keeping every row (NULL scope → global). Idempotent via PRAGMA table_info. Fresh databases get the composite key directly from migrateForget. SCHEMA_VERSION 5 → 6; additive (an older build's INSERT OR REPLACE / WHERE content_hash = ? still run), so not in BREAKING_MIGRATIONS.
  • Readers/writers (src/semantic/forget.ts): filterSuppressedFacts(db, facts, scope) matches scope IN (?, 'global') — a global suppression is the only one that crosses tenants. clearSuppression(db, content, scope) and the restoreMemory delete match scope = ? only.
  • Callers: dropSuppressed in src/dream/daemon.ts passes getConversationScope(db, conversationId); both clearSuppression calls in src/interfaces/shared/remember.ts pass the write scope.
  • Docs: src/_core/db/README.md / SPEC.md REQ-6, src/semantic/SPEC.md REQ-10, CHANGELOG.md.

Tests

  • tests/semantic/forget.test.ts: tenant A's forget suppresses only A; B's forget of the same sentence coexists (2 rows); B's remember lifts only B; a global forget reaches every tenant; restore removes only its own scope's row.
  • tests/migration/forget-schema.test.ts: composite-key rows coexist; a 0.4.0-shaped content_hash-only table is rebuilt with rows kept, NULLglobal, idempotent re-run, checkpoint recorded.
  • tests/interfaces/cli/update.test.ts: rollback test now derives the newest checkpoint from SCHEMA_MIGRATIONS instead of hardcoding forget_v1 / 4 -> 5.
  • npm run lint clean; npm run test:run 1691 passed, 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UZykd17PEpTdhxGXqLVNKw

devinmlowe and others added 3 commits September 21, 2026 01:19
…, scope)

Baseline for the autoresearch loop: 2 failing tests proving a tenant's forget
must not suppress re-extraction for other tenants, a tenant's remember must
not lift another tenant's suppression, and rows for the same hash in two
scopes must coexist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZykd17PEpTdhxGXqLVNKw
Root cause: memory_suppressions had content_hash as its sole primary key and
every reader matched on the hash alone, so one tenant's forget overwrote and
silenced every tenant's re-extraction of the same sentence, and any tenant's
remember lifted it.

- schema: suppressions_scope_v1 rebuilds the table with PRIMARY KEY
  (content_hash, scope), keeping rows (NULL scope -> global); fresh DBs get the
  composite key directly. SCHEMA_VERSION 5 -> 6, additive (older builds still run).
- forget.ts: filterSuppressedFacts(db, facts, scope) matches scope IN (?, 'global');
  clearSuppression(db, content, scope) and restoreMemory delete only their scope's row.
- daemon.ts dropSuppressed passes getConversationScope; remember.ts passes the
  write scope.
- tests: red tests go green; rebuild path covered; update rollback test derives
  the newest checkpoint from SCHEMA_MIGRATIONS instead of hardcoding forget_v1/4->5.
- docs: db README/SPEC, semantic SPEC, CHANGELOG.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZykd17PEpTdhxGXqLVNKw
…ets in CHANGELOG

Conflict was only CHANGELOG.md (Unreleased/Fixed: #87, #109, #108 on main, #106 here). All four kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xo3z3uv6caDhR8zjrv5QF
@devinmlowe
devinmlowe merged commit 6e06edb into main Sep 21, 2026
10 of 12 checks passed
@devinmlowe
devinmlowe deleted the fix/issue-106-scope-bypass branch September 21, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant