Class: Brute::Store::MessageStore

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

Overview

Stores session messages as individual JSON files in the OpenCode parts format. Each session gets a directory; each message is a numbered JSON file inside it.

Storage layout:

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

Message format matches OpenCode’s MessageV2.WithParts:

{ info: { id:, sessionID:, role:, time:, ... },
  parts: [{ id:, type:, ... }, ...] }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(session_id:, dir: nil) ⇒ MessageStore

Returns a new instance of MessageStore.



31
32
33
34
35
36
37
38
39
# File 'lib/brute/store/message_store.rb', line 31

def initialize(session_id:, dir: nil)
  @session_id = session_id
  @dir = dir || File.join(Dir.home, ".brute", "sessions", session_id)
  @messages = {}   # id => { info:, parts: }
  @seq = 0
  @part_seq = 0
  @mutex = Mutex.new
  load_existing
end

Instance Attribute Details

#dirObject (readonly)

Returns the value of attribute dir.



29
30
31
# File 'lib/brute/store/message_store.rb', line 29

def dir
  @dir
end

#session_idObject (readonly)

Returns the value of attribute session_id.



29
30
31
# File 'lib/brute/store/message_store.rb', line 29

def session_id
  @session_id
end

Instance Method Details

#add_step_finish(message_id:, tokens: nil) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/brute/store/message_store.rb', line 142

def add_step_finish(message_id:, tokens: nil)
  @mutex.synchronize do
    msg = @messages[message_id]
    return unless msg

    part = {
      id: next_part_id, sessionID: @session_id, messageID: message_id,
      type: "step-finish",
      reason: "stop",
      tokens: tokens || { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
    }
    msg[:parts] << part
    persist(message_id)
  end
end

#add_text_part(message_id:, text:) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/brute/store/message_store.rb', line 79

def add_text_part(message_id:, text:)
  @mutex.synchronize do
    msg = @messages[message_id]
    return unless msg

    part = { id: next_part_id, sessionID: @session_id, messageID: message_id,
             type: "text", text: text }
    msg[:parts] << part
    persist(message_id)
    part[:id]
  end
end

#add_tool_part(message_id:, tool:, call_id:, input:) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/brute/store/message_store.rb', line 92

def add_tool_part(message_id:, tool:, call_id:, input:)
  @mutex.synchronize do
    msg = @messages[message_id]
    return unless msg

    part = {
      id: next_part_id, sessionID: @session_id, messageID: message_id,
      type: "tool", callID: call_id, tool: tool,
      state: {
        status: "running",
        input: input,
        time: { start: now_ms },
      },
    }
    msg[:parts] << part
    persist(message_id)
    part[:id]
  end
end

#append_assistant(message_id: nil, parent_id: nil, model_id: nil, provider_id: nil) ⇒ Object



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

def append_assistant(message_id: nil, parent_id: nil, model_id: nil, provider_id: nil)
  id = message_id || next_message_id
  msg = {
    info: {
      id: id,
      sessionID: @session_id,
      role: "assistant",
      parentID: parent_id,
      time: { created: now_ms },
      modelID: model_id,
      providerID: provider_id,
      tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
      cost: 0.0,
    },
    parts: [],
  }
  save_message(id, msg)
  id
end

#append_user(text:, message_id: nil) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/brute/store/message_store.rb', line 41

def append_user(text:, message_id: nil)
  id = message_id || next_message_id
  msg = {
    info: {
      id: id,
      sessionID: @session_id,
      role: "user",
      time: { created: now_ms },
    },
    parts: [
      { id: next_part_id, sessionID: @session_id, messageID: id,
        type: "text", text: text },
    ],
  }
  save_message(id, msg)
  id
end

#complete_assistant(message_id:, tokens: nil) ⇒ Object

Finalize an assistant message with token counts and completion time.



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/brute/store/message_store.rb', line 159

def complete_assistant(message_id:, tokens: nil)
  @mutex.synchronize do
    msg = @messages[message_id]
    return unless msg

    msg[:info][:time][:completed] = now_ms
    if tokens
      msg[:info][:tokens] = {
        input: tokens[:input] || tokens[:total_input] || 0,
        output: tokens[:output] || tokens[:total_output] || 0,
        reasoning: tokens[:reasoning] || tokens[:total_reasoning] || 0,
        cache: tokens[:cache] || { read: 0, write: 0 },
      }
    end
    persist(message_id)
  end
end

#complete_tool_part(message_id:, call_id:, output:) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/brute/store/message_store.rb', line 112

def complete_tool_part(message_id:, call_id:, output:)
  @mutex.synchronize do
    msg = @messages[message_id]
    return unless msg

    part = msg[:parts].find { |p| p[:type] == "tool" && p[:callID] == call_id }
    return unless part

    part[:state][:status] = "completed"
    part[:state][:output] = output
    part[:state][:time][:end] = now_ms
    persist(message_id)
  end
end

#countObject



185
186
187
# File 'lib/brute/store/message_store.rb', line 185

def count
  @mutex.synchronize { @messages.size }
end

#error_tool_part(message_id:, call_id:, error:) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/brute/store/message_store.rb', line 127

def error_tool_part(message_id:, call_id:, error:)
  @mutex.synchronize do
    msg = @messages[message_id]
    return unless msg

    part = msg[:parts].find { |p| p[:type] == "tool" && p[:callID] == call_id }
    return unless part

    part[:state][:status] = "error"
    part[:state][:error] = error.to_s
    part[:state][:time][:end] = now_ms
    persist(message_id)
  end
end

#message(id) ⇒ Object



181
182
183
# File 'lib/brute/store/message_store.rb', line 181

def message(id)
  @mutex.synchronize { @messages[id] }
end

#messagesObject



177
178
179
# File 'lib/brute/store/message_store.rb', line 177

def messages
  @mutex.synchronize { @messages.values }
end