Class: Brute::MessageStore

Inherits:
Object
  • Object
show all
Defined in:
lib/brute/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.



28
29
30
31
32
33
34
35
36
# File 'lib/brute/message_store.rb', line 28

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.



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

def dir
  @dir
end

#session_idObject (readonly)

Returns the value of attribute session_id.



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

def session_id
  @session_id
end

Instance Method Details

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

Add a step-finish part to an assistant message.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/brute/message_store.rb', line 151

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

Add a text part to an existing message.



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/brute/message_store.rb', line 84

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

Add a tool part in “running” state. Returns the part ID.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/brute/message_store.rb', line 98

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

Record the start of an assistant message. Returns the message ID. Call complete_assistant later to fill in tokens/timing.



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

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

Record a user message.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/brute/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.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/brute/message_store.rb', line 170

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

Mark a tool part as completed with output.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/brute/message_store.rb', line 119

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

Number of stored messages.



201
202
203
# File 'lib/brute/message_store.rb', line 201

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

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

Mark a tool part as errored.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/brute/message_store.rb', line 135

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

Single message by ID.



196
197
198
# File 'lib/brute/message_store.rb', line 196

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

#messagesObject

All messages in order.



191
192
193
# File 'lib/brute/message_store.rb', line 191

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