Skip to content

feat(memory): claude-mem feature parity — series of 5 PRs - #2

Closed
fazleelahhee wants to merge 2 commits into
mainfrom
feature/memory-claude-mem-parity
Closed

feat(memory): claude-mem feature parity — series of 5 PRs#2
fazleelahhee wants to merge 2 commits into
mainfrom
feature/memory-claude-mem-parity

Conversation

@fazleelahhee

Copy link
Copy Markdown
Contributor

Series

This is the umbrella draft PR for the memory ↔ claude-mem parity feature. The branch ships in 5 sequential, independently reviewable commits/PRs.

PR Status Scope
1. Foundation ✅ landed on this branch memory/db.py schema + cce sessions migrate + 10 tests
2. Capture pending 5 hooks + HTTP endpoints
3. Compress pending extractive worker (bge-small) in cce serve
4. Recall pending extend session_recall + new session_timeline, session_event MCP tools
5. Dashboard pending sessions list + timeline + decisions search

Spec

Design + decisions: docs/specs/2026-04-28-memory-claude-mem-parity-design.md on this branch.

Six brainstorming decisions locked:

  1. Capture model — auto-from-hooks + explicit MCP tools coexist (source='manual'|'auto'|'migrated')
  2. Compression timing — background worker in cce serve (per-project MCP server)
  3. Summary granularity — per-turn + per-session rollup → maps onto 3-layer progressive disclosure
  4. MCP surface — extend session_recall(topic) (back-compat); add session_timeline(session_id) and session_event(event_id)
  5. Migration — one-shot cce sessions migrate (idempotent, user-invoked)
  6. Dashboard v1 — 3 panels: sessions list, session timeline, decisions search

Compressor tiers: extractive default using BAAI/bge-small-en-v1.5 already loaded for the index. No Ollama required, no embedded LLM, no extra deps.

What's in PR 1

  • src/context_engine/memory/db.py — schema bootstrap, version table, connection helper (PRAGMA foreign_keys=ON, journal_mode=WAL)
  • src/context_engine/memory/migrate.py — idempotent JSON-to-SQLite importer (current path + legacy ~/.claude-context-engine/...)
  • cce sessions migrate CLI subcommand with --no-archive
  • tests/memory/test_db.py (5 tests), tests/memory/test_migrate.py (5 tests)
  • No behaviour change to existing JSON capture path

Test plan

  • pytest tests/memory/ — 10 new tests pass
  • Full suite: 311 passed, 1 skipped
  • CLI smoke: cce sessions migrate --help registers and routes correctly
  • Run cce sessions migrate against the sonin install once PR is reviewed (real-data shakeout)

Captures the 6 design decisions from brainstorming: auto+explicit
capture with source column, background compression worker in cce
serve, per-turn + per-session rollup granularity, extended
session_recall + new session_timeline/session_event MCP tools,
one-shot cce sessions migrate command, three dashboard panels.

Compressor is extractive using BAAI/bge-small-en-v1.5 already loaded
for the index — no new dependencies, no extra RAM, no Ollama
required.

Ships as 5 sequential PRs off this branch; each independently
reviewable.
PR 1 of 5 against feature/memory-claude-mem-parity. Lays the storage
foundation: per-project SQLite at ~/.cce/projects/<name>/memory.db
with the v1 schema (sessions, prompts, tool_events, tool_event_payloads,
turn_summaries, decisions, code_areas, pending_compressions,
migrated_files, schema_versions) plus FTS5 virtual tables and triggers
for prompts / decisions / turn_summaries.

Adds `cce sessions migrate` — idempotent one-shot importer for legacy
per-session JSON files (current path and pre-rebrand
~/.claude-context-engine/...). Imported decisions and code areas are
tagged source='migrated' so future session_recall can rank them.
Consumed JSONs are archived into sessions/migrated.zip and removed.

No behaviour change to the existing JSON capture path. Hooks and the
compression worker land in PR 2 and PR 3.

10 new unit tests cover: schema bootstrap, idempotent reconnection,
foreign-key enforcement, FTS triggers, migration of session JSON +
decisions_log archive, idempotent rerun, archive-and-remove. Full
suite: 311 passed, 1 skipped.

See docs/specs/2026-04-28-memory-claude-mem-parity-design.md.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the “Foundation” layer for memory ↔ claude-mem parity by adding a per-project SQLite memory.db (with FTS) plus an idempotent migrator and CLI command to import legacy JSON session artifacts.

Changes:

  • Add memory.db bootstrap + schema versioning (incl. FTS tables/triggers).
  • Add JSON→SQLite migrator and wire it to cce sessions migrate (with optional archiving).
  • Add unit tests covering schema bootstrap and migration behavior; add a design spec doc.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/context_engine/memory/db.py Creates/opens per-project SQLite DB and bootstraps schema + FTS.
