Class: ClaudeMemory::Store::SQLiteStore
- Inherits:
-
Object
- Object
- ClaudeMemory::Store::SQLiteStore
- Includes:
- LLMCache, MetricsAggregator, RetryHandler, SchemaManager
- Defined in:
- lib/claude_memory/store/sqlite_store.rb
Overview
SQLite-backed fact store for ClaudeMemory. Manages all database tables (content_items, entities, facts, provenance, conflicts, fact_links, etc.) via Sequel with Extralite adapter. Includes RetryHandler for transient lock recovery and SchemaManager for automatic migrations on open.
Constant Summary
Constants included from SchemaManager
ClaudeMemory::Store::SchemaManager::SCHEMA_VERSION
Constants included from RetryHandler
RetryHandler::MAX_RETRIES, RetryHandler::RETRY_BASE_DELAY
Instance Attribute Summary collapse
-
#db ⇒ Sequel::Database
readonly
The underlying Sequel database connection.
Instance Method Summary collapse
- #activity_events ⇒ Sequel::Dataset
- #bulk_insert_otel_events(rows) ⇒ Object
-
#bulk_insert_otel_metrics(rows) ⇒ Object
Bulk insert OTel metric rows in a single SQL statement.
- #bulk_insert_otel_traces(rows) ⇒ Object
-
#checkpoint_wal ⇒ void
Checkpoint the WAL file to prevent unlimited growth.
-
#clear_moment_feedback(event_id) ⇒ Integer
Remove the verdict for a moment, if any.
-
#close ⇒ void
Disconnect from the database.
- #conflicts ⇒ Sequel::Dataset
-
#consolidate_observations(from_ids, body:, kind: "event", priority: 3, scope: "project", project_path: nil, source_content_item_id: nil, observed_at: nil) ⇒ Hash?
Semantic consolidation: merge several related observations into one synthesized observation, atomically.
-
#content_item_by_transcript_and_mtime(transcript_path, mtime_iso8601) ⇒ Hash?
Find a content item by transcript path and source modification time.
- #content_items ⇒ Sequel::Dataset
- #delta_cursors ⇒ Sequel::Dataset
- #entities ⇒ Sequel::Dataset
- #entity_aliases ⇒ Sequel::Dataset
-
#expire_observation(observation_id) ⇒ Boolean
Retire a stale observation (status “expired”) without a consolidation target.
- #fact_links ⇒ Sequel::Dataset
- #facts ⇒ Sequel::Dataset
-
#facts_for_slot(subject_entity_id, predicate, status: "active") ⇒ Array<Hash>
Find all facts for a given subject + predicate combination (a “slot”).
-
#facts_with_embeddings(limit: 1000) ⇒ Array<Hash>
Retrieve active facts that have stored embeddings.
-
#find_fact_by_docid(docid) ⇒ Hash?
Look up a fact by its short document identifier.
-
#find_or_create_entity(type:, name:) ⇒ Integer
Find an entity by its slug or create a new one.
-
#get_content_item(id) ⇒ Hash?
Fetch a single content item by primary key.
-
#get_delta_cursor(session_id, transcript_path) ⇒ Integer?
Get the last-read byte offset for a session/transcript pair.
-
#get_meta(key) ⇒ String?
Retrieve a value from the meta table.
-
#increment_corroboration(observation_id, by: 1) ⇒ void
Fold a duplicate’s sighting count into the keeper.
- #ingestion_metrics ⇒ Sequel::Dataset
-
#initialize(db_path) ⇒ SQLiteStore
constructor
Open (or create) a SQLite database and migrate to the current schema.
-
#insert_conflict(fact_a_id:, fact_b_id:, status: "open", notes: nil) ⇒ Integer
Record a conflict between two facts.
-
#insert_fact(subject_entity_id:, predicate:, object_entity_id: nil, object_literal: nil, datatype: nil, polarity: "positive", valid_from: nil, status: "active", confidence: 1.0, created_from: nil, scope: "project", project_path: nil) ⇒ Integer
Insert a new fact (subject-predicate-object triple) with an auto-generated docid.
-
#insert_fact_link(from_fact_id:, to_fact_id:, link_type:) ⇒ Integer
Create a directional link between two facts (e.g. supersession).
-
#insert_mcp_tool_call(tool_name:, duration_ms:, result_count: nil, scope: nil, error_class: nil, called_at: nil) ⇒ Integer
Record a single MCP tool invocation for telemetry.
-
#insert_observation(body:, kind: "event", priority: 3, scope: "project", project_path: nil, source_content_item_id: nil, session_id: nil, observed_at: nil, token_count: nil) ⇒ Integer
Insert an episodic observation.
-
#insert_otel_event(event_name:, occurred_at:, session_id: nil, prompt_id: nil, attributes: nil, resource: nil) ⇒ Integer
Insert one OTel log-style event row.
-
#insert_otel_metric(name:, value_type:, recorded_at:, value_int: nil, value_float: nil, unit: nil, attributes: nil, resource: nil) ⇒ Integer
Insert one OTel metric data point.
-
#insert_otel_trace_span(trace_id:, span_id:, name:, recorded_at:, parent_span_id: nil, session_id: nil, prompt_id: nil, start_unix_nano: nil, end_unix_nano: nil, duration_ms: nil, status_code: nil, attributes: nil, resource: nil) ⇒ Integer
Insert one OTel trace span row.
-
#insert_provenance(fact_id:, content_item_id: nil, quote: nil, attribution_entity_id: nil, strength: "stated", line_start: nil, line_end: nil) ⇒ Integer
Record a provenance link between a fact and its source evidence.
-
#insert_tool_calls(content_item_id, tool_calls_data) ⇒ void
Bulk-insert tool call records for a content item.
- #llm_cache ⇒ Sequel::Dataset
-
#mark_observation_promoted(observation_id, fact_id:) ⇒ Boolean
Mark an observation as promoted to a structured fact.
- #mcp_tool_calls ⇒ Sequel::Dataset
- #moment_feedback ⇒ Sequel::Dataset
- #observations ⇒ Sequel::Dataset
-
#observations_for_fact(fact_id) ⇒ Array<Hash>
Observations that were promoted into the given fact — the reverse of promoted_fact_id, for fact→observation provenance.
-
#open_conflicts ⇒ Array<Hash>
Retrieve all unresolved conflicts.
- #operation_progress ⇒ Sequel::Dataset
- #otel_events ⇒ Sequel::Dataset
- #otel_metrics ⇒ Sequel::Dataset
- #otel_traces ⇒ Sequel::Dataset
-
#promotion_candidates(scope: nil, min_corroboration: 2, limit: 10) ⇒ Array<Hash>
Active, not-yet-promoted observations corroborated at least ‘min_corroboration` times — i.e.
- #provenance ⇒ Sequel::Dataset
-
#provenance_for_fact(fact_id) ⇒ Array<Hash>
Retrieve all provenance records for a given fact.
-
#recent_observations(scope: nil, limit: 20, min_priority: nil) ⇒ Array<Hash>
Fetch active observations, newest first.
-
#reject_fact(fact_id, reason: nil) ⇒ Hash?
Reject a fact as incorrect (e.g. a distiller hallucination).
- #schema_health ⇒ Sequel::Dataset
-
#schema_version ⇒ Integer?
Current schema version stored in the meta table.
-
#set_meta(key, value) ⇒ void
Set a key-value pair in the meta table (upsert).
-
#tombstone_observation(observation_id, into_id:) ⇒ Boolean
Tombstone an observation by pointing it at the consolidated row that replaced it (append-only supersession — the row is preserved, not deleted, mirroring fact_links).
- #tool_calls ⇒ Sequel::Dataset
-
#tool_calls_for_content_item(content_item_id) ⇒ Array<Hash>
Retrieve tool calls for a content item, ordered by timestamp.
-
#undistilled_content_items(limit: 3, min_length: 200) ⇒ Array<Hash>
Fetch content items that have not yet been distilled, ordered newest first.
-
#update_delta_cursor(session_id, transcript_path, offset) ⇒ void
Create or update the byte-offset cursor for a session/transcript pair.
-
#update_fact(fact_id, status: nil, valid_to: nil, scope: nil, project_path: nil, embedding: nil) ⇒ Boolean
Selectively update one or more fields on a fact.
-
#update_fact_embedding(fact_id, embedding_vector) ⇒ void
Overwrite the embedding vector for a fact.
-
#upsert_content_item(source:, text_hash:, byte_len:, session_id: nil, transcript_path: nil, project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil, git_branch: nil, cwd: nil, claude_version: nil, thinking_level: nil, source_mtime: nil) ⇒ Integer
Insert a content item or return the existing id if a duplicate (same text_hash + session_id) already exists.
-
#upsert_moment_feedback(event_id:, verdict:, note: nil, recorded_at: nil) ⇒ Hash
Upsert a thumbs-up/down verdict for a moment.
-
#vector_index ⇒ Index::VectorIndex
Lazily-initialized vector index for semantic search.
Methods included from MetricsAggregator
#aggregate_ingestion_metrics, #backfill_distillation_metrics!, #count_undistilled, #record_ingestion_metrics
Methods included from LLMCache
#llm_cache_key, #llm_cache_lookup, #llm_cache_prune, #llm_cache_store
Methods included from RetryHandler
#transaction_with_retry, #with_retry
Constructor Details
#initialize(db_path) ⇒ SQLiteStore
Open (or create) a SQLite database and migrate to the current schema.
32 33 34 35 36 37 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 32 def initialize(db_path) @db_path = db_path @db = connect_database(db_path) ensure_schema! end |
Instance Attribute Details
#db ⇒ Sequel::Database (readonly)
Returns the underlying Sequel database connection.
28 29 30 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 28 def db @db end |
Instance Method Details
#activity_events ⇒ Sequel::Dataset
109 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 109 def activity_events = @db[:activity_events] |
#bulk_insert_otel_events(rows) ⇒ Object
234 235 236 237 238 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 234 def bulk_insert_otel_events(rows) return 0 if rows.empty? otel_events.multi_insert(rows.map { |r| otel_event_row(**r) }) rows.size end |
#bulk_insert_otel_metrics(rows) ⇒ Object
Bulk insert OTel metric rows in a single SQL statement. Hot-path callers (the OTLP receiver) batch dozens of points per request; multi_insert avoids the per-row prepare/bind overhead.
210 211 212 213 214 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 210 def bulk_insert_otel_metrics(rows) return 0 if rows.empty? otel_metrics.multi_insert(rows.map { |r| otel_metric_row(**r) }) rows.size end |
#bulk_insert_otel_traces(rows) ⇒ Object
270 271 272 273 274 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 270 def bulk_insert_otel_traces(rows) return 0 if rows.empty? otel_traces.multi_insert(rows.map { |r| otel_trace_row(**r) }) rows.size end |
#checkpoint_wal ⇒ void
This method returns an undefined value.
Checkpoint the WAL file to prevent unlimited growth.
53 54 55 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 53 def checkpoint_wal @db.run("PRAGMA wal_checkpoint(TRUNCATE)") end |
#clear_moment_feedback(event_id) ⇒ Integer
Remove the verdict for a moment, if any.
159 160 161 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 159 def clear_moment_feedback(event_id) with_retry { moment_feedback.where(event_id: event_id).delete } end |
#close ⇒ void
This method returns an undefined value.
Disconnect from the database.
41 42 43 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 41 def close @db.disconnect end |
#conflicts ⇒ Sequel::Dataset
88 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 88 def conflicts = @db[:conflicts] |
#consolidate_observations(from_ids, body:, kind: "event", priority: 3, scope: "project", project_path: nil, source_content_item_id: nil, observed_at: nil) ⇒ Hash?
Semantic consolidation: merge several related observations into one synthesized observation, atomically. The new row carries the summed corroboration of its sources (combined sighting weight, which can tip it over the promotion threshold); each source is tombstoned into it. This is the Claude-as-reflector counterpart to the deterministic dedup — it collapses observations that say the same thing in different words, which exact-match dedup can’t.
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 803 def consolidate_observations(from_ids, body:, kind: "event", priority: 3, scope: "project", project_path: nil, source_content_item_id: nil, observed_at: nil) with_retry("consolidate_observations") do @db.transaction do # Read the source set *inside* the transaction so the rows we sum # corroboration from are the same rows we tombstone — otherwise two # reflectors (PreCompact + SessionEnd) could each read the same # active sources and double-count or re-tombstone them. sources = observations .where(id: from_ids, status: "active", scope: scope) .select(:id, :corroboration_count) .all next nil if sources.size < 2 now = Time.now.utc.iso8601 combined = sources.sum { |s| s[:corroboration_count] || 1 } new_id = observations.insert( body: body, kind: kind, priority: priority, scope: scope, project_path: project_path, source_content_item_id: source_content_item_id, token_count: (body.length / 4.0).ceil, corroboration_count: combined, status: "active", observed_at: observed_at || now, created_at: now ) # Re-assert `active` on the update so a source consolidated by a # racing writer between read and write is not tombstoned twice. observations.where(id: sources.map { |s| s[:id] }, status: "active") .update(status: "consolidated", consolidated_into: new_id, reflected_at: now) {id: new_id, merged: sources.size, corroboration_count: combined} end end end |
#content_item_by_transcript_and_mtime(transcript_path, mtime_iso8601) ⇒ Hash?
Find a content item by transcript path and source modification time.
371 372 373 374 375 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 371 def content_item_by_transcript_and_mtime(transcript_path, mtime_iso8601) content_items .where(transcript_path: transcript_path, source_mtime: mtime_iso8601) .first end |
#content_items ⇒ Sequel::Dataset
67 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 67 def content_items = @db[:content_items] |
#delta_cursors ⇒ Sequel::Dataset
70 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 70 def delta_cursors = @db[:delta_cursors] |
#entities ⇒ Sequel::Dataset
73 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 73 def entities = @db[:entities] |
#entity_aliases ⇒ Sequel::Dataset
76 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 76 def entity_aliases = @db[:entity_aliases] |
#expire_observation(observation_id) ⇒ Boolean
Retire a stale observation (status “expired”) without a consolidation target. Append-only — the row is preserved for provenance, just excluded from active recall. Used by the Reflector’s TTL pass.
759 760 761 762 763 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 759 def expire_observation(observation_id) now = Time.now.utc.iso8601 updated = observations.where(id: observation_id).update(status: "expired", reflected_at: now) updated > 0 end |
#fact_links ⇒ Sequel::Dataset
85 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 85 def fact_links = @db[:fact_links] |
#facts ⇒ Sequel::Dataset
79 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 79 def facts = @db[:facts] |
#facts_for_slot(subject_entity_id, predicate, status: "active") ⇒ Array<Hash>
Find all facts for a given subject + predicate combination (a “slot”). Used by the resolver to detect supersession and conflicts.
594 595 596 597 598 599 600 601 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 594 def facts_for_slot(subject_entity_id, predicate, status: "active") facts .where(subject_entity_id: subject_entity_id, predicate: predicate, status: status) .select(:id, :subject_entity_id, :predicate, :object_entity_id, :object_literal, :datatype, :polarity, :valid_from, :valid_to, :status, :confidence, :created_from, :created_at) .all end |
#facts_with_embeddings(limit: 1000) ⇒ Array<Hash>
Retrieve active facts that have stored embeddings.
579 580 581 582 583 584 585 586 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 579 def (limit: 1000) facts .where(Sequel.~(embedding_json: nil)) .where(status: "active") .select(:id, :subject_entity_id, :predicate, :object_literal, :embedding_json, :scope) .limit(limit) .all end |
#find_fact_by_docid(docid) ⇒ Hash?
Look up a fact by its short document identifier.
497 498 499 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 497 def find_fact_by_docid(docid) facts.where(docid: docid).first end |
#find_or_create_entity(type:, name:) ⇒ Integer
Find an entity by its slug or create a new one.
445 446 447 448 449 450 451 452 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 445 def find_or_create_entity(type:, name:) slug = slugify(type, name) existing = entities.where(slug: slug).get(:id) return existing if existing now = Time.now.utc.iso8601 entities.insert(type: type, canonical_name: name, slug: slug, created_at: now) end |
#get_content_item(id) ⇒ Hash?
Fetch a single content item by primary key.
363 364 365 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 363 def get_content_item(id) content_items.where(id: id).first end |
#get_delta_cursor(session_id, transcript_path) ⇒ Integer?
Get the last-read byte offset for a session/transcript pair.
415 416 417 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 415 def get_delta_cursor(session_id, transcript_path) delta_cursors.where(session_id: session_id, transcript_path: transcript_path).get(:last_byte_offset) end |
#get_meta(key) ⇒ String?
Retrieve a value from the meta table.
877 878 879 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 877 def (key) @db[:meta].where(key: key).get(:value) end |
#increment_corroboration(observation_id, by: 1) ⇒ void
This method returns an undefined value.
Fold a duplicate’s sighting count into the keeper. Called by the Reflector’s dedup pass so corroboration survives consolidation — the signal the promotion gate keys off.
772 773 774 775 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 772 def increment_corroboration(observation_id, by: 1) observations.where(id: observation_id) .update(corroboration_count: Sequel[:corroboration_count] + by) end |
#ingestion_metrics ⇒ Sequel::Dataset
100 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 100 def ingestion_metrics = @db[:ingestion_metrics] |
#insert_conflict(fact_a_id:, fact_b_id:, status: "open", notes: nil) ⇒ Integer
Record a conflict between two facts.
643 644 645 646 647 648 649 650 651 652 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 643 def insert_conflict(fact_a_id:, fact_b_id:, status: "open", notes: nil) now = Time.now.utc.iso8601 conflicts.insert( fact_a_id: fact_a_id, fact_b_id: fact_b_id, status: status, detected_at: now, notes: notes ) end |
#insert_fact(subject_entity_id:, predicate:, object_entity_id: nil, object_literal: nil, datatype: nil, polarity: "positive", valid_from: nil, status: "active", confidence: 1.0, created_from: nil, scope: "project", project_path: nil) ⇒ Integer
Insert a new fact (subject-predicate-object triple) with an auto-generated docid.
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 471 def insert_fact(subject_entity_id:, predicate:, object_entity_id: nil, object_literal: nil, datatype: nil, polarity: "positive", valid_from: nil, status: "active", confidence: 1.0, created_from: nil, scope: "project", project_path: nil) now = Time.now.utc.iso8601 docid = generate_docid(subject_entity_id, predicate, object_literal, now) facts.insert( subject_entity_id: subject_entity_id, predicate: predicate, object_entity_id: object_entity_id, object_literal: object_literal, datatype: datatype, polarity: polarity, valid_from: valid_from || now, status: status, confidence: confidence, created_from: created_from, created_at: now, scope: scope, project_path: project_path, docid: docid ) end |
#insert_fact_link(from_fact_id:, to_fact_id:, link_type:) ⇒ Integer
Create a directional link between two facts (e.g. supersession).
665 666 667 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 665 def insert_fact_link(from_fact_id:, to_fact_id:, link_type:) fact_links.insert(from_fact_id: from_fact_id, to_fact_id: to_fact_id, link_type: link_type) end |
#insert_mcp_tool_call(tool_name:, duration_ms:, result_count: nil, scope: nil, error_class: nil, called_at: nil) ⇒ Integer
Record a single MCP tool invocation for telemetry. Inserts synchronously; callers wrap in with_retry at the call site if needed.
174 175 176 177 178 179 180 181 182 183 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 174 def insert_mcp_tool_call(tool_name:, duration_ms:, result_count: nil, scope: nil, error_class: nil, called_at: nil) mcp_tool_calls.insert( tool_name: tool_name, called_at: called_at || Time.now.utc.iso8601, duration_ms: duration_ms, result_count: result_count, scope: scope, error_class: error_class ) end |
#insert_observation(body:, kind: "event", priority: 3, scope: "project", project_path: nil, source_content_item_id: nil, session_id: nil, observed_at: nil, token_count: nil) ⇒ Integer
Insert an episodic observation. token_count is estimated from the body when not supplied (rough ~4 chars/token) so Phase 2 budget math has a value to work with.
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 702 def insert_observation(body:, kind: "event", priority: 3, scope: "project", project_path: nil, source_content_item_id: nil, session_id: nil, observed_at: nil, token_count: nil) now = Time.now.utc.iso8601 with_retry("insert_observation") do observations.insert( body: body, kind: kind, priority: priority, scope: scope, project_path: project_path, source_content_item_id: source_content_item_id, token_count: token_count || (body.length / 4.0).ceil, status: "active", session_id: session_id, observed_at: observed_at || now, created_at: now ) end end |
#insert_otel_event(event_name:, occurred_at:, session_id: nil, prompt_id: nil, attributes: nil, resource: nil) ⇒ Integer
Insert one OTel log-style event row.
225 226 227 228 229 230 231 232 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 225 def insert_otel_event(event_name:, occurred_at:, session_id: nil, prompt_id: nil, attributes: nil, resource: nil) otel_events.insert(otel_event_row( event_name: event_name, occurred_at: occurred_at, session_id: session_id, prompt_id: prompt_id, attributes: attributes, resource: resource )) end |
#insert_otel_metric(name:, value_type:, recorded_at:, value_int: nil, value_float: nil, unit: nil, attributes: nil, resource: nil) ⇒ Integer
Insert one OTel metric data point. Two value columns let us preserve int64 precision for counters (token counts) without losing fidelity in Float — see migration 018.
198 199 200 201 202 203 204 205 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 198 def insert_otel_metric(name:, value_type:, recorded_at:, value_int: nil, value_float: nil, unit: nil, attributes: nil, resource: nil) otel_metrics.insert(otel_metric_row( name: name, value_type: value_type, recorded_at: recorded_at, value_int: value_int, value_float: value_float, unit: unit, attributes: attributes, resource: resource )) end |
#insert_otel_trace_span(trace_id:, span_id:, name:, recorded_at:, parent_span_id: nil, session_id: nil, prompt_id: nil, start_unix_nano: nil, end_unix_nano: nil, duration_ms: nil, status_code: nil, attributes: nil, resource: nil) ⇒ Integer
Insert one OTel trace span row. Only used when traces are explicitly opted in via Configuration#otel_traces_enabled?.
257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 257 def insert_otel_trace_span(trace_id:, span_id:, name:, recorded_at:, parent_span_id: nil, session_id: nil, prompt_id: nil, start_unix_nano: nil, end_unix_nano: nil, duration_ms: nil, status_code: nil, attributes: nil, resource: nil) otel_traces.insert(otel_trace_row( trace_id: trace_id, span_id: span_id, name: name, recorded_at: recorded_at, parent_span_id: parent_span_id, session_id: session_id, prompt_id: prompt_id, start_unix_nano: start_unix_nano, end_unix_nano: end_unix_nano, duration_ms: duration_ms, status_code: status_code, attributes: attributes, resource: resource )) end |
#insert_provenance(fact_id:, content_item_id: nil, quote: nil, attribution_entity_id: nil, strength: "stated", line_start: nil, line_end: nil) ⇒ Integer
Record a provenance link between a fact and its source evidence.
615 616 617 618 619 620 621 622 623 624 625 626 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 615 def insert_provenance(fact_id:, content_item_id: nil, quote: nil, attribution_entity_id: nil, strength: "stated", line_start: nil, line_end: nil) provenance.insert( fact_id: fact_id, content_item_id: content_item_id, quote: quote, attribution_entity_id: attribution_entity_id, strength: strength, line_start: line_start, line_end: line_end ) end |
#insert_tool_calls(content_item_id, tool_calls_data) ⇒ void
This method returns an undefined value.
Bulk-insert tool call records for a content item.
385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 385 def insert_tool_calls(content_item_id, tool_calls_data) tool_calls_data.each do |tc| tool_calls.insert( content_item_id: content_item_id, tool_name: tc[:tool_name], tool_input: tc[:tool_input], tool_result: tc[:tool_result], compressed_summary: tc[:compressed_summary], is_error: tc[:is_error] || false, timestamp: tc[:timestamp] ) end end |
#llm_cache ⇒ Sequel::Dataset
103 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 103 def llm_cache = @db[:llm_cache] |
#mark_observation_promoted(observation_id, fact_id:) ⇒ Boolean
Mark an observation as promoted to a structured fact. Append-only: the row is preserved (provenance), it just stops being a promotion candidate.
784 785 786 787 788 789 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 784 def mark_observation_promoted(observation_id, fact_id:) now = Time.now.utc.iso8601 updated = observations.where(id: observation_id) .update(promoted_at: now, promoted_fact_id: fact_id, reflected_at: now) updated > 0 end |
#mcp_tool_calls ⇒ Sequel::Dataset
106 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 106 def mcp_tool_calls = @db[:mcp_tool_calls] |
#moment_feedback ⇒ Sequel::Dataset
112 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 112 def moment_feedback = @db[:moment_feedback] |
#observations ⇒ Sequel::Dataset
124 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 124 def observations = @db[:observations] |
#observations_for_fact(fact_id) ⇒ Array<Hash>
Observations that were promoted into the given fact — the reverse of promoted_fact_id, for fact→observation provenance.
857 858 859 860 861 862 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 857 def observations_for_fact(fact_id) observations .where(promoted_fact_id: fact_id) .select(:id, :body, :kind, :corroboration_count, :observed_at) .all end |
#open_conflicts ⇒ Array<Hash>
Retrieve all unresolved conflicts.
656 657 658 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 656 def open_conflicts conflicts.where(status: "open").all end |
#operation_progress ⇒ Sequel::Dataset
94 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 94 def operation_progress = @db[:operation_progress] |
#otel_events ⇒ Sequel::Dataset
118 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 118 def otel_events = @db[:otel_events] |
#otel_metrics ⇒ Sequel::Dataset
115 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 115 def otel_metrics = @db[:otel_metrics] |
#otel_traces ⇒ Sequel::Dataset
121 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 121 def otel_traces = @db[:otel_traces] |
#promotion_candidates(scope: nil, min_corroboration: 2, limit: 10) ⇒ Array<Hash>
Active, not-yet-promoted observations corroborated at least ‘min_corroboration` times — i.e. eligible for promotion to a fact. Highest corroboration first.
843 844 845 846 847 848 849 850 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 843 def promotion_candidates(scope: nil, min_corroboration: 2, limit: 10) ds = observations.where(status: "active", promoted_at: nil) ds = ds.where(scope: scope) if scope ds.where { corroboration_count >= min_corroboration } .order(Sequel.desc(:corroboration_count), Sequel.desc(:observed_at)) .limit(limit) .all end |
#provenance ⇒ Sequel::Dataset
82 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 82 def provenance = @db[:provenance] |
#provenance_for_fact(fact_id) ⇒ Array<Hash>
Retrieve all provenance records for a given fact.
631 632 633 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 631 def provenance_for_fact(fact_id) provenance.where(fact_id: fact_id).all end |
#recent_observations(scope: nil, limit: 20, min_priority: nil) ⇒ Array<Hash>
Fetch active observations, newest first. Used by the memory.observations MCP tool and (later) the stable-prefix injection.
731 732 733 734 735 736 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 731 def recent_observations(scope: nil, limit: 20, min_priority: nil) ds = observations.where(status: "active") ds = ds.where(scope: scope) if scope ds = ds.where { priority <= min_priority } if min_priority ds.order(Sequel.desc(:observed_at), Sequel.desc(:id)).limit(limit).all end |
#reject_fact(fact_id, reason: nil) ⇒ Hash?
Reject a fact as incorrect (e.g. a distiller hallucination). Sets status to “rejected”, closes any open conflicts involving the fact, and records the reason in conflict notes when provided. All updates run in a single transaction.
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 549 def reject_fact(fact_id, reason: nil) row = facts.where(id: fact_id).first return nil unless row now = Time.now.utc.iso8601 resolved = 0 @db.transaction do facts.where(id: fact_id).update(status: "rejected", valid_to: now) open_conflict_rows = conflicts .where(status: "open") .where { (fact_a_id =~ fact_id) | (fact_b_id =~ fact_id) } .all open_conflict_rows.each do |conflict| suffix = reason ? " | resolved: rejected fact #{fact_id} (#{reason})" : " | resolved: rejected fact #{fact_id}" notes = "#{conflict[:notes]}#{suffix}" conflicts.where(id: conflict[:id]).update(status: "resolved", notes: notes) end resolved = open_conflict_rows.size end {rejected: true, conflicts_resolved: resolved} end |
#schema_health ⇒ Sequel::Dataset
97 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 97 def schema_health = @db[:schema_health] |
#schema_version ⇒ Integer?
Current schema version stored in the meta table.
59 60 61 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 59 def schema_version @db[:meta].where(key: "schema_version").get(:value)&.to_i end |
#set_meta(key, value) ⇒ void
This method returns an undefined value.
Set a key-value pair in the meta table (upsert).
870 871 872 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 870 def (key, value) @db[:meta].insert_conflict(target: :key, update: {value: value}).insert(key: key, value: value) end |
#tombstone_observation(observation_id, into_id:) ⇒ Boolean
Tombstone an observation by pointing it at the consolidated row that replaced it (append-only supersession — the row is preserved, not deleted, mirroring fact_links). Used by the Reflector.
745 746 747 748 749 750 751 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 745 def tombstone_observation(observation_id, into_id:) now = Time.now.utc.iso8601 updated = observations.where(id: observation_id).update( status: "consolidated", consolidated_into: into_id, reflected_at: now ) updated > 0 end |
#tool_calls ⇒ Sequel::Dataset
91 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 91 def tool_calls = @db[:tool_calls] |
#tool_calls_for_content_item(content_item_id) ⇒ Array<Hash>
Retrieve tool calls for a content item, ordered by timestamp.
402 403 404 405 406 407 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 402 def tool_calls_for_content_item(content_item_id) tool_calls .where(content_item_id: content_item_id) .order(:timestamp) .all end |
#undistilled_content_items(limit: 3, min_length: 200) ⇒ Array<Hash>
Fetch content items that have not yet been distilled, ordered newest first.
675 676 677 678 679 680 681 682 683 684 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 675 def undistilled_content_items(limit: 3, min_length: 200) content_items .left_join(:ingestion_metrics, content_item_id: :id) .where(Sequel[:ingestion_metrics][:id] => nil) .where { byte_len >= min_length } .order(Sequel.desc(:occurred_at)) .limit(limit) .select_all(:content_items) .all end |
#update_delta_cursor(session_id, transcript_path, offset) ⇒ void
This method returns an undefined value.
Create or update the byte-offset cursor for a session/transcript pair.
424 425 426 427 428 429 430 431 432 433 434 435 436 437 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 424 def update_delta_cursor(session_id, transcript_path, offset) now = Time.now.utc.iso8601 delta_cursors .insert_conflict( target: [:session_id, :transcript_path], update: {last_byte_offset: offset, updated_at: now} ) .insert( session_id: session_id, transcript_path: transcript_path, last_byte_offset: offset, updated_at: now ) end |
#update_fact(fact_id, status: nil, valid_to: nil, scope: nil, project_path: nil, embedding: nil) ⇒ Boolean
Selectively update one or more fields on a fact. Only provided (non-nil) keyword arguments are written. Setting scope to “global” automatically clears project_path.
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 512 def update_fact(fact_id, status: nil, valid_to: nil, scope: nil, project_path: nil, embedding: nil) updates = {} updates[:status] = status if status updates[:valid_to] = valid_to if valid_to if scope updates[:scope] = scope updates[:project_path] = (scope == "global") ? nil : project_path end if updates[:embedding_json] = .to_json end return false if updates.empty? facts.where(id: fact_id).update(updates) true end |
#update_fact_embedding(fact_id, embedding_vector) ⇒ void
This method returns an undefined value.
Overwrite the embedding vector for a fact.
536 537 538 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 536 def (fact_id, ) facts.where(id: fact_id).update(embedding_json: .to_json) end |
#upsert_content_item(source:, text_hash:, byte_len:, session_id: nil, transcript_path: nil, project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil, git_branch: nil, cwd: nil, claude_version: nil, thinking_level: nil, source_mtime: nil) ⇒ Integer
Insert a content item or return the existing id if a duplicate (same text_hash + session_id) already exists. Wrapped in retry logic.
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 332 def upsert_content_item(source:, text_hash:, byte_len:, session_id: nil, transcript_path: nil, project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil, git_branch: nil, cwd: nil, claude_version: nil, thinking_level: nil, source_mtime: nil) with_retry("upsert_content_item") do existing = content_items.where(text_hash: text_hash, session_id: session_id).get(:id) return existing if existing now = Time.now.utc.iso8601 content_items.insert( source: source, session_id: session_id, transcript_path: transcript_path, project_path: project_path, occurred_at: occurred_at || now, ingested_at: now, text_hash: text_hash, byte_len: byte_len, raw_text: raw_text, metadata_json: &.to_json, git_branch: git_branch, cwd: cwd, claude_version: claude_version, thinking_level: thinking_level, source_mtime: source_mtime ) end end |
#upsert_moment_feedback(event_id:, verdict:, note: nil, recorded_at: nil) ⇒ Hash
Upsert a thumbs-up/down verdict for a moment. One row per event_id (unique constraint on the column) — repeat clicks overwrite. Returns the persisted row.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 135 def upsert_moment_feedback(event_id:, verdict:, note: nil, recorded_at: nil) raise ArgumentError, "verdict must be 'up' or 'down'" unless %w[up down].include?(verdict) ts = recorded_at || Time.now.utc.iso8601 with_retry do @db.transaction do existing = moment_feedback.where(event_id: event_id).first if existing moment_feedback.where(id: existing[:id]).update( verdict: verdict, note: note, recorded_at: ts ) moment_feedback.where(id: existing[:id]).first else id = moment_feedback.insert( event_id: event_id, verdict: verdict, note: note, recorded_at: ts ) moment_feedback.where(id: id).first end end end end |
#vector_index ⇒ Index::VectorIndex
Lazily-initialized vector index for semantic search.
47 48 49 |
# File 'lib/claude_memory/store/sqlite_store.rb', line 47 def vector_index @vector_index ||= Index::VectorIndex.new(self) end |