Packages
fnord
0.8.74
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/services/memories.ex
defmodule Services.Memories do
@moduledoc """
GenServer maintaining the in-memory graph of all loaded memories.
Tracks current project context and provides client API for CRUD operations.
State includes both global and project-scoped memories when a project is selected.
"""
use GenServer
defstruct [
:project_name,
:memories,
:by_id,
:by_slug,
:roots
]
@type t :: %__MODULE__{
project_name: String.t() | nil,
memories: [AI.Memory.t()],
by_id: %{String.t() => AI.Memory.t()},
by_slug: %{String.t() => AI.Memory.t()},
roots: [AI.Memory.t()]
}
# ----------------------------------------------------------------------------
# Client API
# ----------------------------------------------------------------------------
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Gets all loaded memories (global + project if project selected).
"""
@spec get_all() :: [AI.Memory.t()]
def get_all() do
GenServer.call(__MODULE__, :get_all)
end
@doc """
Gets only root memories (parent_id: nil).
"""
@spec get_roots() :: [AI.Memory.t()]
def get_roots() do
GenServer.call(__MODULE__, :get_roots)
end
@doc """
Gets children of a specific memory by ID.
"""
@spec get_children(String.t()) :: [AI.Memory.t()]
def get_children(parent_id) do
GenServer.call(__MODULE__, {:get_children, parent_id})
end
@doc """
Gets a memory by slug.
"""
@spec get_by_slug(String.t()) :: AI.Memory.t() | nil
def get_by_slug(slug) do
GenServer.call(__MODULE__, {:get_by_slug, slug})
end
@doc """
Gets a memory by ID.
"""
@spec get_by_id(String.t()) :: AI.Memory.t() | nil
def get_by_id(id) do
GenServer.call(__MODULE__, {:get_by_id, id})
end
@doc """
Creates a new memory. Validates parent scope matches if parent_id provided.
Persists to disk and updates in-memory state.
"""
@spec create(AI.Memory.t()) :: :ok | {:error, term}
def create(memory) do
GenServer.call(__MODULE__, {:create, memory})
end
@doc """
Updates an existing memory. Persists to disk and updates in-memory state.
"""
@spec update(AI.Memory.t()) :: :ok | {:error, term}
def update(memory) do
GenServer.call(__MODULE__, {:update, memory})
end
@doc """
Deletes a memory. Removes from disk and in-memory state.
"""
@spec delete(String.t()) :: :ok | {:error, term}
def delete(memory_id) do
GenServer.call(__MODULE__, {:delete, memory_id})
end
@doc """
Sets the current project context and reloads memories.
"""
@spec set_project(String.t() | nil) :: :ok
def set_project(project_name) do
GenServer.call(__MODULE__, {:set_project, project_name})
end
@doc """
Reloads all memories from disk.
"""
@spec reload() :: :ok
def reload() do
GenServer.call(__MODULE__, :reload)
end
# ----------------------------------------------------------------------------
# Server Callbacks
# ----------------------------------------------------------------------------
@impl true
def init(_opts) do
# Initialize store if needed
unless Store.Memories.initialized?() do
Store.Memories.init()
end
# Load initial state with no project context
project_name = get_current_project()
memories = Store.Memories.load_all(project_name)
state = build_state(project_name, memories)
{:ok, state}
end
@impl true
def handle_call(:get_all, _from, state) do
{:reply, state.memories, state}
end
@impl true
def handle_call(:get_roots, _from, state) do
{:reply, state.roots, state}
end
@impl true
def handle_call({:get_children, parent_id}, _from, state) do
case Map.get(state.by_id, parent_id) do
nil ->
{:reply, [], state}
parent ->
children =
parent.children
|> Enum.map(&Map.get(state.by_slug, &1))
|> Enum.reject(&is_nil/1)
{:reply, children, state}
end
end
@impl true
def handle_call({:get_by_slug, slug}, _from, state) do
{:reply, Map.get(state.by_slug, slug), state}
end
@impl true
def handle_call({:get_by_id, id}, _from, state) do
{:reply, Map.get(state.by_id, id), state}
end
@impl true
def handle_call({:create, memory}, _from, state) do
with :ok <- validate_parent_scope(memory, state),
:ok <- check_slug_collision(memory),
:ok <- Store.Memories.create(memory) do
# Add child to parent if parent_id provided
if memory.parent_id do
case Map.get(state.by_id, memory.parent_id) do
nil -> :ok
parent -> Store.Memories.add_child(parent.slug, memory.slug, parent.scope)
end
end
# Reload to pick up new memory
memories = Store.Memories.load_all(state.project_name)
new_state = build_state(state.project_name, memories)
{:reply, :ok, new_state}
else
error -> {:reply, error, state}
end
end
@impl true
def handle_call({:update, memory}, _from, state) do
with :ok <- Store.Memories.Meta.write(memory),
:ok <- Store.Memories.Heuristic.write(memory) do
# Reload to pick up changes
memories = Store.Memories.load_all(state.project_name)
new_state = build_state(state.project_name, memories)
{:reply, :ok, new_state}
else
error -> {:reply, error, state}
end
end
@impl true
def handle_call({:delete, memory_id}, _from, state) do
case Map.get(state.by_id, memory_id) do
nil ->
{:reply, {:error, :not_found}, state}
memory ->
with :ok <- Store.Memories.delete(memory.slug, memory.scope) do
# Remove from parent's children list if it has a parent
if memory.parent_id do
case Map.get(state.by_id, memory.parent_id) do
nil -> :ok
parent -> Store.Memories.remove_child(parent.slug, memory.slug, parent.scope)
end
end
# Reload to remove from state
memories = Store.Memories.load_all(state.project_name)
new_state = build_state(state.project_name, memories)
{:reply, :ok, new_state}
else
error -> {:reply, error, state}
end
end
end
@impl true
def handle_call({:set_project, project_name}, _from, _state) do
memories = Store.Memories.load_all(project_name)
new_state = build_state(project_name, memories)
AI.Memory.debug("Project set to: #{inspect(project_name)}")
AI.Memory.debug("Loaded #{length(memories)} memories (#{length(new_state.roots)} roots)")
Enum.each(new_state.roots, fn mem ->
AI.Memory.debug(" - #{mem.slug} (#{mem.scope})")
end)
{:reply, :ok, new_state}
end
@impl true
def handle_call(:reload, _from, state) do
memories = Store.Memories.load_all(state.project_name)
new_state = build_state(state.project_name, memories)
{:reply, :ok, new_state}
end
# ----------------------------------------------------------------------------
# Private Helpers
# ----------------------------------------------------------------------------
defp get_current_project() do
case Settings.get_selected_project() do
{:ok, project} -> project
{:error, :project_not_set} -> nil
end
end
defp build_state(project_name, memories) do
by_id = memories |> Enum.map(&{&1.id, &1}) |> Map.new()
by_slug = memories |> Enum.map(&{&1.slug, &1}) |> Map.new()
roots = memories |> Enum.filter(&is_nil(&1.parent_id))
%__MODULE__{
project_name: project_name,
memories: memories,
by_id: by_id,
by_slug: by_slug,
roots: roots
}
end
defp validate_parent_scope(memory, state) do
if memory.parent_id do
case Map.get(state.by_id, memory.parent_id) do
nil ->
{:error, "Parent memory not found: #{memory.parent_id}"}
parent ->
if parent.scope == memory.scope do
:ok
else
{:error, "Parent scope (#{parent.scope}) must match child scope (#{memory.scope})"}
end
end
else
:ok
end
end
defp check_slug_collision(memory) do
if Store.Memories.exists?(memory.slug, memory.scope) do
{:error,
"Memory with label '#{memory.label}' already exists (slug: #{memory.slug}). Choose a more specific label."}
else
:ok
end
end
end