src/context_engine/memory/migrate.py Implements idempotent importer from legacy session JSON + optional archiving.
src/context_engine/cli.py Adds cce sessions migrate CLI subcommand.
tests/memory/test_db.py Validates schema creation, FK pragma, FTS behavior, constraints.
tests/memory/test_migrate.py Validates migrate import counts, idempotency, archiving, and decisions_log shape.
src/context_engine/memory/__init__.py Package docstring describing new storage approach.
tests/memory/__init__.py Marks tests.memory as a package.
docs/specs/2026-04-28-memory-claude-mem-parity-design.md Design/spec documentation for the 5-PR series and schema/architecture.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +118 to +127
# decisions_log.json is a top-level list of decision dicts, not a session.
if source.name == _DECISIONS_LOG_NAME and isinstance(data, list):
for d in data:
_insert_decision(
conn,
session_id=None,
decision=d.get("decision", ""),
reason=d.get("reason", ""),
timestamp=d.get("timestamp"),
)

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In decisions_log.json import, each entry includes a session_id (see session_capture.prune_old_sessions), but the migrator always inserts session_id=None. This drops linkage that can be preserved when the referenced session already exists in the DB; consider using d.get('session_id') and only keeping it when _session_exists(conn, ...) is true (otherwise NULL to satisfy the FK).

