Class: Insika::SessionStore
- Inherits:
-
Object
- Object
- Insika::SessionStore
- Includes:
- Coercion
- Defined in:
- lib/insika/session_store.rb
Overview
Domain store for sessions. Persists transcript + vars
over an injected Insika::Store, with a fixed schema
session:<id> in the "sessions" scope.
The persisted transcript is the SOURCE OF TRUTH for reconstruction; live
events are just delivery state. The message shape
({"role"=>, "content"=>}, role ∈ user|assistant|system|tool) is the
same one Runner#seed_history already consumes — the Executor converts nothing.
Normalizes symbol→string on WRITE (the backend only guarantees round-trip of JSON types); READ returns the data as it comes from the backend (string keys), never symmetrizing back to symbols.
Defined Under Namespace
Classes: Session
Constant Summary collapse
- SCOPE =
"sessions"- KEY_PREFIX =
"session:"
Instance Method Summary collapse
-
#append_messages(id, messages) ⇒ Object
-> Session (transcript += messages).
-
#create(id: SecureRandom.uuid, vars: {}) ⇒ Object
-> Session; ArgumentError if id already exists (a duplicate session is a domain violation — it never overwrites silently).
-
#delete(id) ⇒ Object
-> bool (delegates to the backend: false for a nonexistent id).
-
#each_id ⇒ Object
-> enumerates ids without the "session:" prefix.
-
#find(id) ⇒ Object
-> Session | nil.
-
#initialize(store:) ⇒ SessionStore
constructor
store: any Insika::Store (Memory, SQLite, ...) — injected by the composition root (config/wiring.rb).
-
#update_vars(id, vars) ⇒ Object
-> Session (SHALLOW merge: an existing nested key is replaced wholesale, not merged).
Methods included from Coercion
blank?, deep_stringify, presence, present?, utf8
Constructor Details
#initialize(store:) ⇒ SessionStore
store: any Insika::Store (Memory, SQLite, ...) — injected by the composition root (config/wiring.rb). SessionStore does not know the concrete backend.
31 32 33 |
# File 'lib/insika/session_store.rb', line 31 def initialize(store:) @store = store end |
Instance Method Details
#append_messages(id, messages) ⇒ Object
-> Session (transcript += messages). Read-modify-write on the task's own fiber, without a lock. Each message gets an "at" (ISO8601 UTC) if not provided. NotFoundError if the session does not exist.
CONCURRENCY LIMITATION (§11 R2c): the RMW (read record -> += -> set) is atomic ONLY because the SessionActor serializes turns of the same session (one owner at a time). That serialization exists solely in SUPERVISED mode (the actor loop lives on the supervisor). Two concurrent send_message on the same session_id OUTSIDE that path (e.g. calling append_messages directly, or a non-supervised deployment) would interleave read/set and LOSE messages — there is no compare-and-swap here. Route same-session writes through the SessionActor; see session_actor.rb.
72 73 74 75 76 77 78 79 80 |
# File 'lib/insika/session_store.rb', line 72 def (id, ) record = fetch!(id) incoming = (.is_a?(Hash) ? [] : Array()) .map { |msg| stamp(deep_stringify(msg)) } record["messages"] += incoming record["updated_at"] = @store.set(SCOPE, key_for(id), record) to_session(record) end |
#create(id: SecureRandom.uuid, vars: {}) ⇒ Object
-> Session; ArgumentError if id already exists (a duplicate session is a domain violation — it never overwrites silently).
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/insika/session_store.rb', line 37 def create(id: SecureRandom.uuid, vars: {}) key = key_for(id) raise ArgumentError, "session already exists: #{id}" unless @store.get(SCOPE, key).nil? now = record = { "id" => id.to_s, "messages" => [], "vars" => deep_stringify(vars), "memory_refs" => [], "created_at" => now, "updated_at" => now } @store.set(SCOPE, key, record) to_session(record) end |
#delete(id) ⇒ Object
-> bool (delegates to the backend: false for a nonexistent id)
93 94 95 |
# File 'lib/insika/session_store.rb', line 93 def delete(id) @store.delete(SCOPE, key_for(id)) end |
#each_id ⇒ Object
-> enumerates ids without the "session:" prefix. Without a block, returns an Enumerator.
99 100 101 102 103 104 105 |
# File 'lib/insika/session_store.rb', line 99 def each_id return enum_for(:each_id) unless block_given? @store.list(SCOPE, KEY_PREFIX).each do |key| yield key.delete_prefix(KEY_PREFIX) end end |
#find(id) ⇒ Object
-> Session | nil
55 56 57 58 |
# File 'lib/insika/session_store.rb', line 55 def find(id) record = @store.get(SCOPE, key_for(id)) record && to_session(record) end |
#update_vars(id, vars) ⇒ Object
-> Session (SHALLOW merge: an existing nested key is replaced wholesale, not merged). NotFoundError if absent.
84 85 86 87 88 89 90 |
# File 'lib/insika/session_store.rb', line 84 def update_vars(id, vars) record = fetch!(id) record["vars"] = record["vars"].merge(deep_stringify(vars)) record["updated_at"] = @store.set(SCOPE, key_for(id), record) to_session(record) end |