anima-core 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +8 -0
  3. data/agents/codebase-analyzer.md +1 -1
  4. data/agents/codebase-pattern-finder.md +1 -1
  5. data/agents/documentation-researcher.md +1 -1
  6. data/agents/thoughts-analyzer.md +1 -1
  7. data/agents/web-search-researcher.md +1 -1
  8. data/app/channels/session_channel.rb +46 -49
  9. data/app/decorators/agent_message_decorator.rb +2 -2
  10. data/app/decorators/{event_decorator.rb → message_decorator.rb} +40 -40
  11. data/app/decorators/system_message_decorator.rb +2 -2
  12. data/app/decorators/tool_call_decorator.rb +2 -2
  13. data/app/decorators/tool_decorator.rb +4 -4
  14. data/app/decorators/tool_response_decorator.rb +2 -2
  15. data/app/decorators/user_message_decorator.rb +3 -3
  16. data/app/decorators/web_get_tool_decorator.rb +41 -9
  17. data/app/jobs/agent_request_job.rb +20 -20
  18. data/app/jobs/count_message_tokens_job.rb +39 -0
  19. data/app/jobs/passive_recall_job.rb +4 -4
  20. data/app/models/concerns/{event → message}/broadcasting.rb +16 -16
  21. data/app/models/goal.rb +4 -4
  22. data/app/models/goal_pinned_message.rb +11 -0
  23. data/app/models/message.rb +132 -0
  24. data/app/models/pinned_message.rb +41 -0
  25. data/app/models/session.rb +232 -192
  26. data/app/models/snapshot.rb +25 -25
  27. data/db/migrate/20260326180000_rename_event_to_message.rb +172 -0
  28. data/lib/agent_loop.rb +17 -9
  29. data/lib/agents/registry.rb +1 -1
  30. data/lib/analytical_brain/runner.rb +35 -35
  31. data/lib/analytical_brain/tools/activate_skill.rb +5 -9
  32. data/lib/analytical_brain/tools/assign_nickname.rb +2 -4
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +5 -9
  34. data/lib/analytical_brain/tools/everything_is_ready.rb +1 -2
  35. data/lib/analytical_brain/tools/finish_goal.rb +5 -8
  36. data/lib/analytical_brain/tools/read_workflow.rb +5 -9
  37. data/lib/analytical_brain/tools/rename_session.rb +3 -10
  38. data/lib/analytical_brain/tools/set_goal.rb +3 -7
  39. data/lib/analytical_brain/tools/update_goal.rb +3 -7
  40. data/lib/anima/settings.rb +19 -4
  41. data/lib/anima/version.rb +1 -1
  42. data/lib/events/bounce_back.rb +7 -7
  43. data/lib/events/subscribers/persister.rb +7 -7
  44. data/lib/events/subscribers/subagent_message_router.rb +12 -12
  45. data/lib/events/subscribers/transient_broadcaster.rb +2 -2
  46. data/lib/llm/client.rb +5 -2
  47. data/lib/mneme/compressed_viewport.rb +57 -57
  48. data/lib/mneme/l2_runner.rb +4 -4
  49. data/lib/mneme/passive_recall.rb +2 -2
  50. data/lib/mneme/runner.rb +57 -75
  51. data/lib/mneme/search.rb +55 -38
  52. data/lib/mneme/tools/attach_messages_to_goals.rb +103 -0
  53. data/lib/mneme/tools/everything_ok.rb +1 -3
  54. data/lib/mneme/tools/save_snapshot.rb +12 -16
  55. data/lib/skills/registry.rb +1 -1
  56. data/lib/tools/bash.rb +82 -7
  57. data/lib/tools/edit.rb +4 -6
  58. data/lib/tools/{request_feature.rb → open_issue.rb} +10 -13
  59. data/lib/tools/read.rb +4 -4
  60. data/lib/tools/registry.rb +1 -1
  61. data/lib/tools/remember.rb +46 -55
  62. data/lib/tools/spawn_specialist.rb +12 -23
  63. data/lib/tools/spawn_subagent.rb +9 -19
  64. data/lib/tools/subagent_prompts.rb +0 -2
  65. data/lib/tools/think.rb +3 -10
  66. data/lib/tools/web_get.rb +23 -4
  67. data/lib/tools/write.rb +3 -3
  68. data/lib/tui/cable_client.rb +3 -3
  69. data/lib/tui/message_store.rb +37 -37
  70. data/lib/tui/screens/chat.rb +27 -15
  71. data/lib/workflows/registry.rb +1 -1
  72. data/skills/activerecord/SKILL.md +1 -1
  73. data/skills/dragonruby/SKILL.md +1 -1
  74. data/skills/draper-decorators/SKILL.md +1 -1
  75. data/skills/gh-issue.md +1 -1
  76. data/skills/mcp-server/SKILL.md +1 -1
  77. data/skills/ratatui-ruby/SKILL.md +1 -1
  78. data/skills/rspec/SKILL.md +1 -1
  79. data/templates/config.toml +21 -4
  80. data/templates/soul.md +7 -19
  81. data/workflows/create_handoff.md +1 -1
  82. data/workflows/create_note.md +1 -1
  83. data/workflows/create_plan.md +1 -1
  84. data/workflows/implement_plan.md +1 -1
  85. data/workflows/iterate_plan.md +1 -1
  86. data/workflows/research_codebase.md +1 -1
  87. data/workflows/resume_handoff.md +1 -1
  88. data/workflows/review_pr.md +78 -16
  89. data/workflows/thoughts_init.md +1 -1
  90. data/workflows/validate_plan.md +1 -1
  91. metadata +10 -9
  92. data/app/jobs/count_event_tokens_job.rb +0 -39
  93. data/app/models/event.rb +0 -110
  94. data/app/models/goal_pinned_event.rb +0 -11
  95. data/app/models/pinned_event.rb +0 -41
  96. data/lib/mneme/tools/attach_events_to_goals.rb +0 -107