Copilot uses AI. Check for mistakes.
Comment on lines +161 to +163
def _insert_decision(conn, *, session_id, decision, reason, timestamp):
epoch = int(timestamp) if timestamp else int(time.time())
iso = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(epoch))

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp is treated as falsy, so a valid 0/0.0 (which the legacy pruner writes as the default when missing) will be replaced with time.time(). This makes migrated rows look “new” and can distort ordering/ranking. Consider checking timestamp is not None (and possibly allowing 0 → epoch 0) instead of a truthiness check.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +144
session_id = data.get("id")
# We do not synthesise a sessions row from migrated data — the legacy
# JSON files predate the new sessions schema and miss timestamps in a
# form we can trust. Importing decisions with session_id=None is the
# safer choice; the FK is nullable on purpose.
for d in data.get("decisions", []) or []:
_insert_decision(
conn,
session_id=session_id if _session_exists(conn, session_id) else None,
decision=d.get("decision", ""),

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_session_exists(conn, session_id) is executed repeatedly inside the decisions/code_areas loops even though session_id is constant per file. This results in extra DB queries per imported row; compute it once (or memoize by session_id) and reuse the result for both loops.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +97
summary.files_imported += 1
_mark_imported(conn, f)
consumed.append(f)
if consumed:
consumed_per_dir[sessions_dir] = consumed

conn.commit()

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Archiving happens after conn.commit(), but files are recorded in migrated_files before archiving/removal. If _archive_and_remove raises (e.g., zip write error), the DB will be committed and the file will be permanently considered “already imported”, so reruns won’t be able to archive/delete it. Consider reordering so archive write failures don’t leave files stuck in an imported-but-unarchived state (e.g., only mark imported after successful archive, or add an archived flag and treat already-imported-but-present files as cleanup candidates).

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +147
Eight tables in `~/.cce/projects/<name>/memory.db`. Timestamps stored as
both ISO text and epoch int (text for humans, int for sorting). FTS5
virtual tables shown last.

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section says “Eight tables in memory.db”, but the implemented schema in memory/db.py includes additional tables (e.g., tool_event_payloads, migrated_files, schema_versions) plus FTS virtual tables and triggers. Please align the table count and the SQL snippet with the actual v1 schema so the spec remains a reliable reference.

Suggested change
Eight tables in `~/.cce/projects/<name>/memory.db`. Timestamps stored as
both ISO text and epoch int (text for humans, int for sorting). FTS5
virtual tables shown last.
The v1 schema in `~/.cce/projects/<name>/memory.db` includes the core
memory tables plus support tables such as `tool_event_payloads`,
`migrated_files`, and `schema_versions`, along with FTS5 virtual tables
and their triggers. Timestamps are stored as both ISO text and epoch int
(text for humans, int for sorting).

Copilot uses AI. Check for mistakes.
fazleelahhee added a commit that referenced this pull request Apr 28, 2026
Consolidates the 11 actionable items from the 5-PR memory stack into one
fixup commit on the unified ai-memory branch.

migrate.py
  - Archive *before* mark-imported + commit, with rollback on zip failure
    so a failed archive no longer leaves files stuck imported-but-not-archived.
  - Preserve session_id linkage from decisions_log.json when the referenced
    session already exists (was unconditionally NULL).
  - Memoise _session_exists per-file (constant per archive entry).
  - Use `timestamp is not None` so legacy 0/0.0 timestamps keep their original
    ordering instead of being stamped to "now".

compressor.py
  - Drop unused `Any` import.
  - Cap raw_input at _TOOL_INPUT_CHAR_CAP (4 KB) before json.loads so multi-MB
    patch payloads don't stall the compression worker.
  - Yield the asyncio loop after every drained item, with a short breath every
    5 items, so a backlog doesn't monopolise mcp.run_stdio().

mcp_server.py
  - Use FTS5 MATCH on decisions_fts / turn_summaries_fts (and a LIKE filter on
    code_areas) to prefilter recall candidates instead of embedding the latest
    600 rows on every session_recall call.
  - Clamp `limit` in session_timeline to 1..200 with a helper that handles bad
    input cleanly.
  - Wrap the sessions metadata query and the session_event payload query in
    try/except so DB errors return a specific message.
  - Handle NULL raw_input/raw_output in session_event without rendering the
    string "None".
  - Update the dual-write comment to reflect that recall now goes through FTS5.

design spec
  - Drop the inaccurate "Eight tables" sentence; describe the support and
    FTS5 tables that ship in v1 instead.

Tests: 43/43 memory + dashboard pass; 344/345 across the full suite (the
single failure is test_ollama_client hitting httpx.ReadTimeout because no
Ollama is running in this environment — unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fazleelahhee

Copy link
Copy Markdown
Contributor Author

Superseded by #7, which consolidates this 5-PR stack into a single branch (ai-memory) against main with Copilot's review feedback addressed in 353dc27. Closing — please review #7.

fazleelahhee added a commit that referenced this pull request Apr 28, 2026
Adds full conversation memory to cce as a peer feature alongside code search:
per-project SQLite store, lifecycle capture hooks, hybrid FTS5 + sqlite-vec
recall, automatic SessionStart resume injection, and a recall benchmark.

## What's in

**Foundation** — schema v2 in `~/.cce/projects/<name>/memory.db` with sessions,
prompts, tool_events, turn_summaries, decisions, code_areas, plus FTS5 virtual
tables and sqlite-vec `vec0` tables for semantic search. v1→v2 migrates in place.

**Capture** — 5 lifecycle hooks (SessionStart, UserPromptSubmit, PostToolUse,
Stop, SessionEnd) post to a loopback aiohttp server inside `cce serve`. Hook
shell script is platform-aware (cce_hook.sh on POSIX, cce_hook.cmd on Windows)
and shell-quoted correctly for both shells. Capture fails closed.

**Compress** — extractive worker drains `pending_compressions` on a thread,
using bge-small (already loaded for the index — no LLM, no API tokens).

**Recall** — `session_recall(topic)` runs hybrid retrieval: FTS5 + sqlite-vec
with reciprocal rank fusion, content-keyed dedup, threshold-tuned against an
empirical bge-small noise floor. TL;DR header (top-3 by centroid similarity)
+ 20 tagged matches with relative-time hints and drill affordances
(`→ session_timeline("<sid>")` / `→ session_event(id=N)`).

**Resume** — SessionStart hook returns last session's rollup + recent
decisions as plain text. Claude Code captures hook stdout and injects it
into the model's context at session start. Matcher is `startup|clear|compact`
so the resume re-injects after `/clear` or `/compact` too. *This is what
solves "decisions you made last week have to be re-explained today."*

**Operational** — daemon-thread vec backfill (no startup stall), payload
retention via `cce sessions prune` + automatic background `auto_prune_loop`,
cleanup triggers for orphaned vec rows, FTS stop-word filter, init-time
reachability probe, `cce sessions status` CLI command, recall benchmark
script (`scripts/bench_recall.py`).

**Tooling** — `cce sessions migrate` imports legacy JSON; idempotent;
archives consumed files. CLAUDE.md template (block v3) documents the full
MCP tool surface including `session_timeline` and `session_event`.

## Bench

R@5=0.75, P@5=0.46, MRR=0.74 on the 14-query benchmark; off-topic queries
("how is the weather today") reject cleanly. Token cost per recall:
~600-900 tokens (5-7× cheaper than competing memory tools that LLM-curate).

## Tests

149 memory + dashboard + integration + CLI tests; 387 of full suite pass.
CI green on Python 3.11 / 3.12 / 3.13.

## Stats

39 files changed, +6,663 / -189 LOC over 28 commits. Supersedes the
original 5-PR stack (#2-#6, all closed).
fazleelahhee added a commit that referenced this pull request Apr 28, 2026
Wire-up audit on main surfaced 8 items. All addressed.

#1 production: RRF dedup broken in dual-write window (live-confirmed
   duplicate in session_recall). Fixed by canonicalising _content_key
   through grammar.compress(lite).
#2 ux: dashboard returned raw compressed bytes. /api/memory/* now
   applies grammar.expand on read.
#3 cce sessions migrate now compresses on insert (homogeneous storage).
#4-7 test gaps closed.
#8 confirmed reserved-for-future column.

Tests: 603/603 (+25 new). CI green on Python 3.11 / 3.12 / 3.13.
Live-verified the dedup fix on /home/fazle/trading/v3.
@rajkumarsakthivel
rajkumarsakthivel deleted the feature/memory-claude-mem-parity branch May 19, 2026 20:08
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.

2 participants