Class: Brute::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/brute/session.rb

Overview

Manages session persistence. Each session is a conversation that can be saved to disk and resumed later.

New directory-based layout (per-session directory):

~/.brute/sessions/{session-id}/
  session.meta.json          # session metadata
  context.json               # llm.rb context blob (for resumption)
  msg_0001.json              # structured messages (OpenCode format)
  msg_0002.json
  ...

Also supports the legacy flat layout for reading:

~/.brute/sessions/{session-id}.json
~/.brute/sessions/{session-id}.meta.json

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id: nil, dir: nil) ⇒ Session

Returns a new instance of Session.



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

def initialize(id: nil, dir: nil)
  @id = id || SecureRandom.uuid
  @base_dir = dir || File.join(Dir.home, ".brute", "sessions")
  @session_dir = File.join(@base_dir, @id)
  @path = File.join(@session_dir, "context.json")
  @title = nil
  @metadata = {}
  FileUtils.mkdir_p(@session_dir)

  # Check for legacy flat-file layout and migrate path if present
  @legacy_path = File.join(@base_dir, "#{@id}.json")
  @legacy_meta = File.join(@base_dir, "#{@id}.meta.json")
end

Instance Attribute Details

#idObject (readonly)

Returns the value of attribute id.



26
27
28
# File 'lib/brute/session.rb', line 26

def id
  @id
end

#pathObject (readonly)

Returns the value of attribute path.



26
27
28
# File 'lib/brute/session.rb', line 26

def path
  @path
end

#titleObject (readonly)

Returns the value of attribute title.



26
27
28
# File 'lib/brute/session.rb', line 26

def title
  @title
end

Class Method Details

.list(dir: nil) ⇒ Object

List all saved sessions, newest first. Scans both new directory-based layout and legacy flat files.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/brute/session.rb', line 81

def self.list(dir: nil)
  dir ||= File.join(Dir.home, ".brute", "sessions")
  return [] unless File.directory?(dir)

  sessions = {}

  # New layout: {id}/session.meta.json
  Dir.glob(File.join(dir, "*", "session.meta.json")).each do |meta_path|
    data = JSON.parse(File.read(meta_path), symbolize_names: true)
    id = data[:id]
    next unless id
    sessions[id] = {
      id: id,
      title: data[:title],
      saved_at: data[:saved_at],
      path: File.join(File.dirname(meta_path), "context.json"),
    }
  end

  # Legacy layout: {id}.meta.json (only if not already found)
  Dir.glob(File.join(dir, "*.meta.json")).each do |meta_path|
    # Skip files inside session subdirectories
    next if meta_path.include?("/session.meta.json")
    data = JSON.parse(File.read(meta_path), symbolize_names: true)
    id = data[:id]
    next unless id
    next if sessions.key?(id)  # new layout takes precedence
    sessions[id] = {
      id: id,
      title: data[:title],
      saved_at: data[:saved_at],
      path: meta_path.sub(/\.meta\.json$/, ".json"),
    }
  end

  sessions.values.sort_by { |s| s[:saved_at] || "" }.reverse
end

Instance Method Details

#deleteObject

Delete a session from disk (both new and legacy layouts).



120
121
122
123
124
125
126
127
# File 'lib/brute/session.rb', line 120

def delete
  # New layout: remove the whole directory
  FileUtils.rm_rf(@session_dir) if File.directory?(@session_dir)

  # Legacy layout: remove flat files
  File.delete(@legacy_path) if File.exist?(@legacy_path)
  File.delete(@legacy_meta) if File.exist?(@legacy_meta)
end

#message_storeObject

Returns a MessageStore for this session’s structured messages.



43
44
45
# File 'lib/brute/session.rb', line 43

def message_store
  @message_store ||= MessageStore.new(session_id: @id, dir: @session_dir)
end

#restore(context) ⇒ Object

Restore a context from this session. Returns true if restored successfully, false if no session file found.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/brute/session.rb', line 61

def restore(context)
  # Try new layout first, then legacy
  ctx_path = if File.exist?(@path)
    @path
  elsif File.exist?(@legacy_path)
    @legacy_path
  end

  return false unless ctx_path

  context.restore(path: ctx_path)

  # Load metadata
  load_meta

  true
end

#save(context, title: nil, metadata: {}) ⇒ Object

Save a context to this session.



48
49
50
51
52
53
54
55
56
57
# File 'lib/brute/session.rb', line 48

def save(context, title: nil, metadata: {})
  @title = title if title
  @metadata.merge!()

  # Use llm.rb's built-in serialization for context (used for resumption)
  context.save(path: @path)

  # Write metadata sidecar
  save_meta
end