Class: Snapshot

Inherits:
ApplicationRecord show all
Defined in:
app/models/snapshot.rb

Overview

A persisted summary of conversation context created by Mneme before events evict from the viewport. Snapshots capture the “gist” of what happened so the agent retains awareness of past context.

Level 1 snapshots are created from raw events (messages + thinks). Level 2 snapshots compress multiple Level 1 snapshots (days/weeks scale). Both levels use the same event ID range tracking — an L2 snapshot’s range is the union of its constituent L1 snapshots.

Constant Summary collapse

MAX_TEXT_BYTES =

32KB — generous upper bound (~8K tokens). The LLM tool description advises a tighter limit (mneme_max_tokens), but this hard cap prevents unbounded storage.

32_768

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#from_event_idInteger

Returns first event ID covered by this snapshot.

Returns:

  • (Integer)

    first event ID covered by this snapshot



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/snapshot.rb', line 22

class Snapshot < ApplicationRecord
  belongs_to :session

  # 32KB — generous upper bound (~8K tokens). The LLM tool description advises
  # a tighter limit (mneme_max_tokens), but this hard cap prevents unbounded storage.
  MAX_TEXT_BYTES = 32_768

  validates :text, presence: true, length: {maximum: MAX_TEXT_BYTES}
  validates :from_event_id, presence: true
  validates :to_event_id, presence: true
  validates :level, presence: true, numericality: {greater_than: 0}
  validates :token_count, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
  validate :from_event_id_not_after_to_event_id

  scope :for_level, ->(level) { where(level: level) }
  scope :chronological, -> { order(:from_event_id) }

  # L1 snapshots whose event range is NOT fully contained within any L2 snapshot.
  # Used to determine which L1 snapshots are still "live" in the viewport.
  scope :not_covered_by_l2, -> {
    where.not(
      "EXISTS (SELECT 1 FROM snapshots l2 " \
      "WHERE l2.session_id = snapshots.session_id " \
      "AND l2.level = 2 " \
      "AND l2.from_event_id <= snapshots.from_event_id " \
      "AND l2.to_event_id >= snapshots.to_event_id)"
    )
  }

  # Snapshots whose source events have fully evicted from the sliding window.
  # A snapshot is visible when its entire event range precedes the first
  # event currently in the viewport.
  #
  # @param first_event_id [Integer] the first event ID in the sliding window
  scope :source_events_evicted, ->(first_event_id) {
    where("to_event_id < ?", first_event_id)
  }

  # @return [Integer] token cost, using cached count or heuristic estimate
  def token_cost
    token_count.positive? ? token_count : estimate_tokens
  end

  private

  def from_event_id_not_after_to_event_id
    return unless from_event_id && to_event_id
    errors.add(:from_event_id, "must be <= to_event_id") if from_event_id > to_event_id
  end

  # @return [Integer] estimated token count (at least 1)
  def estimate_tokens
    [(text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
  end
end

#levelInteger

Returns compression level (1 = from raw events, 2 = from L1 snapshots).

Returns:

  • (Integer)

    compression level (1 = from raw events, 2 = from L1 snapshots)



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/snapshot.rb', line 22

class Snapshot < ApplicationRecord
  belongs_to :session

  # 32KB — generous upper bound (~8K tokens). The LLM tool description advises
  # a tighter limit (mneme_max_tokens), but this hard cap prevents unbounded storage.
  MAX_TEXT_BYTES = 32_768

  validates :text, presence: true, length: {maximum: MAX_TEXT_BYTES}
  validates :from_event_id, presence: true
  validates :to_event_id, presence: true
  validates :level, presence: true, numericality: {greater_than: 0}
  validates :token_count, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
  validate :from_event_id_not_after_to_event_id

  scope :for_level, ->(level) { where(level: level) }
  scope :chronological, -> { order(:from_event_id) }

  # L1 snapshots whose event range is NOT fully contained within any L2 snapshot.
  # Used to determine which L1 snapshots are still "live" in the viewport.
  scope :not_covered_by_l2, -> {
    where.not(
      "EXISTS (SELECT 1 FROM snapshots l2 " \
      "WHERE l2.session_id = snapshots.session_id " \
      "AND l2.level = 2 " \
      "AND l2.from_event_id <= snapshots.from_event_id " \
      "AND l2.to_event_id >= snapshots.to_event_id)"
    )
  }

  # Snapshots whose source events have fully evicted from the sliding window.
  # A snapshot is visible when its entire event range precedes the first
  # event currently in the viewport.
  #
  # @param first_event_id [Integer] the first event ID in the sliding window
  scope :source_events_evicted, ->(first_event_id) {
    where("to_event_id < ?", first_event_id)
  }

  # @return [Integer] token cost, using cached count or heuristic estimate
  def token_cost
    token_count.positive? ? token_count : estimate_tokens
  end

  private

  def from_event_id_not_after_to_event_id
    return unless from_event_id && to_event_id
    errors.add(:from_event_id, "must be <= to_event_id") if from_event_id > to_event_id
  end

  # @return [Integer] estimated token count (at least 1)
  def estimate_tokens
    [(text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
  end
end

#textString

Returns the summary text generated by Mneme.

Returns:

  • (String)

    the summary text generated by Mneme



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/snapshot.rb', line 22

class Snapshot < ApplicationRecord
  belongs_to :session

  # 32KB — generous upper bound (~8K tokens). The LLM tool description advises
  # a tighter limit (mneme_max_tokens), but this hard cap prevents unbounded storage.
  MAX_TEXT_BYTES = 32_768

  validates :text, presence: true, length: {maximum: MAX_TEXT_BYTES}
  validates :from_event_id, presence: true
  validates :to_event_id, presence: true
  validates :level, presence: true, numericality: {greater_than: 0}
  validates :token_count, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
  validate :from_event_id_not_after_to_event_id

  scope :for_level, ->(level) { where(level: level) }
  scope :chronological, -> { order(:from_event_id) }

  # L1 snapshots whose event range is NOT fully contained within any L2 snapshot.
  # Used to determine which L1 snapshots are still "live" in the viewport.
  scope :not_covered_by_l2, -> {
    where.not(
      "EXISTS (SELECT 1 FROM snapshots l2 " \
      "WHERE l2.session_id = snapshots.session_id " \
      "AND l2.level = 2 " \
      "AND l2.from_event_id <= snapshots.from_event_id " \
      "AND l2.to_event_id >= snapshots.to_event_id)"
    )
  }

  # Snapshots whose source events have fully evicted from the sliding window.
  # A snapshot is visible when its entire event range precedes the first
  # event currently in the viewport.
  #
  # @param first_event_id [Integer] the first event ID in the sliding window
  scope :source_events_evicted, ->(first_event_id) {
    where("to_event_id < ?", first_event_id)
  }

  # @return [Integer] token cost, using cached count or heuristic estimate
  def token_cost
    token_count.positive? ? token_count : estimate_tokens
  end

  private

  def from_event_id_not_after_to_event_id
    return unless from_event_id && to_event_id
    errors.add(:from_event_id, "must be <= to_event_id") if from_event_id > to_event_id
  end

  # @return [Integer] estimated token count (at least 1)
  def estimate_tokens
    [(text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
  end
end

#to_event_idInteger

Returns last event ID covered by this snapshot.

Returns:

  • (Integer)

    last event ID covered by this snapshot



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/snapshot.rb', line 22

class Snapshot < ApplicationRecord
  belongs_to :session

  # 32KB — generous upper bound (~8K tokens). The LLM tool description advises
  # a tighter limit (mneme_max_tokens), but this hard cap prevents unbounded storage.
  MAX_TEXT_BYTES = 32_768

  validates :text, presence: true, length: {maximum: MAX_TEXT_BYTES}
  validates :from_event_id, presence: true
  validates :to_event_id, presence: true
  validates :level, presence: true, numericality: {greater_than: 0}
  validates :token_count, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
  validate :from_event_id_not_after_to_event_id

  scope :for_level, ->(level) { where(level: level) }
  scope :chronological, -> { order(:from_event_id) }

  # L1 snapshots whose event range is NOT fully contained within any L2 snapshot.
  # Used to determine which L1 snapshots are still "live" in the viewport.
  scope :not_covered_by_l2, -> {
    where.not(
      "EXISTS (SELECT 1 FROM snapshots l2 " \
      "WHERE l2.session_id = snapshots.session_id " \
      "AND l2.level = 2 " \
      "AND l2.from_event_id <= snapshots.from_event_id " \
      "AND l2.to_event_id >= snapshots.to_event_id)"
    )
  }

  # Snapshots whose source events have fully evicted from the sliding window.
  # A snapshot is visible when its entire event range precedes the first
  # event currently in the viewport.
  #
  # @param first_event_id [Integer] the first event ID in the sliding window
  scope :source_events_evicted, ->(first_event_id) {
    where("to_event_id < ?", first_event_id)
  }

  # @return [Integer] token cost, using cached count or heuristic estimate
  def token_cost
    token_count.positive? ? token_count : estimate_tokens
  end

  private

  def from_event_id_not_after_to_event_id
    return unless from_event_id && to_event_id
    errors.add(:from_event_id, "must be <= to_event_id") if from_event_id > to_event_id
  end

  # @return [Integer] estimated token count (at least 1)
  def estimate_tokens
    [(text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
  end
end

#token_countInteger

Returns cached token count of the summary text.

Returns:

  • (Integer)

    cached token count of the summary text



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/snapshot.rb', line 22

class Snapshot < ApplicationRecord
  belongs_to :session

  # 32KB — generous upper bound (~8K tokens). The LLM tool description advises
  # a tighter limit (mneme_max_tokens), but this hard cap prevents unbounded storage.
  MAX_TEXT_BYTES = 32_768

  validates :text, presence: true, length: {maximum: MAX_TEXT_BYTES}
  validates :from_event_id, presence: true
  validates :to_event_id, presence: true
  validates :level, presence: true, numericality: {greater_than: 0}
  validates :token_count, numericality: {greater_than_or_equal_to: 0}, allow_nil: true
  validate :from_event_id_not_after_to_event_id

  scope :for_level, ->(level) { where(level: level) }
  scope :chronological, -> { order(:from_event_id) }

  # L1 snapshots whose event range is NOT fully contained within any L2 snapshot.
  # Used to determine which L1 snapshots are still "live" in the viewport.
  scope :not_covered_by_l2, -> {
    where.not(
      "EXISTS (SELECT 1 FROM snapshots l2 " \
      "WHERE l2.session_id = snapshots.session_id " \
      "AND l2.level = 2 " \
      "AND l2.from_event_id <= snapshots.from_event_id " \
      "AND l2.to_event_id >= snapshots.to_event_id)"
    )
  }

  # Snapshots whose source events have fully evicted from the sliding window.
  # A snapshot is visible when its entire event range precedes the first
  # event currently in the viewport.
  #
  # @param first_event_id [Integer] the first event ID in the sliding window
  scope :source_events_evicted, ->(first_event_id) {
    where("to_event_id < ?", first_event_id)
  }

  # @return [Integer] token cost, using cached count or heuristic estimate
  def token_cost
    token_count.positive? ? token_count : estimate_tokens
  end

  private

  def from_event_id_not_after_to_event_id
    return unless from_event_id && to_event_id
    errors.add(:from_event_id, "must be <= to_event_id") if from_event_id > to_event_id
  end

  # @return [Integer] estimated token count (at least 1)
  def estimate_tokens
    [(text.bytesize / Event::BYTES_PER_TOKEN.to_f).ceil, 1].max
  end
end

Instance Method Details

#token_costInteger

Returns token cost, using cached count or heuristic estimate.

Returns:

  • (Integer)

    token cost, using cached count or heuristic estimate



61
62
63
# File 'app/models/snapshot.rb', line 61

def token_cost
  token_count.positive? ? token_count : estimate_tokens
end