Class: ClaudeMemory::Store::SQLiteStore

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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.

Parameters:

  • db_path (String)

    filesystem path to the SQLite database file



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

#dbSequel::Database (readonly)

Returns the underlying Sequel database connection.

Returns:

  • (Sequel::Database)

    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_eventsSequel::Dataset

Returns:

  • (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



231
232
233
234
235
# File 'lib/claude_memory/store/sqlite_store.rb', line 231

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.



207
208
209
210
211
# File 'lib/claude_memory/store/sqlite_store.rb', line 207

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



267
268
269
270
271
# File 'lib/claude_memory/store/sqlite_store.rb', line 267

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_walvoid

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.

Returns:

  • (Integer)

    number of rows deleted (0 or 1)



156
157
158
# File 'lib/claude_memory/store/sqlite_store.rb', line 156

def clear_moment_feedback(event_id)
  with_retry { moment_feedback.where(event_id: event_id).delete }
end

#closevoid

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

#conflictsSequel::Dataset

Returns:

  • (Sequel::Dataset)


88
# File 'lib/claude_memory/store/sqlite_store.rb', line 88

def conflicts = @db[:conflicts]

#content_item_by_transcript_and_mtime(transcript_path, mtime_iso8601) ⇒ Hash?

Find a content item by transcript path and source modification time.

Parameters:

  • transcript_path (String)

    filesystem path to the transcript

  • mtime_iso8601 (String)

    ISO 8601 modification timestamp

Returns:

  • (Hash, nil)


368
369
370
371
372
# File 'lib/claude_memory/store/sqlite_store.rb', line 368

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_itemsSequel::Dataset

Returns:

  • (Sequel::Dataset)


67
# File 'lib/claude_memory/store/sqlite_store.rb', line 67

def content_items = @db[:content_items]

#delta_cursorsSequel::Dataset

Returns:

  • (Sequel::Dataset)


70
# File 'lib/claude_memory/store/sqlite_store.rb', line 70

def delta_cursors = @db[:delta_cursors]

#entitiesSequel::Dataset

Returns:

  • (Sequel::Dataset)


73
# File 'lib/claude_memory/store/sqlite_store.rb', line 73

def entities = @db[:entities]

#entity_aliasesSequel::Dataset

Returns:

  • (Sequel::Dataset)


76
# File 'lib/claude_memory/store/sqlite_store.rb', line 76

def entity_aliases = @db[:entity_aliases]

Returns:

  • (Sequel::Dataset)


85
# File 'lib/claude_memory/store/sqlite_store.rb', line 85

def fact_links = @db[:fact_links]

#factsSequel::Dataset

Returns:

  • (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.

Parameters:

  • subject_entity_id (Integer)

    subject entity id

  • predicate (String)

    predicate label

  • status (String) (defaults to: "active")

    filter by status (default: “active”)

Returns:

  • (Array<Hash>)


591
592
593
594
595
596
597
598
# File 'lib/claude_memory/store/sqlite_store.rb', line 591

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.

Parameters:

  • limit (Integer) (defaults to: 1000)

    maximum rows to return

Returns:

  • (Array<Hash>)

    fact rows with :id, :subject_entity_id, :predicate, :object_literal, :embedding_json, :scope



576
577
578
579
580
581
582
583
# File 'lib/claude_memory/store/sqlite_store.rb', line 576

def facts_with_embeddings(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.

Parameters:

  • docid (String)

    8-character hex document id

Returns:

  • (Hash, nil)


494
495
496
# File 'lib/claude_memory/store/sqlite_store.rb', line 494

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.

Parameters:

  • type (String)

    entity type (e.g. “database”, “framework”, “person”)

  • name (String)

    canonical entity name

Returns:

  • (Integer)

    entity row id



442
443
444
445
446
447
448
449
# File 'lib/claude_memory/store/sqlite_store.rb', line 442

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.

Parameters:

  • id (Integer)

    content item id

Returns:

  • (Hash, nil)


360
361
362
# File 'lib/claude_memory/store/sqlite_store.rb', line 360

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.

Parameters:

  • session_id (String)

    session identifier

  • transcript_path (String)

    transcript file path

Returns:

  • (Integer, nil)

    byte offset, or nil if no cursor exists



412
413
414
# File 'lib/claude_memory/store/sqlite_store.rb', line 412

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.

Parameters:

  • key (String)

    metadata key

Returns:

  • (String, nil)


696
697
698
# File 'lib/claude_memory/store/sqlite_store.rb', line 696

def get_meta(key)
  @db[:meta].where(key: key).get(:value)
end

#ingestion_metricsSequel::Dataset

Returns:

  • (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.

Parameters:

  • fact_a_id (Integer)

    first conflicting fact id

  • fact_b_id (Integer)

    second conflicting fact id

  • status (String) (defaults to: "open")

    conflict status (“open” or “resolved”)

  • notes (String, nil) (defaults to: nil)

    human-readable notes about the conflict

Returns:

  • (Integer)

    inserted conflict row id



640
641
642
643
644
645
646
647
648
649
# File 'lib/claude_memory/store/sqlite_store.rb', line 640

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.

Parameters:

  • subject_entity_id (Integer)

    entity id for the subject

  • predicate (String)

    predicate label (e.g. “uses_database”, “depends_on”)

  • object_entity_id (Integer, nil) (defaults to: nil)

    entity id for the object (if entity-valued)

  • object_literal (String, nil) (defaults to: nil)

    literal value for the object

  • datatype (String, nil) (defaults to: nil)

    datatype hint for the object literal

  • polarity (String) (defaults to: "positive")

    “positive” or “negative”

  • valid_from (String, nil) (defaults to: nil)

    ISO 8601 validity start (defaults to now UTC)

  • status (String) (defaults to: "active")

    fact status (“active”, “superseded”, “rejected”)

  • confidence (Float) (defaults to: 1.0)

    confidence score 0.0..1.0

  • created_from (String, nil) (defaults to: nil)

    provenance tag (e.g. “promoted:path:id”)

  • scope (String) (defaults to: "project")

    “global” or “project”

  • project_path (String, nil) (defaults to: nil)

    project directory for project-scoped facts

Returns:

  • (Integer)

    inserted fact row id



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/claude_memory/store/sqlite_store.rb', line 468

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

Create a directional link between two facts (e.g. supersession).

Parameters:

  • from_fact_id (Integer)

    source fact id

  • to_fact_id (Integer)

    target fact id

  • link_type (String)

    relationship type (e.g. “supersedes”, “conflicts_with”)

Returns:

  • (Integer)

    inserted fact_link row id



662
663
664
# File 'lib/claude_memory/store/sqlite_store.rb', line 662

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.

Parameters:

  • tool_name (String)

    name of the MCP tool invoked

  • duration_ms (Integer)

    execution time in milliseconds

  • result_count (Integer, nil) (defaults to: nil)

    number of results returned

  • scope (String, nil) (defaults to: nil)

    “global” or “project”

  • error_class (String, nil) (defaults to: nil)

    error class name if the call failed

  • called_at (String, nil) (defaults to: nil)

    ISO 8601 timestamp (defaults to now UTC)

Returns:

  • (Integer)

    inserted row id



171
172
173
174
175
176
177
178
179
180
# File 'lib/claude_memory/store/sqlite_store.rb', line 171

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_otel_event(event_name:, occurred_at:, session_id: nil, prompt_id: nil, attributes: nil, resource: nil) ⇒ Integer

Insert one OTel log-style event row.

Parameters:

  • event_name (String)

    e.g. “user_prompt”, “tool_result”, “api_request”

  • occurred_at (String)

    ISO 8601 timestamp

  • session_id (String, nil) (defaults to: nil)
  • prompt_id (String, nil) (defaults to: nil)

    UUID correlating events from one prompt

  • attributes (Hash, nil) (defaults to: nil)
  • resource (Hash, nil) (defaults to: nil)

Returns:

  • (Integer)

    inserted row id



222
223
224
225
226
227
228
229
# File 'lib/claude_memory/store/sqlite_store.rb', line 222

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.

Parameters:

  • name (String)

    OTel metric name (e.g. “claude_code.token.usage”)

  • value_type (String)

    “int” or “double”

  • value_int (Integer, nil) (defaults to: nil)

    integer value when value_type == “int”

  • value_float (Float, nil) (defaults to: nil)

    float value when value_type == “double”

  • unit (String, nil) (defaults to: nil)

    OTel unit string (“tokens”, “USD”, “s”, …)

  • attributes (Hash, nil) (defaults to: nil)

    flattened attribute map

  • resource (Hash, nil) (defaults to: nil)

    resource attribute map

  • recorded_at (String)

    ISO 8601 timestamp

Returns:

  • (Integer)

    inserted row id



195
196
197
198
199
200
201
202
# File 'lib/claude_memory/store/sqlite_store.rb', line 195

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?.

Parameters:

  • trace_id (String)
  • span_id (String)
  • name (String)
  • recorded_at (String)
  • parent_span_id (String, nil) (defaults to: nil)
  • session_id (String, nil) (defaults to: nil)
  • prompt_id (String, nil) (defaults to: nil)
  • start_unix_nano (Integer, nil) (defaults to: nil)
  • end_unix_nano (Integer, nil) (defaults to: nil)
  • duration_ms (Integer, nil) (defaults to: nil)
  • status_code (String, nil) (defaults to: nil)
  • attributes (Hash, nil) (defaults to: nil)
  • resource (Hash, nil) (defaults to: nil)

Returns:

  • (Integer)

    inserted row id



254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/claude_memory/store/sqlite_store.rb', line 254

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.

Parameters:

  • fact_id (Integer)

    fact row id

  • content_item_id (Integer, nil) (defaults to: nil)

    source content item id

  • quote (String, nil) (defaults to: nil)

    verbatim quote from the source

  • attribution_entity_id (Integer, nil) (defaults to: nil)

    entity who stated the fact

  • strength (String) (defaults to: "stated")

    evidence strength (“stated”, “inferred”, “derived”)

  • line_start (Integer, nil) (defaults to: nil)

    starting line in source content

  • line_end (Integer, nil) (defaults to: nil)

    ending line in source content

Returns:

  • (Integer)

    inserted provenance row id



612
613
614
615
616
617
618
619
620
621
622
623
# File 'lib/claude_memory/store/sqlite_store.rb', line 612

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.

Parameters:

  • content_item_id (Integer)

    owning content item id

  • tool_calls_data (Array<Hash>)

    tool call hashes with keys :tool_name, :tool_input, :tool_result, :compressed_summary, :is_error, :timestamp



382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/claude_memory/store/sqlite_store.rb', line 382

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_cacheSequel::Dataset

Returns:

  • (Sequel::Dataset)


103
# File 'lib/claude_memory/store/sqlite_store.rb', line 103

def llm_cache = @db[:llm_cache]

#mcp_tool_callsSequel::Dataset

Returns:

  • (Sequel::Dataset)


106
# File 'lib/claude_memory/store/sqlite_store.rb', line 106

def mcp_tool_calls = @db[:mcp_tool_calls]

#moment_feedbackSequel::Dataset

Returns:

  • (Sequel::Dataset)


112
# File 'lib/claude_memory/store/sqlite_store.rb', line 112

def moment_feedback = @db[:moment_feedback]

#open_conflictsArray<Hash>

Retrieve all unresolved conflicts.

Returns:

  • (Array<Hash>)


653
654
655
# File 'lib/claude_memory/store/sqlite_store.rb', line 653

def open_conflicts
  conflicts.where(status: "open").all
end

#operation_progressSequel::Dataset

Returns:

  • (Sequel::Dataset)


94
# File 'lib/claude_memory/store/sqlite_store.rb', line 94

def operation_progress = @db[:operation_progress]

#otel_eventsSequel::Dataset

Returns:

  • (Sequel::Dataset)


118
# File 'lib/claude_memory/store/sqlite_store.rb', line 118

def otel_events = @db[:otel_events]

#otel_metricsSequel::Dataset

Returns:

  • (Sequel::Dataset)


115
# File 'lib/claude_memory/store/sqlite_store.rb', line 115

def otel_metrics = @db[:otel_metrics]

#otel_tracesSequel::Dataset

Returns:

  • (Sequel::Dataset)


121
# File 'lib/claude_memory/store/sqlite_store.rb', line 121

def otel_traces = @db[:otel_traces]

#provenanceSequel::Dataset

Returns:

  • (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.

Parameters:

  • fact_id (Integer)

    fact row id

Returns:

  • (Array<Hash>)


628
629
630
# File 'lib/claude_memory/store/sqlite_store.rb', line 628

def provenance_for_fact(fact_id)
  provenance.where(fact_id: fact_id).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.

Parameters:

  • fact_id (Integer)

    fact row id to reject

  • reason (String, nil) (defaults to: nil)

    optional rejection reason appended to conflict notes

Returns:

  • (Hash, nil)

    {rejected: true, conflicts_resolved: Integer} or nil if the fact does not exist



546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/claude_memory/store/sqlite_store.rb', line 546

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_healthSequel::Dataset

Returns:

  • (Sequel::Dataset)


97
# File 'lib/claude_memory/store/sqlite_store.rb', line 97

def schema_health = @db[:schema_health]

#schema_versionInteger?

Current schema version stored in the meta table.

Returns:

  • (Integer, nil)


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).

Parameters:

  • key (String)

    metadata key

  • value (String)

    metadata value



689
690
691
# File 'lib/claude_memory/store/sqlite_store.rb', line 689

def set_meta(key, value)
  @db[:meta].insert_conflict(target: :key, update: {value: value}).insert(key: key, value: value)
end

#tool_callsSequel::Dataset

Returns:

  • (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.

Parameters:

  • content_item_id (Integer)

    content item id

Returns:

  • (Array<Hash>)


399
400
401
402
403
404
# File 'lib/claude_memory/store/sqlite_store.rb', line 399

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.

Parameters:

  • limit (Integer) (defaults to: 3)

    maximum rows to return

  • min_length (Integer) (defaults to: 200)

    minimum byte_len threshold

Returns:

  • (Array<Hash>)


672
673
674
675
676
677
678
679
680
681
# File 'lib/claude_memory/store/sqlite_store.rb', line 672

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.

Parameters:

  • session_id (String)

    session identifier

  • transcript_path (String)

    transcript file path

  • offset (Integer)

    new byte offset



421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/claude_memory/store/sqlite_store.rb', line 421

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.

Parameters:

  • fact_id (Integer)

    fact row id

  • status (String, nil) (defaults to: nil)

    new status value

  • valid_to (String, nil) (defaults to: nil)

    ISO 8601 end-of-validity timestamp

  • scope (String, nil) (defaults to: nil)

    “global” or “project”

  • project_path (String, nil) (defaults to: nil)

    project directory (cleared when scope is “global”)

  • embedding (Array<Float>, nil) (defaults to: nil)

    embedding vector to store as JSON

Returns:

  • (Boolean)

    true if any fields were updated, false if all args were nil



509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
# File 'lib/claude_memory/store/sqlite_store.rb', line 509

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 embedding
    updates[:embedding_json] = embedding.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.

Parameters:

  • fact_id (Integer)

    fact row id

  • embedding_vector (Array<Float>)

    embedding to store as JSON



533
534
535
# File 'lib/claude_memory/store/sqlite_store.rb', line 533

def update_fact_embedding(fact_id, embedding_vector)
  facts.where(id: fact_id).update(embedding_json: embedding_vector.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.

Parameters:

  • source (String)

    origin type (e.g. “transcript”, “hook”)

  • text_hash (String)

    SHA-256 hex digest of the raw text

  • byte_len (Integer)

    byte length of the raw text

  • session_id (String, nil) (defaults to: nil)

    Claude Code session identifier

  • transcript_path (String, nil) (defaults to: nil)

    filesystem path to the transcript file

  • project_path (String, nil) (defaults to: nil)

    project directory path

  • occurred_at (String, nil) (defaults to: nil)

    ISO 8601 timestamp (defaults to now UTC)

  • raw_text (String, nil) (defaults to: nil)

    original text content

  • metadata (Hash, nil) (defaults to: nil)

    additional metadata stored as JSON

  • git_branch (String, nil) (defaults to: nil)

    active git branch at ingestion time

  • cwd (String, nil) (defaults to: nil)

    working directory at ingestion time

  • claude_version (String, nil) (defaults to: nil)

    Claude Code version string

  • thinking_level (String, nil) (defaults to: nil)

    thinking level setting

  • source_mtime (String, nil) (defaults to: nil)

    ISO 8601 mtime of the source file

Returns:

  • (Integer)

    content item row id (existing or newly inserted)



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/claude_memory/store/sqlite_store.rb', line 329

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.

Parameters:

  • event_id (Integer)

    activity_events row id

  • verdict (String)

    “up” or “down”

  • note (String, nil) (defaults to: nil)

    optional freeform note

  • recorded_at (String, nil) (defaults to: nil)

    ISO 8601 timestamp (defaults to now UTC)

Returns:

  • (Hash)

    row after upsert

Raises:

  • (ArgumentError)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/claude_memory/store/sqlite_store.rb', line 132

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_indexIndex::VectorIndex

Lazily-initialized vector index for semantic search.

Returns:



47
48
49
# File 'lib/claude_memory/store/sqlite_store.rb', line 47

def vector_index
  @vector_index ||= Index::VectorIndex.new(self)
end