Class: RubynCode::Memory::SessionPersistence

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/memory/session_persistence.rb

Overview

Saves and restores full conversation sessions to SQLite, enabling session continuity across process restarts and session browsing.

Constant Summary collapse

JSON_ATTRS =
%i[metadata messages].freeze
SIMPLE_ATTRS =
%i[title status model].freeze

Instance Method Summary collapse

Constructor Details

#initialize(db) ⇒ SessionPersistence

Returns a new instance of SessionPersistence.

Parameters:



12
13
14
15
16
17
18
19
# File 'lib/rubyn_code/memory/session_persistence.rb', line 12

def initialize(db)
  @db = db
  # Per-session journal bookkeeping: how many messages are already
  # persisted and their object identities, so append-only saves can be
  # detected without comparing message contents.
  @journal_state = {}
  ensure_table
end

Instance Method Details

#delete_session(session_id) ⇒ void

This method returns an undefined value.

Deletes a session permanently.

Parameters:

  • session_id (String)


117
118
119
120
# File 'lib/rubyn_code/memory/session_persistence.rb', line 117

def delete_session(session_id)
  clear_journal(session_id)
  @db.execute('DELETE FROM sessions WHERE id = ?', [session_id])
end

#list_sessions(project_path: nil, status: nil, limit: 20) ⇒ Array<Hash>

Lists sessions, optionally filtered by project and/or status.

Parameters:

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

    filter by project

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

    filter by status (“active”, “archived”, “deleted”)

  • limit (Integer) (defaults to: 20)

    maximum results (default 20)

Returns:

  • (Array<Hash>)

    session summaries (without full messages)



79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/rubyn_code/memory/session_persistence.rb', line 79

def list_sessions(project_path: nil, status: nil, limit: 20)
  where_clause, params = build_list_filters(project_path, status)
  params << limit

  rows = @db.query(<<~SQL, params).to_a
    SELECT id, project_path, title, model, status, metadata, created_at, updated_at
    FROM sessions
    #{where_clause}
    ORDER BY updated_at DESC
    LIMIT ?
  SQL

  rows.map { |row| row_to_session_summary(row) }
end

#load_session(session_id) ⇒ Hash?

Loads a session by ID.

Parameters:

  • session_id (String)

Returns:

  • (Hash, nil)

    { messages:, metadata:, title:, model:, status:, project_path: } or nil



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/rubyn_code/memory/session_persistence.rb', line 51

def load_session(session_id)
  rows = @db.query(
    'SELECT * FROM sessions WHERE id = ?',
    [session_id]
  ).to_a
  return nil if rows.empty?

  row = rows.first
  blob_messages = parse_json_array(row['messages'])
  blob_messages += journal_messages(session_id) if blob_messages.is_a?(Array)
  {
    messages: blob_messages,
    metadata: parse_json_hash(row['metadata']),
    title: row['title'],
    model: row['model'],
    status: row['status'],
    project_path: row['project_path'],
    created_at: row['created_at'],
    updated_at: row['updated_at']
  }
end

#save_session(session_id:, project_path:, messages:, **opts) ⇒ void

This method returns an undefined value.

Persists a complete session snapshot.

Hot-path friendly: when the messages array has only grown since the last save for this session, the new messages are appended to the messages journal table instead of rewriting the whole JSON blob. The blob is only rewritten when history was replaced (compaction, undo, resume) or on the first save of a process.

Parameters:

  • attrs (Hash)

    session attributes: :session_id, :project_path, :messages (required); :title, :model, :metadata (optional)



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rubyn_code/memory/session_persistence.rb', line 33

def save_session(session_id:, project_path:, messages:, **opts)
  if appendable?(session_id, messages)
    append_to_journal(session_id, messages, opts)
  else
    snapshot_session(session_id, project_path, messages, opts)
  end
  remember_journal_state(session_id, messages)
rescue StandardError
  # Journal append can fail (e.g. role CHECK constraint on legacy
  # schemas) — fall back to a full snapshot.
  snapshot_session(session_id, project_path, messages, opts)
  remember_journal_state(session_id, messages)
end

#update_session(session_id, **attrs) ⇒ void

This method returns an undefined value.

Updates session attributes.

Parameters:

  • session_id (String)
  • attrs (Hash)

    attributes to update (:title, :status, :model, :metadata, :messages)



99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/rubyn_code/memory/session_persistence.rb', line 99

def update_session(session_id, **attrs)
  return if attrs.empty?

  sets, params = build_update_clauses(attrs)
  return if sets.empty?

  sets << 'updated_at = ?'
  params << Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')
  params << session_id

  @db.execute("UPDATE sessions SET #{sets.join(', ')} WHERE id = ?", params)
  clear_journal(session_id) if attrs.key?(:messages)
end