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]

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



147
148
149
# File 'lib/claude_memory/store/sqlite_store.rb', line 147

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)


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

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


455
456
457
458
459
460
461
462
# File 'lib/claude_memory/store/sqlite_store.rb', line 455

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



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

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)


358
359
360
# File 'lib/claude_memory/store/sqlite_store.rb', line 358

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



306
307
308
309
310
311
312
313
# File 'lib/claude_memory/store/sqlite_store.rb', line 306

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)


224
225
226
# File 'lib/claude_memory/store/sqlite_store.rb', line 224

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



276
277
278
# File 'lib/claude_memory/store/sqlite_store.rb', line 276

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)


560
561
562
# File 'lib/claude_memory/store/sqlite_store.rb', line 560

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



504
505
506
507
508
509
510
511
512
513
# File 'lib/claude_memory/store/sqlite_store.rb', line 504

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



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

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



526
527
528
# File 'lib/claude_memory/store/sqlite_store.rb', line 526

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



162
163
164
165
166
167
168
169
170
171
# File 'lib/claude_memory/store/sqlite_store.rb', line 162

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_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



476
477
478
479
480
481
482
483
484
485
486
487
# File 'lib/claude_memory/store/sqlite_store.rb', line 476

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



246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/claude_memory/store/sqlite_store.rb', line 246

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


517
518
519
# File 'lib/claude_memory/store/sqlite_store.rb', line 517

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]

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


492
493
494
# File 'lib/claude_memory/store/sqlite_store.rb', line 492

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



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/claude_memory/store/sqlite_store.rb', line 410

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



553
554
555
# File 'lib/claude_memory/store/sqlite_store.rb', line 553

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


263
264
265
266
267
268
# File 'lib/claude_memory/store/sqlite_store.rb', line 263

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


536
537
538
539
540
541
542
543
544
545
# File 'lib/claude_memory/store/sqlite_store.rb', line 536

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



285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/claude_memory/store/sqlite_store.rb', line 285

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



373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/claude_memory/store/sqlite_store.rb', line 373

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



397
398
399
# File 'lib/claude_memory/store/sqlite_store.rb', line 397

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)



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/claude_memory/store/sqlite_store.rb', line 193

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)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/claude_memory/store/sqlite_store.rb', line 123

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