Class: Crimson::SessionManager

Inherits:
Object
  • Object
show all
Defined in:
lib/crimson/session_manager.rb

Overview

JSONL-based session persistence manager. Sessions are stored as per-directory JSONL files with a header entry.

Constant Summary collapse

CURRENT_SESSION_VERSION =

Current session file format version.

1

Instance Method Summary collapse

Constructor Details

#initialize(sessions_dir: nil) ⇒ SessionManager

Returns a new instance of SessionManager.

Parameters:

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

    base directory for session storage



19
20
21
# File 'lib/crimson/session_manager.rb', line 19

def initialize(sessions_dir: nil)
  @sessions_dir = sessions_dir || File.join(Crimson::CONFIG_DIR, "sessions")
end

Instance Method Details

#append(session_id, cwd:, entry:) ⇒ void

This method returns an undefined value.

Append an entry to a session.

Parameters:

  • session_id (String)
  • cwd (String)

    working directory

  • entry (SessionEntry)


91
92
93
94
95
# File 'lib/crimson/session_manager.rb', line 91

def append(session_id, cwd:, entry:)
  file = session_file(session_id, cwd: cwd)
  FileUtils.mkdir_p(File.dirname(file))
  File.open(file, "a") { |f| f.puts(entry.to_json) }
end

#create(cwd:, parent_session: nil) ⇒ String

Create a new session and return its ID.

Parameters:

  • cwd (String)

    working directory for the session

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

    optional parent session ID

Returns:

  • (String)

    session ID



27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/crimson/session_manager.rb', line 27

def create(cwd:, parent_session: nil)
  id = SecureRandom.uuid
  FileUtils.mkdir_p(session_dir(cwd: cwd))
  header = {
    type: "session_header",
    version: CURRENT_SESSION_VERSION,
    id: id,
    timestamp: Time.now.utc.iso8601,
    cwd: cwd,
    parentSession: parent_session
  }
  File.write(session_file(id, cwd: cwd), JSON.generate(header) + "\n")
  id
end

#delete(session_id, cwd:) ⇒ void

This method returns an undefined value.

Delete a session file.

Parameters:

  • session_id (String)
  • cwd (String)

    working directory



196
197
198
199
# File 'lib/crimson/session_manager.rb', line 196

def delete(session_id, cwd:)
  file = session_file(session_id, cwd: cwd)
  File.delete(file) if File.exist?(file)
end

#dir_hash(cwd:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Compute a short directory hash for session folder naming.



208
209
210
# File 'lib/crimson/session_manager.rb', line 208

def dir_hash(cwd:)
  Digest::SHA256.hexdigest(cwd)[0, 12]
end

#fork(session_id, cwd:, from_entry_id:) ⇒ String

Fork a session at a specific entry, creating a new branching session.

Parameters:

  • session_id (String)
  • cwd (String)

    working directory

  • from_entry_id (String)

    entry ID to fork at

Returns:

  • (String)

    new session ID

Raises:

  • (RuntimeError)

    if entry is not found



181
182
183
184
185
186
187
188
189
190
# File 'lib/crimson/session_manager.rb', line 181

def fork(session_id, cwd:, from_entry_id:)
  entries = load(session_id, cwd: cwd)
  fork_point = entries.index { |e| e.id == from_entry_id }
  raise "Entry #{from_entry_id} not found in session #{session_id}" unless fork_point

  prefix = entries[0..fork_point]
  new_id = SecureRandom.uuid
  prefix.each { |e| append(new_id, cwd: cwd, entry: e) }
  new_id
end

#latest(cwd:) ⇒ SessionMeta?

Get the most recent session for a directory.

Parameters:

  • cwd (String)

    working directory

Returns:



170
171
172
173
# File 'lib/crimson/session_manager.rb', line 170

def latest(cwd:)
  sessions = list(cwd: cwd)
  sessions.first
end

#list(cwd:) ⇒ Array<SessionMeta>

List all sessions for a given directory, sorted by mtime (newest first).

Parameters:

  • cwd (String)

    working directory

Returns:



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/crimson/session_manager.rb', line 100

def list(cwd:)
  dir = session_dir(cwd: cwd)
  return [] unless Dir.exist?(dir)

  Dir.glob(File.join(dir, "*.jsonl")).filter_map do |file|
    id = File.basename(file, ".jsonl")
    entries = []
    last_user_content = nil
    session_name = nil

    File.foreach(file) do |line|
      line = line.strip
      next if line.empty?
      begin
        parsed = JSON.parse(line)
        if parsed["type"] == "session_header"
          session_name = parsed["name"]
          next
        end
        entry = SessionEntry.from_h(parsed)
        entries << entry
        last_user_content = entry.content if entry.role == "user"
      rescue JSON::ParserError
        next
      end
    end

    next if entries.empty?

    SessionMeta.new(
      id: id,
      entry_count: entries.length,
      last_timestamp: entries.last.timestamp,
      preview: last_user_content && last_user_content.length > 80 ? last_user_content[0, 77] + "..." : last_user_content,
      name: session_name,
      mtime: File.mtime(file)
    )
  end.sort_by { |s| s.mtime }.reverse
end

#load(session_id, cwd:) ⇒ Array<SessionEntry>

Load all entries for a session.

Parameters:

  • session_id (String)
  • cwd (String)

    working directory

Returns:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/crimson/session_manager.rb', line 46

def load(session_id, cwd:)
  file = session_file(session_id, cwd: cwd)
  return [] unless File.exist?(file)

  entries = []
  File.foreach(file) do |line|
    line = line.strip
    next if line.empty?
    begin
      parsed = JSON.parse(line)
      next if parsed["type"] == "session_header"
      entries << SessionEntry.from_h(parsed)
    rescue JSON::ParserError
      next
    end
  end
  entries
end

#load_header(session_id, cwd:) ⇒ Hash?

Load only the header entry of a session.

Parameters:

  • session_id (String)
  • cwd (String)

    working directory

Returns:

  • (Hash, nil)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/crimson/session_manager.rb', line 69

def load_header(session_id, cwd:)
  file = session_file(session_id, cwd: cwd)
  return nil unless File.exist?(file)

  File.foreach(file) do |line|
    line = line.strip
    next if line.empty?
    begin
      parsed = JSON.parse(line)
      return parsed if parsed["type"] == "session_header"
    rescue JSON::ParserError
      next
    end
  end
  nil
end

#session_file(session_id, cwd:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



202
203
204
# File 'lib/crimson/session_manager.rb', line 202

def session_file(session_id, cwd:)
  File.join(session_dir(cwd: cwd), "#{session_id}.jsonl")
end

#set_name(session_id, cwd:, name:) ⇒ void

This method returns an undefined value.

Set the human-readable name for a session.

Parameters:

  • session_id (String)
  • cwd (String)

    working directory

  • name (String)


145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/crimson/session_manager.rb', line 145

def set_name(session_id, cwd:, name:)
  file = session_file(session_id, cwd: cwd)
  return unless File.exist?(file)

  lines = File.readlines(file)
  lines.each_with_index do |line, idx|
    stripped = line.strip
    next if stripped.empty?
    begin
      parsed = JSON.parse(stripped)
      if parsed["type"] == "session_header"
        parsed["name"] = name
        lines[idx] = JSON.generate(parsed) + "\n"
        File.write(file, lines.join)
        return
      end
    rescue JSON::ParserError
      next
    end
  end
end