Skip to content

feat(sessions): search over archived Claude session transcripts#103

Merged
mruwnik merged 2 commits into
masterfrom
claude/u2-e19-cb1a77bfe862acfd7702567e760b8613
Jul 2, 2026
Merged

feat(sessions): search over archived Claude session transcripts#103
mruwnik merged 2 commits into
masterfrom
claude/u2-e19-cb1a77bfe862acfd7702567e760b8613

Conversation

@mruwnik

@mruwnik mruwnik commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Closes #100.

Adds claude_session_search / claude_session_fetch MCP tools so archived Claude Code session transcripts (retained 2 years) become queryable instead of requiring exact session IDs.

What's here

  • SessionSegment SourceItem subtype — embedding-sized runs of conversational messages (tool traffic stripped) riding the standard hybrid pipeline (Qdrant + BM25 + RRF) in a new session collection, with transcript line indices pointing back into the JSONL for context windows.
  • Indexer — hourly index_stale_sessions sweep + per-session index_session task with an indexed_up_to watermark. The trailing partial segment is held back until the transcript is idle (SESSION_INDEX_MIN_IDLE_SECONDS, default 1h) so growing sessions don't produce overlapping segments. sha256 dedup makes reindexing idempotent; an embed failure halts the watermark so the slice is retried instead of silently dropped. Existing sessions backfill automatically on first sweep.
  • Owner-only, even for admins — segments carry creator_id + explicit NULL project_id; session_search prefilters source_ids by Session.user_id; and every generic surface (search, list_items, count_items, fetch, get_metadata_schemas, available-modalities) excludes the session modality entirely.
  • Retentioncleanup_old_claude_sessions now deletes segment source_item parent rows (Qdrant points are GC'd by the existing clean-all-collections sweep).
  • Acceptance criterion from the issue: "when did we discuss X?" resolves in two tool calls — claude_session_search(query)claude_session_fetch(session_id, start_index).

Review

3 rounds of differ-review (all comments resolved): round 1 fixed LIKE-wildcard escaping, lean SearchConfig (no HyDE/query-analysis LLM calls), and a cheaper hourly sweep; round 2 verified fixes, no new findings; round 3 fixed the embed-failure watermark gap, the get_metadata_schemas session leak, and a negative-limit edge. A separate adversarial pass also closed an admin-visibility hole in list_items/count_items/fetch.

Tests: 63 new tests (parser, indexing tasks, MCP tools, access-control regressions); affected suites (~1640 tests) pass; pyright + ruff clean on changed files.

🤖 Generated with Claude Code

https://claude.ai/code/session_012YX6tYnmz3K8LZg6MXfHCv

Claude and others added 2 commits July 2, 2026 18:33
Adds claude_session_search / claude_session_fetch MCP tools so archived
Claude Code session transcripts (retained 2 years) become queryable
instead of requiring exact session IDs.

- SessionSegment SourceItem subtype: embedding-sized runs of
  conversational messages (tool traffic stripped) riding the standard
  hybrid pipeline (Qdrant + BM25 + RRF) in a new "session" collection,
  with transcript line indices pointing back into the JSONL.
- Indexer: hourly index_stale_sessions sweep + per-session index_session
  task with an indexed_up_to watermark; the trailing partial segment is
  held back until the transcript is idle so growing sessions don't
  produce overlapping segments. sha256 dedup makes reindexing
  idempotent; existing sessions backfill automatically.
- Owner-only even for admins: segments carry creator_id + explicit NULL
  project_id, session_search prefilters source_ids by Session.user_id,
  and the generic search/list/count/fetch tools exclude the session
  modality entirely.
- Retention: cleanup_old_claude_sessions now deletes segment source_item
  parents (Qdrant points are GC'd by the clean-all-collections sweep).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YX6tYnmz3K8LZg6MXfHCv
Fixes ruff E402 that was failing backend CI: the imports introduced for
the archived transcript search tests were placed mid-file after the
existing test functions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YX6tYnmz3K8LZg6MXfHCv

@mruwnik mruwnik left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Heironeous (Claude Opus 4.8): Deep independent review — LGTM, no blocking issues found. This is a carefully engineered PR; the security model and the indexing state machine both hold up under scrutiny.

Access control (the risk that matters here) is sound. The owner-only-even-for-admins guarantee is enforced by owned_segment_ids scoping source_ids to Session.user_id == user_id — which holds regardless of admin * scope, since an admin's access_filter is None and would otherwise bypass everything. Defense-in-depth is real: I verified all five generic surfaces exclude SESSION_COLLECTIONSsearch, fetch, list_items/count_items (both share apply_item_filters), get_metadata_schemas, and _get_available_modalities. session_fetch re-checks session.user_id != user.id. Segments carry creator_id + explicit NULL project_id, and there's no REST search route (search is MCP-only; search_base requires explicit modalities rather than defaulting to all), so no alternate leak path. Same "Session not found" error on missing vs. non-owned = no existence leak.

Indexing state machine is correct. I traced the tricky paths:

  • The IntegrityError-on-concurrent-insert path is safe: process_content_item commits each successful segment before the loop advances, so a rollback of a later duplicate can't orphan an earlier committed segment or leave a watermark gap — the watermark is re-set and re-committed on the next successful segment.
  • The embed-failure halt (delete FAILED row, hold indexed_at stale, break) correctly forces the hourly sweep to retry the slice instead of advancing past a permanent zero-chunk gap.
  • Segmentation is deterministic from the watermark, and the sha256(content, session_id, start_index) + unique (session_id, start_index) index make reindex idempotent from both directions (existence check catches the common case, IntegrityError catches the race).
  • Tail-holdback via transcript_idle + the tail_may_be_pending requeue condition is self-terminating (stops requeuing once mtime is >IDLE before indexed_at).

Migration, retention cascade (delete source_item parents before session rows so chunks/segments cascade), LIKE-escaping, and the lean SearchConfig (no HyDE/query-analysis) all check out.

Minor / optional (none blocking):

  1. Left an inline note on owned_segment_ids re: unbounded source_ids as a future scaling concern.
  2. A SKIPPED segment (too short to embed) is counted toward created in the result dict though it produces no chunks — purely cosmetic; it's still sha256-deduped on reindex so it won't be recreated.
  3. sessions.py::extract_conversation_text (summarizer) and claude_sessions.py::parse_event/extract_text_blocks (indexer) are two independent implementations of "pull conversational text from a transcript event" that differ subtly (is_meta handling, per-message truncation, join char). Consider having the summarizer reuse the parser to avoid drift. DRY nit only.

I'll keep watching and re-review on changes.

Comment thread src/memory/api/MCP/servers/claude.py
@mruwnik

mruwnik commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Heironeous (Claude Opus 4.8): Re-reviewed at head fe0cfd2 (was f3a53bc at my first pass). The only delta is a test-only refactor: imports (json, uuid_lib, SearchResult, settings_module, CodingSession, CodingProject, SessionSegment) hoisted from inside a test body to the top of tests/memory/api/MCP/test_claude.py — matches the repo's imports-at-top convention. No production code changed; checks are green. Prior assessment stands: LGTM, no blocking issues.

@mruwnik
mruwnik merged commit 2fe9ef0 into master Jul 2, 2026
2 checks passed
@mruwnik
mruwnik deleted the claude/u2-e19-cb1a77bfe862acfd7702567e760b8613 branch July 2, 2026 21:56
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.

Search over archived Claude session transcripts

1 participant