Packages
fnord
0.9.16
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/memory_consolidation.ex
defmodule Services.MemoryConsolidation do
@moduledoc """
Short-lived GenServer that coordinates memory consolidation within a single
scope. Owns the precomputed similarity matrix and the set of remaining
memories, serving as the single-threaded source of truth for which memories
are still available.
Workers call `checkout/1` to get a focus memory and its live candidates,
then `complete/3` to report which memories were consumed. The pool filters
candidates against `remaining` at checkout time, so workers always get a
fresh view of what's available.
Started per-scope by `Memory.Consolidator` and stopped when the
consolidation pass is complete.
"""
use GenServer
# Cosine similarity floor. Candidates below this score are not sent to the
# agent at all -- they're too dissimilar to be worth evaluating.
@similarity_floor 0.25
# Maximum candidates to send per focus memory.
@max_candidates 10
# --------------------------------------------------------------------------
# State
# --------------------------------------------------------------------------
defstruct [
# MapSet of {scope, slug} keys for memories not yet checked out or consumed.
:remaining,
# MapSet of {scope, slug} keys currently checked out to workers.
:in_flight,
# %{ {scope, slug} => Memory.t() } for quick lookup by slug key.
:memories,
# %{ {scope, slug} => [{candidate_slug_key, score}] } -- precomputed top
# candidates per memory from the similarity matrix.
:similarity,
# Running report counters.
:report
]
# --------------------------------------------------------------------------
# Public API
# --------------------------------------------------------------------------
@doc "Start a worker pool for the given list of memories."
@spec start_link(list(Memory.t())) :: GenServer.on_start()
def start_link(memories) do
GenServer.start_link(__MODULE__, memories)
end
@doc """
Check out a focus memory and its live candidates. Returns one of:
- `{:ok, focus, candidates}` -- work to do
- `{:skip, focus}` -- focus had no live candidates, already counted as kept
- `:done` -- no more memories to process
"""
@spec checkout(GenServer.server()) ::
{:ok, Memory.t(), list(map())} | {:skip, Memory.t()} | :done
def checkout(server) do
GenServer.call(server, :checkout, :infinity)
end
@doc """
Report that a focus memory has been processed. `result` is one of:
- `{:ok, eaten_slugs}` -- focus kept, these candidates were consumed
- `{:delete, eaten_slugs}` -- focus itself was deleted, plus these candidates
- `{:error, eaten_slugs}` -- processing failed, candidates may still have been consumed
"""
@spec complete(GenServer.server(), Memory.t(), {atom(), list()}) :: :ok
def complete(server, focus, result) do
GenServer.call(server, {:complete, focus, result}, :infinity)
end
@doc "Get the final consolidation report."
@spec report(GenServer.server()) :: map()
def report(server) do
GenServer.call(server, :report)
end
# --------------------------------------------------------------------------
# GenServer callbacks
# --------------------------------------------------------------------------
@impl GenServer
def init(memories) do
memory_map = build_memory_map(memories)
similarity = build_similarity_matrix(memories, memory_map)
remaining = memory_map |> Map.keys() |> MapSet.new()
state = %__MODULE__{
remaining: remaining,
in_flight: MapSet.new(),
memories: memory_map,
similarity: similarity,
report: %{merged: 0, deleted: 0, kept: 0, errors: 0}
}
{:ok, state}
end
@impl GenServer
# Checkout: find the next remaining memory that isn't in-flight, filter its
# candidates against the current remaining set, and hand it to the worker.
def handle_call(:checkout, _from, state) do
case pick_next(state) do
nil ->
{:reply, :done, state}
{slug_key, focus} ->
candidates = live_candidates(slug_key, state)
state = %{state | in_flight: MapSet.put(state.in_flight, slug_key)}
if candidates == [] do
# No live candidates -- mark as kept immediately, no LLM call needed.
state = %{
state
| remaining: MapSet.delete(state.remaining, slug_key),
in_flight: MapSet.delete(state.in_flight, slug_key),
report: bump(state.report, :kept)
}
{:reply, {:skip, focus}, state}
else
{:reply, {:ok, focus, candidates}, state}
end
end
end
# Complete: worker finished processing a focus memory. Update remaining set
# and report counters based on what happened.
def handle_call({:complete, focus, result}, _from, state) do
focus_key = slug_key(focus)
{status, eaten_slugs} =
case result do
{:ok, eaten} -> {:kept, eaten}
{:delete, eaten} -> {:deleted, eaten}
{:error, eaten} -> {:error, eaten}
end
# Remove focus from in-flight and remaining.
state = %{
state
| in_flight: MapSet.delete(state.in_flight, focus_key),
remaining: MapSet.delete(state.remaining, focus_key)
}
# Remove eaten candidates from remaining.
state =
Enum.reduce(eaten_slugs, state, fn eaten_key, st ->
%{
st
| remaining: MapSet.delete(st.remaining, eaten_key),
in_flight: MapSet.delete(st.in_flight, eaten_key)
}
end)
# Update report counters.
report = state.report
report =
case status do
:kept -> bump(report, :kept)
:deleted -> bump(report, :deleted)
:error -> bump(report, :errors)
end
# Each eaten slug counts as a merge -- the focus absorbed it or it was
# explicitly deleted. The worker already applied the specific action on
# disk; from the coordinator's perspective they're all consumed.
report =
Enum.reduce(eaten_slugs, report, fn _, r -> bump(r, :merged) end)
{:reply, :ok, %{state | report: report}}
end
def handle_call(:report, _from, state) do
{:reply, state.report, state}
end
# --------------------------------------------------------------------------
# Similarity matrix
# --------------------------------------------------------------------------
# Build a map of slug_key => Memory struct for O(1) lookup.
defp build_memory_map(memories) do
Map.new(memories, fn mem -> {slug_key(mem), mem} end)
end
# Precompute the similarity matrix: for each memory, find its top candidates
# above the floor. This is O(n²) dot products but runs once at startup and
# avoids repeated computation during the consolidation pass.
defp build_similarity_matrix(memories, memory_map) do
Map.new(memory_map, fn {key, mem} ->
candidates =
case mem.embeddings do
nil ->
[]
needle ->
memories
|> Enum.reject(fn m ->
slug_key(m) == key or is_nil(m.embeddings) or me_memory?(m)
end)
|> Enum.map(fn m ->
score = AI.Util.cosine_similarity(needle, m.embeddings)
{slug_key(m), score}
end)
|> Enum.filter(fn {_, score} -> score >= @similarity_floor end)
|> Enum.sort_by(fn {_, score} -> score end, :desc)
|> Enum.take(@max_candidates)
end
{key, candidates}
end)
end
# --------------------------------------------------------------------------
# Checkout helpers
# --------------------------------------------------------------------------
# Pick the next memory from remaining that isn't currently in-flight.
defp pick_next(state) do
state.remaining
|> MapSet.difference(state.in_flight)
|> Enum.at(0)
|> case do
nil -> nil
key -> {key, Map.fetch!(state.memories, key)}
end
end
# Filter a memory's precomputed candidates against what's still remaining
# and not in-flight. Returns enriched candidate maps ready for the agent.
defp live_candidates(focus_key, state) do
state.similarity
|> Map.get(focus_key, [])
|> Enum.filter(fn {cand_key, _score} ->
MapSet.member?(state.remaining, cand_key) and
not MapSet.member?(state.in_flight, cand_key)
end)
|> Enum.map(fn {cand_key, score} ->
mem = Map.fetch!(state.memories, cand_key)
%{
memory: mem,
score: score,
tier: Memory.Consolidator.tier_label(score)
}
end)
end
# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
defp slug_key(%Memory{scope: scope, slug: slug}) when is_binary(slug), do: {scope, slug}
defp slug_key(%Memory{scope: scope, title: title}),
do: {scope, Memory.title_to_slug(title)}
defp bump(report, key), do: Map.update!(report, key, &(&1 + 1))
# The "Me" identity memory must never appear as a candidate for merge or
# deletion. It can be a focus (so it absorbs others), but nothing eats it.
defp me_memory?(%Memory{scope: :global, title: "Me"}), do: true
defp me_memory?(_), do: false
end