Class: Rubino::Session::Store
- Inherits:
-
Object
- Object
- Rubino::Session::Store
- Defined in:
- lib/rubino/session/store.rb
Overview
Persists and queries messages within a session.
Ordering note: created_at is iso8601 with second precision, so multiple messages can share the same timestamp. Read/delete paths that need a strict total order break ties on the SQLite ‘rowid` column.
#last_for_role is the entry point used by retry/undo to find the last user (or assistant) turn before rewinding history.
Instance Method Summary collapse
-
#append(message) ⇒ Object
Appends a message to a session.
-
#copy_into(target_session_id, messages) ⇒ Object
Copies messages into another session preserving ALL wire-significant fields.
-
#count(session_id) ⇒ Object
Returns total message count for a session.
-
#create(session_id:, role:, content:, **attrs) ⇒ Object
Creates and appends a message from attributes.
-
#delete_from_inclusive(session_id, from_id:) ⇒ Integer
Deletes the given message and every message inserted after it.
-
#for_session(session_id, limit: nil) ⇒ Object
Returns all messages for a session in chronological order.
-
#initialize(db: nil) ⇒ Store
constructor
A new instance of Store.
-
#last_for_role(session_id, role) ⇒ Object
Returns the most recent message for ‘role` (e.g. “user”, “assistant”).
-
#recent(session_id, count:) ⇒ Object
Returns the N most recent messages for a session.
-
#search(query:, since: nil, until_: nil, role: nil, tool: nil, limit: 20) ⇒ Array<Hash>
Full-text search across messages backed by the ‘messages_fts` FTS5 virtual table (see migration 007).
-
#token_sum(session_id) ⇒ Object
Returns estimated token sum for a session.
Constructor Details
Instance Method Details
#append(message) ⇒ Object
Appends a message to a session
21 22 23 24 25 26 |
# File 'lib/rubino/session/store.rb', line 21 def append() raise SessionError, "Invalid message" unless .valid? @db[:messages].insert(.to_row) end |
#copy_into(target_session_id, messages) ⇒ Object
Copies messages into another session preserving ALL wire-significant fields. Assistant tool calls live in metadata (not tool_call_id), so dropping metadata orphans the toolUse block and 400s strict providers (Anthropic/Bedrock) on resume. token_count is copied too so the target session’s budget accounting stays accurate.
44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/rubino/session/store.rb', line 44 def copy_into(target_session_id, ) .each do |msg| create( session_id: target_session_id, role: msg.role, content: msg.content, tool_name: msg.tool_name, tool_call_id: msg.tool_call_id, token_count: msg.token_count, metadata: msg. ) end end |
#count(session_id) ⇒ Object
Returns total message count for a session
84 85 86 |
# File 'lib/rubino/session/store.rb', line 84 def count(session_id) @db[:messages].where(session_id: session_id).count end |
#create(session_id:, role:, content:, **attrs) ⇒ Object
Creates and appends a message from attributes
29 30 31 32 33 34 35 36 37 |
# File 'lib/rubino/session/store.rb', line 29 def create(session_id:, role:, content:, **attrs) = Message.new( session_id: session_id, role: role, content: content, **attrs ) append() end |
#delete_from_inclusive(session_id, from_id:) ⇒ Integer
Deletes the given message and every message inserted after it. Used by undo/retry to rewind history.
Uses tuple ordering on (created_at, rowid): rows strictly later by timestamp are removed, and ties on created_at are broken by rowid so same-second inserts are still cut at the right point.
105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/rubino/session/store.rb', line 105 def delete_from_inclusive(session_id, from_id:) msg = @db[:messages] .where(id: from_id, session_id: session_id) .select(:created_at, Sequel.lit("rowid AS row_id")) .first return 0 unless msg @db[:messages] .where(session_id: session_id) .where(Sequel.lit("(created_at > ?) OR (created_at = ? AND rowid >= ?)", msg[:created_at], msg[:created_at], msg[:row_id])) .delete end |
#for_session(session_id, limit: nil) ⇒ Object
Returns all messages for a session in chronological order. created_at is second-precision, so we tie-break on rowid — without this, an assistant preamble and a tool_result persisted in the same second can come back swapped, which makes the resumed transcript look like the tool fired before the model’s preamble (or worse, like an empty assistant box wrapping the tool).
64 65 66 67 68 69 70 |
# File 'lib/rubino/session/store.rb', line 64 def for_session(session_id, limit: nil) dataset = @db[:messages] .where(session_id: session_id) .order(:created_at, Sequel.lit("rowid")) dataset = dataset.limit(limit) if limit dataset.all.map { |row| hydrate(row) } end |
#last_for_role(session_id, role) ⇒ Object
Returns the most recent message for ‘role` (e.g. “user”, “assistant”). Tie-broken on rowid like the other read paths. Used by retry/undo.
163 164 165 166 167 168 169 |
# File 'lib/rubino/session/store.rb', line 163 def last_for_role(session_id, role) row = @db[:messages] .where(session_id: session_id, role: role) .order(Sequel.desc(:created_at), Sequel.desc(Sequel.lit("rowid"))) .first row && hydrate(row) end |
#recent(session_id, count:) ⇒ Object
Returns the N most recent messages for a session
73 74 75 76 77 78 79 80 81 |
# File 'lib/rubino/session/store.rb', line 73 def recent(session_id, count:) @db[:messages] .where(session_id: session_id) .order(Sequel.desc(:created_at), Sequel.desc(Sequel.lit("rowid"))) .limit(count) .all .reverse .map { |row| hydrate(row) } end |
#search(query:, since: nil, until_: nil, role: nil, tool: nil, limit: 20) ⇒ Array<Hash>
Full-text search across messages backed by the ‘messages_fts` FTS5 virtual table (see migration 007). Returns hydrated rows with an FTS5 snippet() highlighting the match. Filters compose on top of the FTS MATCH so the index does the heavy lifting and SQL prunes the rest.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/rubino/session/store.rb', line 132 def search(query:, since: nil, until_: nil, role: nil, tool: nil, limit: 20) return [] if query.nil? || query.to_s.strip.empty? limit = limit.to_i.clamp(1, 100) match_query = sanitize_fts_query(query) dataset = @db[:messages_fts] .where(Sequel.lit("messages_fts MATCH ?", match_query)) .join(:messages, Sequel[:messages][:rowid] => Sequel[:messages_fts][:rowid]) .select( Sequel[:messages][:id].as(:message_id), Sequel[:messages][:session_id], Sequel[:messages][:role], Sequel[:messages][:created_at], Sequel.lit("snippet(messages_fts, 0, '<mark>', '</mark>', '...', 16) AS snippet") ) dataset = dataset.where(Sequel[:messages][:role] => role) if role dataset = dataset.where(Sequel[:messages][:tool_name] => tool) if tool dataset = dataset.where(Sequel.lit("messages.created_at >= ?", since)) if since dataset = dataset.where(Sequel.lit("messages.created_at <= ?", until_)) if until_ dataset .order(Sequel.desc(Sequel[:messages][:created_at]), Sequel.desc(Sequel.lit("messages.rowid"))) .limit(limit) .all .map { |row| row.merge(run_id: nil) } end |
#token_sum(session_id) ⇒ Object
Returns estimated token sum for a session
89 90 91 92 93 |
# File 'lib/rubino/session/store.rb', line 89 def token_sum(session_id) @db[:messages] .where(session_id: session_id) .sum(:token_count) || 0 end |