Class: ClaudeMemory::Dashboard::Moments

Inherits:
Object
  • Object
show all
Defined in:
lib/claude_memory/dashboard/moments.rb

Overview

Turns the flat activity_events log into enriched “moments” — the user-visible primitive for the feed-first dashboard. Each moment inlines the data needed to render its card (content preview, linked facts, resolved top_fact_ids) so the client never needs a second round trip per row.

A moment’s :kind is a stable narrative category the client uses to pick a card renderer. It’s derived from event_type + status so the client doesn’t have to re-derive the same mapping.

Constant Summary collapse

DEFAULT_LIMIT =
50
CONTENT_PREVIEW_BYTES =
800
FEED_EVENT_TYPES =
%w[hook_context recall store_extraction hook_ingest hook_sweep].freeze
KIND_TO_EVENT_TYPES =

Kind → underlying event_type(s). Used to pull only relevant rows from the DB when the caller specifies kinds; without this, a noisy stream of ingests pushes the value moments past the query limit.

{
  "context_injection" => %w[hook_context],
  "context_skipped" => %w[hook_context],
  "recall_hit" => %w[recall],
  "recall_empty" => %w[recall],
  "extraction" => %w[store_extraction],
  "ingest" => %w[hook_ingest],
  "ingest_skipped" => %w[hook_ingest],
  "sweep" => %w[hook_sweep]
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(manager) ⇒ Moments

Returns a new instance of Moments.



34
35
36
# File 'lib/claude_memory/dashboard/moments.rb', line 34

def initialize(manager)
  @manager = manager
end

Instance Method Details

#list(params = {}) ⇒ Object

Parameters:

  • params (Hash) (defaults to: {})

    “limit” — max moments (default 50, clamped 1..200) “before” — ISO 8601 cursor; return moments strictly older than this “kinds” — comma-separated kinds to include (default: all feed kinds)



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 'lib/claude_memory/dashboard/moments.rb', line 42

def list(params = {})
  store = default_store
  return empty_response unless store

  limit = (params["limit"] || DEFAULT_LIMIT).to_i.clamp(1, 200)
  before = params["before"]
  kinds = parse_kinds(params["kinds"])

  event_types = resolve_event_types(kinds)
  dataset = store.activity_events
    .where(event_type: event_types)
    .order(Sequel.desc(:occurred_at))
  dataset = dataset.where { occurred_at < before } if before && !before.empty?

  # Fetch up to 2x limit so per-kind filtering still produces a full
  # page (e.g. recall_hit vs recall_empty both live under event_type=recall).
  rows = dataset.limit(limit * 2).all
  events = rows.map { |r|
    r[:details] = r[:detail_json] ? JSON.parse(r[:detail_json], symbolize_names: true) : nil
    r.delete(:detail_json)
    r
  }

  moments = events.map { |e| build_moment(store, e) }
  moments = moments.select { |m| kinds.include?(m[:kind]) } unless kinds.empty?
  has_more = moments.size > limit
  moments = moments.first(limit)
  attach_feedback(store, moments)

  {
    moments: moments,
    next_before: moments.last&.dig(:occurred_at),
    has_more: has_more
  }
end