Class: Insika::SessionStore

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

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 append_messages(id, messages)
  record = fetch!(id)
  incoming = (messages.is_a?(Hash) ? [messages] : Array(messages))
             .map { |msg| stamp(deep_stringify(msg)) }
  record["messages"] += incoming
  record["updated_at"] = timestamp
  @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).

Raises:

  • (ArgumentError)


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 = timestamp
  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_idObject

-> 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"] = timestamp
  @store.set(SCOPE, key_for(id), record)
  to_session(record)
end