@@ -9,19 +9,15 @@ module AnalyticalBrain
9
9
  class ReadWorkflow < ::Tools::Base
10
10
  def self.tool_name = "read_workflow"
11
11
 
12
- def self.description = "Read a workflow's full content and activate it on the session. " \
13
- "Use the content to create appropriate goals with set_goal."
12
+ def self.description = "Activate a workflow and return its content for goal planning."
14
13
 
15
14
  def self.input_schema
16
15
  {
17
16
  type: "object",
18
17
  properties: {
19
- name: {
20
- type: "string",
21
- description: "Name of the workflow to read (from the available workflows list)"
22
- }
18
+ workflow_name: {type: "string"}
23
19
  },
24
- required: %w[name]
20
+ required: %w[workflow_name]
25
21
  }
26
22
  end
27
23
 
@@ -30,11 +26,11 @@ module AnalyticalBrain
30
26
  @main_session = main_session
31
27
  end
32
28
 
33
- # @param input [Hash<String, Object>] with "name" key
29
+ # @param input [Hash<String, Object>] with "workflow_name" key
34
30
  # @return [String] workflow name, description, and full content
35
31
  # @return [Hash] with :error key on validation failure
36
32
  def execute(input)
37
- workflow_name = input["name"].to_s.strip
33
+ workflow_name = input["workflow_name"].to_s.strip
38
34
  return {error: "Workflow name cannot be blank"} if workflow_name.empty?
39
35
 
40
36
  workflow = @main_session.activate_workflow(workflow_name)
@@ -11,21 +11,14 @@ module AnalyticalBrain
11
11
  class RenameSession < ::Tools::Base
12
12
  def self.tool_name = "rename_session"
13
13
 
14
- def self.description = "Rename the conversation session. " \
15
- "Use one emoji followed by 1-3 descriptive words."
14
+ def self.description = "Rename the session."
16
15
 
17
16
  def self.input_schema
18
17
  {
19
18
  type: "object",
20
19
  properties: {
21
- emoji: {
22
- type: "string",
23
- description: "A single emoji representing the conversation topic"
24
- },
25
- name: {
26
- type: "string",
27
- description: "1-3 word descriptive name for the session"
28
- }
20
+ emoji: {type: "string"},
21
+ name: {type: "string", description: "1-3 words."}
29
22
  },
30
23
  required: %w[emoji name]
31
24
  }
@@ -8,8 +8,7 @@ module AnalyticalBrain
8
8
  class SetGoal < ::Tools::Base
9
9
  def self.tool_name = "set_goal"
10
10
 
11
- def self.description = "Create a goal on the main session. " \
12
- "Omit parent_goal_id for a root goal, or provide it to create a sub-goal (TODO item)."
11
+ def self.description = "Create a goal or sub-goal."
13
12
 
14
13
  def self.input_schema
15
14
  {
@@ -17,12 +16,9 @@ module AnalyticalBrain
17
16
  properties: {
18
17
  description: {
19
18
  type: "string",
20
- description: "What needs to be accomplished (1-2 sentences)"
19
+ description: "1 sentence."
21
20
  },
22
- parent_goal_id: {
23
- type: "integer",
24
- description: "ID of the parent goal (omit for root goals)"
25
- }
21
+ parent_goal_id: {type: "integer"}
26
22
  },
27
23
  required: %w[description]
28
24
  }
@@ -15,20 +15,16 @@ module AnalyticalBrain
15
15
  class UpdateGoal < ::Tools::Base
16
16
  def self.tool_name = "update_goal"
17
17
 
18
- def self.description = "Update a goal's description. " \
19
- "Use this to refine a goal as understanding evolves."
18
+ def self.description = "Refine a goal's wording as understanding evolves."
20
19
 
21
20
  def self.input_schema
22
21
  {
23
22
  type: "object",
24
23
  properties: {
25
- goal_id: {
26
- type: "integer",
27
- description: "ID of the goal to update"
28
- },
24
+ goal_id: {type: "integer"},
29
25
  description: {
30
26
  type: "string",
31
- description: "New description for the goal (1-2 sentences)"
27
+ description: "1 sentence."
32
28
  }
33
29
  },
34
30
  required: %w[goal_id description]
@@ -60,6 +60,13 @@ module Anima
60
60
  self.config_path = nil
61
61
  end
62
62
 
63
+ # ─── Agent Identity ─────────────────────────────────────────────
64
+
65
+ # The agent's display name. Separates engine identity ("Anima") from
66
+ # agent identity — any agent running on Anima can name itself.
67
+ # @return [String]
68
+ def agent_name = get("agent", "name")
69
+
63
70
  # ─── LLM ───────────────────────────────────────────────────────
64
71
 
65
72
  # Primary model for conversations.
@@ -131,6 +138,10 @@ module Anima
131
138
  # @return [Integer]
132
139
  def max_web_response_bytes = get("tools", "max_web_response_bytes")
133
140
 
141
+ # Minimum characters of extracted web content before flagging as possibly incomplete.
142
+ # @return [Integer]
143
+ def min_web_content_chars = get("tools", "min_web_content_chars")
144
+
134
145
  # ─── Session ────────────────────────────────────────────────────
135
146
 
136
147
  # View mode applied to new sessions: "basic", "verbose", or "debug".
@@ -191,9 +202,9 @@ module Anima
191
202
  # @return [Boolean]
192
203
  def analytical_brain_blocking_on_agent_message = get("analytical_brain", "blocking_on_agent_message")
193
204
 
194
- # Number of recent events to include in the analytical brain's context window.
205
+ # Number of recent messages to include in the analytical brain's context window.
195
206
  # @return [Integer]
196
- def analytical_brain_event_window = get("analytical_brain", "event_window")
207
+ def analytical_brain_message_window = get("analytical_brain", "message_window")
197
208
 
198
209
  # ─── Mneme (Memory Department) ────────────────────────────────
199
210
 
@@ -217,8 +228,8 @@ module Anima
217
228
  # @return [Integer]
218
229
  def mneme_l2_snapshot_threshold = get("mneme", "l2_snapshot_threshold")
219
230
 
220
- # Fraction of the main viewport token budget reserved for pinned events.
221
- # Pinned events appear between snapshots and the sliding window.
231
+ # Fraction of the main viewport token budget reserved for pinned messages.
232
+ # Pinned messages appear between snapshots and the sliding window.
222
233
  # @return [Float]
223
234
  def mneme_pinned_budget_fraction = get("mneme", "pinned_budget_fraction")
224
235
 
@@ -236,6 +247,10 @@ module Anima
236
247
  # @return [Integer]
237
248
  def recall_max_snippet_tokens = get("recall", "max_snippet_tokens")
238
249
 
250
+ # Recency decay factor for search ranking (0.0 = pure relevance).
251
+ # @return [Float]
252
+ def recall_recency_decay = get("recall", "recency_decay")
253
+
239
254
  private
240
255
 
241
256
  # Reads a setting from the config file.
data/lib/anima/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Anima
4
- VERSION = "1.1.2"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -6,24 +6,24 @@ module Events
6
6
  # this event notifies clients to remove the phantom message and
7
7
  # restore the text to the input field.
8
8
  #
9
- # Not persisted — not included in {Event::TYPES}.
9
+ # Not persisted — not included in {Message::TYPES}.
10
10
  class BounceBack < Base
11
11
  TYPE = "bounce_back"
12
12
 
13
13
  # @return [String] human-readable error description
14
14
  attr_reader :error
15
15
 
16
- # @return [Integer, nil] database ID of the rolled-back event (for client-side removal)
17
- attr_reader :event_id
16
+ # @return [Integer, nil] database ID of the rolled-back message (for client-side removal)
17
+ attr_reader :message_id
18
18
 
19
19
  # @param content [String] original user message text to restore to input
20
20
  # @param error [String] error description for the flash message
21
21
  # @param session_id [Integer] session the message was intended for
22
- # @param event_id [Integer, nil] ID of the event that was broadcast optimistically
23
- def initialize(content:, error:, session_id:, event_id: nil)
22
+ # @param message_id [Integer, nil] ID of the message that was broadcast optimistically
23
+ def initialize(content:, error:, session_id:, message_id: nil)
24
24
  super(content: content, session_id: session_id)
25
25
  @error = error
26
- @event_id = event_id
26
+ @message_id = message_id
27
27
  end
28
28
 
29
29
  def type
@@ -31,7 +31,7 @@ module Events
31
31
  end
32
32
 
33
33
  def to_h
34
- super.merge(error: error, event_id: event_id)
34
+ super.merge(error: error, message_id: message_id)
35
35
  end
36
36
  end
37
37
  end
@@ -3,7 +3,7 @@
3
3
  module Events
4
4
  module Subscribers
5
5
  # Persists all events to SQLite as they flow through the event bus.
6
- # Each event is written as an Event record belonging to the active session.
6
+ # Each event is written as a Message record belonging to the active session.
7
7
  #
8
8
  # When initialized with a specific session, all events are saved to that
9
9
  # session. When initialized without one (global mode), the session is
@@ -31,7 +31,7 @@ module Events
31
31
  # Skips non-pending user messages — those are persisted by their
32
32
  # callers ({SessionChannel#speak} for idle sessions,
33
33
  # {AgentLoop#process} for direct usage). Also skips event types
34
- # not in {Event::TYPES} (transient events like {Events::BounceBack}).
34
+ # not in {Message::TYPES} (transient events like {Events::BounceBack}).
35
35
  #
36
36
  # @param event [Hash] with :payload containing event data
37
37
  def emit(event)
@@ -40,15 +40,15 @@ module Events
40
40
 
41
41
  event_type = payload[:type]
42
42
  return if event_type.nil?
43
- return unless Event::TYPES.include?(event_type)
43
+ return unless Message::TYPES.include?(event_type)
44
44
  return if persisted_by_job?(event_type, payload)
45
45
 
46
46
  target_session = @session || Session.find_by(id: payload[:session_id])
47
47
  return unless target_session
48
48
 
49
49
  @mutex.synchronize do
50
- target_session.events.create!(
51
- event_type: event_type,
50
+ target_session.messages.create!(
51
+ message_type: event_type,
52
52
  payload: payload,
53
53
  status: payload[:status],
54
54
  tool_use_id: payload[:tool_use_id],
@@ -64,12 +64,12 @@ module Events
64
64
  private
65
65
 
66
66
  # Non-pending user messages are persisted by their callers
67
- # ({SessionChannel#speak}, {AgentLoop#process}) so the event ID
67
+ # ({SessionChannel#speak}, {AgentLoop#process}) so the message ID
68
68
  # is available for bounce-back cleanup if LLM delivery fails.
69
69
  # Pending messages are still auto-persisted here because they
70
70
  # queue while the session is busy.
71
71
  def persisted_by_job?(event_type, payload)
72
- event_type == "user_message" && payload[:status] != Event::PENDING_STATUS
72
+ event_type == "user_message" && payload[:status] != Message::PENDING_STATUS
73
73
  end
74
74
  end
75
75
  end
@@ -6,16 +6,19 @@ module Events
6
6
  # bidirectional @mention communication.
7
7
  #
8
8
  # **Child → Parent:** When a sub-agent emits an {Events::AgentMessage},
9
- # the router persists a {Events::UserMessage} in the parent session
10
- # with attribution prefix, then wakes the parent via {AgentRequestJob}.
9
+ # the router creates a {Events::UserMessage} in the parent session
10
+ # with attribution prefix. If the parent is idle, persists directly
11
+ # and wakes it via {AgentRequestJob}. If the parent is mid-turn,
12
+ # emits a pending message that is promoted after the current loop
13
+ # completes — same mechanism as {SessionChannel#speak}.
11
14
  #
12
15
  # **Parent → Child:** When a parent agent emits an {Events::AgentMessage}
13
16
  # containing `@name` mentions, the router persists the message in each
14
17
  # matching child session and wakes them via {AgentRequestJob}.
15
18
  #
16
- # Both directions use direct persistence + job enqueue (same pattern as
17
- # {Tools::SpawnSubagent#spawn_child}) to avoid conflicts with the global
18
- # {Persister} which skips non-pending user messages.
19
+ # Both directions delegate to {Session#enqueue_user_message}, which
20
+ # respects the target session's processing state — persisting directly
21
+ # when idle, deferring via pending queue when mid-turn.
19
22
  #
20
23
  # This replaces the +return_result+ tool — sub-agents communicate
21
24
  # through natural text messages instead of structured tool calls.
@@ -60,9 +63,8 @@ module Events
60
63
 
61
64
  private
62
65
 
63
- # Forwards a sub-agent's text message to its parent session.
64
- # Persists directly and enqueues a job so the parent agent wakes
65
- # up to process the message.
66
+ # Forwards a sub-agent's text message to its parent session
67
+ # via {Session#enqueue_user_message}.
66
68
  #
67
69
  # @param child [Session] the sub-agent session
68
70
  # @param content [String] the sub-agent's message text
@@ -73,8 +75,7 @@ module Events
73
75
  name = child.name || "agent-#{child.id}"
74
76
  attributed = format(ATTRIBUTION_FORMAT, name, content)
75
77
 
76
- parent.create_user_event(attributed)
77
- AgentRequestJob.perform_later(parent.id)
78
+ parent.enqueue_user_message(attributed)
78
79
  end
79
80
 
80
81
  # Scans a parent agent's message for @mentions and routes the message
@@ -93,8 +94,7 @@ module Events
93
94
  child = active_children[name]
94
95
  next unless child
95
96
 
96
- child.create_user_event(content)
97
- AgentRequestJob.perform_later(child.id)
97
+ child.enqueue_user_message(content)
98
98
  end
99
99
  end
100
100
  end
@@ -3,8 +3,8 @@
3
3
  module Events
4
4
  module Subscribers
5
5
  # Bridges transient (non-persisted) events to ActionCable so clients
6
- # receive them over WebSocket. Persisted events reach clients via
7
- # {Event::Broadcasting} callbacks; this subscriber handles events
6
+ # receive them over WebSocket. Persisted messages reach clients via
7
+ # {Message::Broadcasting} callbacks; this subscriber handles events
8
8
  # that never touch the database.
9
9
  #
10
10
  # @example Registering at boot
data/lib/llm/client.rb CHANGED
@@ -177,9 +177,12 @@ module LLM
177
177
  # tool raises. Per the Anthropic tool-use protocol, every tool_use must
178
178
  # have a matching tool_result; a missing result permanently corrupts the
179
179
  # conversation history and breaks the session.
180
+ #
181
+ # Falls back to SecureRandom.uuid when Anthropic omits the tool_use id,
182
+ # ensuring the ToolCall/ToolResponse pair always shares a valid identifier.
180
183
  def execute_single_tool(tool_use, registry, session_id)
181
184
  name = tool_use["name"]
182
- id = tool_use["id"]
185
+ id = tool_use["id"] || SecureRandom.uuid
183
186
  input = tool_use["input"] || {}
184
187
  timeout = input["timeout"] || Anima::Settings.tool_timeout
185
188
 
@@ -231,7 +234,7 @@ module LLM
231
234
  # @return [Hash] tool_result content block
232
235
  def interrupt_tool(tool_use, session_id)
233
236
  name = tool_use["name"]
234
- id = tool_use["id"]
237
+ id = tool_use["id"] || SecureRandom.uuid
235
238
  input = tool_use["input"] || {}
236
239
 
237
240
  Events::Bus.emit(Events::ToolCall.new(
@@ -7,16 +7,16 @@ module Mneme
7
7
  # aggregate counters like `[4 tools called]`.
8
8
  #
9
9
  # The viewport is split into three zones separated by delimiters:
10
- # - **Eviction zone** — events about to leave the viewport (upper third)
11
- # - **Middle zone** — events in the middle of the viewport
12
- # - **Recent zone** — the most recent events (lower third)
10
+ # - **Eviction zone** — messages about to leave the viewport (upper third)
11
+ # - **Middle zone** — messages in the middle of the viewport
12
+ # - **Recent zone** — the most recent messages (lower third)
13
13
  #
14
14
  # Zone boundaries are calculated WITH tool call tokens (they affect
15
15
  # position), then tool calls are removed and replaced with counters.
16
16
  #
17
17
  # @example
18
18
  # viewport = Mneme::CompressedViewport.new(session, token_budget: 60_000)
19
- # viewport.render #=> "── EVICTION ZONE ──\nevent 42 User: ..."
19
+ # viewport.render #=> "── EVICTION ZONE ──\nmessage 42 User: ..."
20
20
  class CompressedViewport
21
21
  ZONE_DELIMITERS = {
22
22
  eviction: "── EVICTION ZONE (upper third) ──",
@@ -26,74 +26,74 @@ module Mneme
26
26
 
27
27
  # @param session [Session] the session to build viewport for
28
28
  # @param token_budget [Integer] total tokens available for Mneme's viewport
29
- # @param from_event_id [Integer, nil] start from this event ID (inclusive);
29
+ # @param from_message_id [Integer, nil] start from this message ID (inclusive);
30
30
  # when nil, uses the session's full viewport
31
- def initialize(session, token_budget:, from_event_id: nil)
31
+ def initialize(session, token_budget:, from_message_id: nil)
32
32
  @session = session
33
33
  @token_budget = token_budget
34
- @from_event_id = from_event_id
34
+ @from_message_id = from_message_id
35
35
  end
36
36
 
37
37
  # Renders the compressed viewport as a string ready for Mneme's LLM context.
38
38
  #
39
39
  # @return [String] compressed viewport with zone delimiters
40
40
  def render
41
- return "" if events.empty?
41
+ return "" if messages.empty?
42
42
 
43
- zones = split_into_zones(events)
43
+ zones = split_into_zones(messages)
44
44
  render_zones(zones)
45
45
  end
46
46
 
47
- # @return [Array<Event>] the raw events selected for this viewport
48
- def events
49
- @events ||= fetch_events
47
+ # @return [Array<Message>] the raw messages selected for this viewport
48
+ def messages
49
+ @messages ||= fetch_messages
50
50
  end
51
51
 
52
52
  private
53
53
 
54
- # Fetches events within token budget, starting from from_event_id.
54
+ # Fetches messages within token budget, starting from from_message_id.
55
55
  # Selects newest-first until budget exhausted, returns chronological.
56
- # Caches per-event token costs in @event_costs for reuse by split_into_zones.
56
+ # Caches per-message token costs in @message_costs for reuse by split_into_zones.
57
57
  #
58
- # @return [Array<Event>]
59
- def fetch_events
60
- scope = @session.events.context_events.deliverable
58
+ # @return [Array<Message>]
59
+ def fetch_messages
60
+ scope = @session.messages.context_messages.deliverable
61
61
 
62
- if @from_event_id
63
- scope = scope.where("id >= ?", @from_event_id)
62
+ if @from_message_id
63
+ scope = scope.where("id >= ?", @from_message_id)
64
64
  end
65
65
 
66
66
  selected = []
67
- @event_costs = {}
67
+ @message_costs = {}
68
68
  remaining = @token_budget
69
69
 
70
- scope.reorder(id: :desc).each do |event|
71
- cost = event_token_cost(event)
70
+ scope.reorder(id: :desc).each do |message|
71
+ cost = message_token_cost(message)
72
72
  break if cost > remaining && selected.any?
73
73
 
74
- selected << event
75
- @event_costs[event.id] = cost
74
+ selected << message
75
+ @message_costs[message.id] = cost
76
76
  remaining -= cost
77
77
  end
78
78
 
79
79
  selected.reverse
80
80
  end
81
81
 
82
- # Splits events into three zones by token count.
83
- # Zone boundaries are calculated including ALL events (tool calls count
82
+ # Splits messages into three zones by token count.
83
+ # Zone boundaries are calculated including ALL messages (tool calls count
84
84
  # toward position), but zone assignment uses cumulative tokens.
85
85
  #
86
- # @return [Hash{Symbol => Array<Event>}] :eviction, :middle, :recent
87
- def split_into_zones(events)
88
- costs = events.map { |event| [event, @event_costs[event.id] || event_token_cost(event)] }
86
+ # @return [Hash{Symbol => Array<Message>}] :eviction, :middle, :recent
87
+ def split_into_zones(messages)
88
+ costs = messages.map { |message| [message, @message_costs[message.id] || message_token_cost(message)] }
89
89
  zone_size = costs.sum(&:last) / 3.0
90
90
 
91
91
  result = {eviction: [], middle: [], recent: []}
92
92
  cumulative = 0
93
93
 
94
- costs.each do |event, cost|
94
+ costs.each do |message, cost|
95
95
  cumulative += cost
96
- result[zone_for_cumulative(cumulative, zone_size)] << event
96
+ result[zone_for_cumulative(cumulative, zone_size)] << message
97
97
  end
98
98
 
99
99
  result
@@ -101,7 +101,7 @@ module Mneme
101
101
 
102
102
  # Renders zones with delimiters, compressing tool calls into counters.
103
103
  #
104
- # @param zones [Hash{Symbol => Array<Event>}]
104
+ # @param zones [Hash{Symbol => Array<Message>}]
105
105
  # @return [String]
106
106
  def render_zones(zones)
107
107
  %i[eviction middle recent].flat_map { |name|
@@ -124,23 +124,23 @@ module Mneme
124
124
  end
125
125
  end
126
126
 
127
- # Renders a single zone: conversation events as full text, consecutive
127
+ # Renders a single zone: conversation messages as full text, consecutive
128
128
  # tool calls/responses compressed into `[N tools called]` counters.
129
- # tool_response events are intentionally silent — they affect zone boundaries
130
- # via token cost but are not rendered; only tool_call events increment the counter.
129
+ # tool_response messages are intentionally silent — they affect zone boundaries
130
+ # via token cost but are not rendered; only tool_call messages increment the counter.
131
131
  #
132
- # @param zone_events [Array<Event>]
132
+ # @param zone_messages [Array<Message>]
133
133
  # @return [String]
134
- def render_zone(zone_events)
134
+ def render_zone(zone_messages)
135
135
  lines = []
136
136
  tool_count = 0
137
137
 
138
- zone_events.each do |event|
139
- if conversation_event?(event) || think_event?(event)
138
+ zone_messages.each do |message|
139
+ if conversation_message?(message) || think_message?(message)
140
140
  lines << flush_tool_count(tool_count)
141
141
  tool_count = 0
142
- lines << render_event_line(event)
143
- elsif event.event_type == "tool_call"
142
+ lines << render_message_line(message)
143
+ elsif message.message_type == "tool_call"
144
144
  tool_count += 1
145
145
  end
146
146
  end
@@ -149,17 +149,17 @@ module Mneme
149
149
  lines.compact.join("\n")
150
150
  end
151
151
 
152
- # @return [Boolean] true if event is a user/agent/system message
153
- def conversation_event?(event)
154
- event.event_type.in?(Event::CONVERSATION_TYPES)
152
+ # @return [Boolean] true if message is a user/agent/system message
153
+ def conversation_message?(message)
154
+ message.message_type.in?(Message::CONVERSATION_TYPES)
155
155
  end
156
156
 
157
- # Think events are tool_call events with tool_name == "think".
157
+ # Think messages are tool_call messages with tool_name == "think".
158
158
  # They carry the agent's reasoning and are treated as conversation.
159
159
  #
160
160
  # @return [Boolean]
161
- def think_event?(event)
162
- event.event_type == "tool_call" && event.payload["tool_name"] == Event::THINK_TOOL
161
+ def think_message?(message)
162
+ message.message_type == "tool_call" && message.payload["tool_name"] == Message::THINK_TOOL
163
163
  end
164
164
 
165
165
  ROLE_LABELS = {
@@ -168,17 +168,17 @@ module Mneme
168
168
  "system_message" => "System"
169
169
  }.freeze
170
170
 
171
- # Renders a single event as a transcript line.
171
+ # Renders a single message as a transcript line.
172
172
  #
173
- # @param event [Event]
173
+ # @param message [Message]
174
174
  # @return [String]
175
- def render_event_line(event)
176
- prefix = "event #{event.id}"
177
- data = event.payload
178
- if think_event?(event)
175
+ def render_message_line(message)
176
+ prefix = "message #{message.id}"
177
+ data = message.payload
178
+ if think_message?(message)
179
179
  "#{prefix} Think: #{data.dig("tool_input", "thoughts")}"
180
180
  else
181
- "#{prefix} #{ROLE_LABELS.fetch(event.event_type)}: #{data["content"]}"
181
+ "#{prefix} #{ROLE_LABELS.fetch(message.message_type)}: #{data["content"]}"
182
182
  end
183
183
  end
184
184
 
@@ -192,9 +192,9 @@ module Mneme
192
192
  end
193
193
 
194
194
  # @return [Integer] token cost using cached count or heuristic
195
- def event_token_cost(event)
196
- cached = event.token_count
197
- (cached > 0) ? cached : event.estimate_tokens
195
+ def message_token_cost(message)
196
+ cached = message.token_count
197
+ (cached > 0) ? cached : message.estimate_tokens
198
198
  end
199
199
  end
200
200
  end
@@ -110,22 +110,22 @@ module Mneme
110
110
  # @return [Array<Hash>] single-element messages array
111
111
  def build_messages(snapshots)
112
112
  content = snapshots.map.with_index(1) { |snap, idx|
113
- "--- Snapshot #{idx} (events #{snap.from_event_id}..#{snap.to_event_id}) ---\n#{snap.text}"
113
+ "--- Snapshot #{idx} (messages #{snap.from_message_id}..#{snap.to_message_id}) ---\n#{snap.text}"
114
114
  }.join("\n\n")
115
115
 
116
116
  [{role: "user", content: "Compress these #{snapshots.size} Level 1 snapshots into a single Level 2 summary:\n\n#{content}"}]
117
117
  end
118
118
 
119
119
  # Builds the tool registry with L2 context for SaveSnapshot.
120
- # The event range spans from the first L1's start to the last L1's end.
120
+ # The message range spans from the first L1's start to the last L1's end.
121
121
  #
122
122
  # @param snapshots [Array<Snapshot>]
123
123
  # @return [Tools::Registry]
124
124
  def build_registry(snapshots)
125
125
  registry = ::Tools::Registry.new(context: {
126
126
  main_session: @session,
127
- from_event_id: snapshots.first.from_event_id,
128
- to_event_id: snapshots.last.to_event_id,
127
+ from_message_id: snapshots.first.from_message_id,
128
+ to_message_id: snapshots.last.to_message_id,
129
129
  level: 2
130
130
  })
131
131
  TOOLS.each { |tool| registry.register(tool) }
@@ -32,8 +32,8 @@ module Mneme
32
32
 
33
33
  # Exclude events from the current session's viewport — no point recalling
34
34
  # what the agent already sees.
35
- viewport_ids = @session.viewport_event_ids.to_set
36
- results.reject { |result| viewport_ids.include?(result.event_id) }
35
+ viewport_ids = @session.viewport_message_ids.to_set
36
+ results.reject { |result| viewport_ids.include?(result.message_id) }
37
37
  end
38
38
 
39
39
  private