Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e7b0c28
docs(s032): the owner's decisions at G0, the embedders and the vector…
HackTuah Sep 21, 2026
6a6d5cf
feat(s032): semantic memory: embedder, vector stores, observer, hybri…
HackTuah Sep 21, 2026
062cb12
feat(s032): the smoke path reports EXLA, a vector search in the binar…
HackTuah Sep 21, 2026
31a793b
test(s012): red: a whitespace-only assistant text before tool calls l…
HackTuah Sep 21, 2026
87a8f53
fix(s012): persist the assistant row when its text is whitespace only
HackTuah Sep 21, 2026
f6fc944
test(s011): red: the adapter hands req_llm a tool-call tuple it refuses
HackTuah Sep 21, 2026
4f47964
fix(s011): encode assistant tool calls for req_llm as maps with name …
HackTuah Sep 21, 2026
8bf06fb
feat(s032): exla loaded on demand in the release, the smoke probe aft…
HackTuah Sep 21, 2026
8cfac84
docs(s032): docs/perf.md and scripts/vector_bench.exs; the AC6 proof …
HackTuah Sep 21, 2026
352f55f
test(s032): the supervisor without and with the serving; AC2 automate…
HackTuah Sep 21, 2026
372e05d
test(s032): the supervisor's reason line is captured at info
HackTuah Sep 21, 2026
fa019cf
test(s032): two flakes met on run 35603385277 closed at their source
HackTuah Sep 21, 2026
a8fa7e9
feat(s032): exla declared runtime: false so the release can carry it …
HackTuah Sep 21, 2026
53cb840
docs(s032): G3 findings and follow-ups in NOTES; the exla row says ho…
HackTuah Sep 21, 2026
de55660
fix(s032): pgvector searches with an iterative HNSW scan so a filtere…
HackTuah Sep 21, 2026
d9b2686
test(s032): the scoring-agreement test starts exla on demand like the…
HackTuah Sep 21, 2026
732238c
feat(s032): the exla reason unwrapped and summarised to the NIF's und…
HackTuah Sep 21, 2026
eda618f
test(s024): red: the signer alarm is not cleared when a signer signs …
HackTuah Sep 21, 2026
ab07781
fix(s024): a signer that signs again clears the signer alarm
HackTuah Sep 21, 2026
a954393
feat(s032): complete slice 032 (semantic memory and hybrid retrieval)
HackTuah Sep 21, 2026
a82bd4c
fix(s032): the smoke run asked for by TRINITY_SMOKE=1, so Kernel.CLI …
HackTuah Sep 21, 2026
193800e
fix(s032): the test pools queue instead of shedding under the stress …
HackTuah Sep 21, 2026
3f891ff
docs(s032): the closing correction: the final tree, its runs, the cov…
HackTuah Sep 21, 2026
cb0dc62
docs(s032): finding 16, the archive round-trip flake on the fips leg,…
HackTuah Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
# Slice 032: pgvector's image of Postgres 17, so the vector extension exists for the
# semantic memory's Postgres store.
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: trinity
POSTGRES_PASSWORD: trinity
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,20 @@ jobs:
ours() { ps -eo pid,ppid,comm | grep -E 'burrito|beam\.smp|erl_child_setup|desktop_' | grep -v grep || true; }
ours > ps-before.txt
chmod +x "burrito_out/${{ matrix.artifact }}"
"./burrito_out/${{ matrix.artifact }}" --no-halt --smoke | tee smoke.log
# Slice 032: the variable, not `--smoke`: Kernel.CLI read the argument as a file
# ("No file named --smoke", exit 1) and won the race against the halting Task on
# macOS in run 35608084951.
TRINITY_SMOKE=1 "./burrito_out/${{ matrix.artifact }}" --no-halt | tee smoke.log
sleep 3
ours > ps-after.txt
grep -q '^TRINITY_SMOKE_PORT=[0-9][0-9]*$' smoke.log
# Slice 013: the renderer's NIF loaded and rendered in this binary (exit 3 otherwise).
grep -q '^TRINITY_SMOKE_MARKDOWN=ok$' smoke.log
# Slice 032, AC7: a fake-vector search ran inside this binary through the vector
# store in force (exit 4 otherwise). The EXLA and SEMANTIC lines are recorded, not
# asserted: whether the XLA library loads in Burrito's ERTS is what they measure.
grep -q '^TRINITY_SMOKE_VEC=ok:' smoke.log
grep -E '^TRINITY_SMOKE_(EXLA|SEMANTIC)=' smoke.log
echo "--- ours, before ---"; cat ps-before.txt
echo "--- ours, after ----"; cat ps-after.txt
diff ps-before.txt ps-after.txt
Expand All @@ -207,14 +215,20 @@ jobs:
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
& ".\burrito_out\${{ matrix.artifact }}" --no-halt --smoke | Tee-Object smoke.log
$env:TRINITY_SMOKE = "1"
& ".\burrito_out\${{ matrix.artifact }}" --no-halt | Tee-Object smoke.log
if ($LASTEXITCODE -ne 0) { throw "smoke exited $LASTEXITCODE" }
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_PORT=\d+$')) {
throw "no port line"
}
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_MARKDOWN=ok$')) {
throw "the markdown renderer did not render in this binary (slice 013)"
}
# Slice 032, AC7, as on the other two: the vector search binds, the other two lines are recorded.
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_VEC=ok:')) {
throw "the vector search did not run inside this binary (slice 032)"
}
Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_(EXLA|SEMANTIC)='

# AC1's property on a runner, and AC6's per-OS cold start, which used to be skipped.
# `bash` on every OS, Windows included: the job that never curled was the one gap in
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ standards register names the rows that ask for them.
| 026 | Store-and-forward receipts for disconnected operation | 2 Tools | L | 024 | planned |
| 030 | Persona (SOUL) + always-on memory tier | 3 Memory | M | 012 | approved |
| 031 | Session search (SQLite FTS5) | 3 Memory | S | 010 | approved |
| 032 | Embeddings + semantic memory + hybrid retrieval | 3 Memory | L | 031 | planned |
| 032 | Embeddings + semantic memory + hybrid retrieval | 3 Memory | L | 031 | done |
| 033 | Project context: AGENTS.md | 3 Memory | S | 030, 022 | approved |
| 034 | Export, import, restore | 3 Memory | S | 030, 031 | approved |
| 040 | Skills registry + agentskills.io format + progressive disclosure | 4 Skills | M | 020 | planned |
Expand Down
11 changes: 6 additions & 5 deletions VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ never pin a version hex marks as retired or vulnerable.
| `ecto_sql` | ~> 3.13 | ✅ in `mix.lock` | |
| `ecto_sqlite3` | >= 0.0.0 | ✅ in `mix.lock` | Primary DB. FTS5 available. |
| `postgrex` | >= 0.0.0 (optional) | ✅ in `mix.lock` | Secondary DB driver, `optional: true` so the desktop build carries none of it; compiled in only under `TRINITY_DB=postgres`, which the CI job proves. Added at Slice 010. Was one row with pgvector; pgvector keeps its own row below. |
| `pgvector` | optional, ~> 0.3 | 🔍 not yet a dependency | Vectors on the Postgres path. Not yet a dependency; Slice 032 decides. Split from the postgrex row at Slice 010. |
| `pgvector` | ~> 0.4.1 | ✅ in `mix.lock` | Vectors on the Postgres path: `memories.embedding_vector vector(384)` under an HNSW cosine index, searched by `Trinity.Memory.VectorStores.Pgvector` (Slice 032). The postgres job runs on the `pgvector/pgvector:pg17` image. Split from the postgrex row at Slice 010. |
| `oban` | ~> 2.24 | 🔍 not yet a dependency | Uses `Oban.Engines.Lite` on SQLite. ⚠️ Oban Pro Workflows/Smart engine are Postgres-only. Added at Slice 050. |
| `req` | ~> 0.5 | ✅ in `mix.lock` | HTTP client. |
| `req_llm` | ~> 1.22 | ✅ in `mix.lock` | Provider layer (streaming, tools, structured output, usage). ⚠️ The pin was `~> 1.10` against a recorded latest of 1.10.0; the real latest was twelve minors ahead. Check event shapes against the current version at Slice 011, not against this file's prose. Added at Slice 011. |
Expand Down Expand Up @@ -118,10 +118,11 @@ never pin a version hex marks as retired or vulnerable.

| Name | Pin | Verified | Note |
|---|---|---|---|
| `nx, exla` | latest stable | 🔍 not a single package | Local embeddings. EXLA binary size matters for desktop: measure in 032. Two packages, so no single lock key. |
| `bumblebee` | ~> 0.7 | 🔍 not yet a dependency | `all-MiniLM-L6-v2` embeddings; Whisper later. Added at Slice 032. |
| `sqlite_vec` | ~> 0.1 | 🔍 not yet a dependency | Vectors in SQLite. Verify the loadable extension works inside the Burrito bundle (Slice 032). ⚠️ Pre-1.0, no release in roughly 22 months, 6,938 downloads all-time. R11's trigger already fires. Decide the fallback before Slice 032 starts. |
| `hnswlib` | ~> 0.1.7 | 🔍 not yet a dependency | ⚠️ Pre-1.0. Optional accelerator; not on the critical path. |
| `nx` | ~> 0.13.1 | ✅ in `mix.lock` | Tensors for the local embedder (Slice 032). The 0.13 line, not 1.0.0 (2026-09-10): bumblebee 0.7.1 accepts `~> 0.12 or ~> 0.13` and has no release for 1.0. Measured at Slice 032: docs/perf.md. |
| `exla` | ~> 0.13.1 | ✅ in `mix.lock` | The XLA backend the serving compiles to (Slice 032); precompiled for x86_64 and aarch64 Linux and macOS, none for Windows (not declared on a Windows host; the tier is off there). Declared `runtime: false` and carried by the release in `:load` mode, started on demand by `Trinity.Memory.Embedders.Bumblebee.exla/0`: its NIF does not load in Burrito's Linux ERTS (NOTES finding 2). The XLA shared library is 463 MB on disk: docs/perf.md. Pulls `xla` 0.10.0 and `fine`. |
| `bumblebee` | ~> 0.7.1 | ✅ in `mix.lock` | `all-MiniLM-L6-v2` embeddings through `Trinity.Memory.Embedders.Bumblebee` (Slice 032); Whisper later. Pulls `axon` 0.8.1 and `tokenizers` 0.5.1 (a precompiled Rust NIF). |
| `sqlite_vec` | not used (decided at Slice 032, 2026-09-21) | 🔍 not a single package | Vectors in SQLite. Its only release (0.1.0, 2024-11-19) requires `nx ~> 0.9` and cannot sit in a tree with nx 0.13; R11's trigger fired and the owner chose brute force in Elixir over the persona's semantic rows (`Trinity.Memory.VectorStores.Brute`, docs/02: fine to about 10^5) with pgvector on the Postgres path. The row stays so the decision is visible where a reader would look for the package. |
| `hnswlib` | ~> 0.1.10 | 🔍 not yet a dependency | ⚠️ Pre-1.0 (0.1.10, 2026-09-17, active). The scale option after brute force; not yet a dependency, the slice that measures brute force past 10^5 rows adds it. |

### Tools, sandbox and desktop

Expand Down
4 changes: 3 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ config :trinity, :tools,
Trinity.Tools.Web.Search,
# Slice 031: full-text search over past messages.
Trinity.Tools.SessionSearch,
# Slice 032: hybrid recall over semantic memories and past messages.
Trinity.Tools.Recall,
# Slice 030: the always-on memory tiers.
Trinity.Tools.Memory,
Trinity.Tools.Shell.Run
Expand All @@ -82,7 +84,7 @@ config :trinity, :tools,
web: ["web_fetch", "web_search"],
shell: ["shell"],
# Slice 031: search over past conversations.
memory: ["session_search", "memory"]
memory: ["session_search", "recall", "memory"]
}

# Slice 022: the filesystem roots beside the data directory (always a root) and the session's
Expand Down
8 changes: 6 additions & 2 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import Config
# which is the same call: this file is evaluated before the application is available under
# Mix, so it may not call project modules. `Burrito.Util.Args.get_arguments/0` reads the same
# thing.
smoke? = "--smoke" in Enum.map(:init.get_plain_arguments(), &to_string/1)
smoke? =
"--smoke" in Enum.map(:init.get_plain_arguments(), &to_string/1) or
System.get_env("TRINITY_SMOKE") == "1"

if System.get_env("PHX_SERVER") || smoke? do
config :trinity, TrinityWeb.Endpoint, server: true
Expand Down Expand Up @@ -119,7 +121,9 @@ if config_env() == :prod do
if System.get_env("TRINITY_DB") == "postgres" do
config :trinity, Trinity.Repo,
url: System.get_env("DATABASE_URL") || raise("TRINITY_DB=postgres needs DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
# Slice 032: pgvector's `vector` type.
types: Trinity.Repo.PostgrexTypes

config :trinity, Trinity.Repo.Receipts,
url: System.get_env("DATABASE_URL"),
Expand Down
31 changes: 26 additions & 5 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ config :trinity, :tools,
Trinity.Tools.Web.Search,
# Slice 031: full-text search over past messages.
Trinity.Tools.SessionSearch,
# Slice 032: hybrid recall.
Trinity.Tools.Recall,
# Slice 030: the always-on memory tiers.
Trinity.Tools.Memory,
Trinity.Tools.Shell.Run
Expand All @@ -37,7 +39,7 @@ config :trinity, :tools,
web: ["web_fetch", "web_search"],
shell: ["shell"],
# Slice 031: search over past conversations.
memory: ["session_search", "memory"]
memory: ["session_search", "recall", "memory"]
},
timeout_ms: 2_000

Expand Down Expand Up @@ -97,21 +99,36 @@ if System.get_env("TRINITY_DB") == "postgres" do
config :trinity, Trinity.Repo,
url: System.get_env("DATABASE_URL") || raise("TRINITY_DB=postgres needs DATABASE_URL"),
pool_size: 10,
pool: Ecto.Adapters.SQL.Sandbox
pool: Ecto.Adapters.SQL.Sandbox,
# Slice 032: pgvector's `vector` type.
types: Trinity.Repo.PostgrexTypes,
# Slice 032: the sandbox shares one connection with every process a test starts, and
# DBConnection sheds load (refuses checkouts) when the queue stays over `queue_target`
# for `queue_interval`. The 100-session and 20-writer tests queue that hard on the
# postgres job's runner, more so once each turn also runs the retriever (runs 35610164189
# and 35610171809); a queue that is long on purpose should wait, not shed.
queue_target: 5_000,
queue_interval: 30_000

# Slice 024: the receipts Repo shares the Postgres database (its own migrations table).
config :trinity, Trinity.Repo.Receipts,
url: System.get_env("DATABASE_URL"),
pool_size: 10,
pool: Ecto.Adapters.SQL.Sandbox
pool: Ecto.Adapters.SQL.Sandbox,
queue_target: 5_000,
queue_interval: 30_000
else
config :trinity, Trinity.Repo,
database: Path.expand("../trinity_test.db", __DIR__),
pool: Ecto.Adapters.SQL.Sandbox
pool: Ecto.Adapters.SQL.Sandbox,
queue_target: 5_000,
queue_interval: 30_000

config :trinity, Trinity.Repo.Receipts,
database: Path.expand("../trinity_test_receipts.db", __DIR__),
pool: Ecto.Adapters.SQL.Sandbox
pool: Ecto.Adapters.SQL.Sandbox,
queue_target: 5_000,
queue_interval: 30_000
end

# We don't run a server during test. If one is required,
Expand Down Expand Up @@ -150,3 +167,7 @@ config :phoenix,
# Slice 024: the receipt signing key and registry for the suite live under the project's
# ignored tmp/, never in the data directory of the machine running the tests.
config :trinity, :receipts, keys_dir: Path.expand("../tmp/test_keys", __DIR__)

# Slice 032: the suite embeds with the deterministic fake; nothing leaves the machine and no
# model is needed.
config :trinity, :memory, embedder: :fake, observer: false
1 change: 1 addition & 0 deletions coverage.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ slice_id percent sha date
030 78.40 c657da8 2026-09-21
033 78.50 3b5e89e 2026-09-21
034 78.83 3a5785f 2026-09-21
032 78.99 193800e 2026-09-21
12 changes: 9 additions & 3 deletions docs/01-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ Trinity.Application
│ # INSERT but does not guarantee each row read the correct predecessor.
├── Trinity.Effects.Boot # Slice 024, as built: a transient Task writing the boot receipt
│ # once the signer and the authority are known
├── Trinity.Memory.Supervisor # Nx.Serving for embeddings, retrieval. Slice 032
├── Trinity.Memory.Supervisor # Slice 032, as built: Trinity.Memory.TaskSupervisor (the
│ │ # observer's task after each turn, the page's model download)
│ └── Nx.Serving (Trinity.Memory.Embedding) # the local embedder, started only when the embedder is
│ # :local and the model is in the cache; absent otherwise, the
│ # tier off and the reason logged once. ensure_embedding/0
│ # starts it after a download.
├── Trinity.Skills.Registry # hot-loaded skills index. Slice 040
├── Oban # cron + durable jobs. Slice 050
├── Trinity.MCP.Supervisor # MCP clients (one per server, lib per 059) + server. Slice 059-062
Expand Down Expand Up @@ -106,7 +111,8 @@ through those modules only. `boundary` `exports:` lists enforce this.
| `Trinity.LLM.Provider` | `stream/3`, `generate/3`, `embed/2`, `models/0`, `capabilities/1` | config `:providers` list |
| `Trinity.Tools.Tool` | `name/0`, `description/0`, `schema/0`, `risk/0`, `execute/2` | config `:tools` list + MCP dynamic registration |
| `Trinity.Gateways.Adapter` | `child_spec/1`, `deliver/2`, `capabilities/0` | config `:gateways` list |
| `Trinity.Memory.VectorStore` | `upsert/2`, `search/3`, `delete/1` | config `:vector_store` (`SqliteVec` \| `Pgvector` \| `Hnswlib`) |
| `Trinity.Memory.VectorStore` | `upsert/3`, `search/3`, `delete/1`, `count/1` (as built at 032: the scope filter is `search/3`'s required argument) | the database adapter, at compile time (`Brute` on SQLite, `Pgvector` on Postgres; `Hnswlib` when a slice measures past 10^5 rows) |
| `Trinity.Memory.Embedder` | `embed/1`, `dim/0`, `model_id/0`, `availability/0` | `config :trinity, :memory, embedder:` (`:local` default, `:hosted` opt-in only, `:fake` in the suite) |
| `Trinity.Skills.Loader` | `load/1`, `validate/1` | config `:skill_loaders` |
| `Trinity.Permissions.Policy` | `decide/3` → `:allow | :deny | {:ask, prompt}` | config |

Expand All @@ -120,7 +126,7 @@ UI/Gateway ──user_message──▶ Session(gen_statem)
Session: append message → build prompt (persona + always-on memory + skills index + history[compacted])
→ LLM.stream/3 ──chunks──▶ PubSub "session:<id>" ──▶ LiveView + gateways
→ on tool_call: Permissions.decide → (ask → approval_wait) → Tools.execute in Task → result appended
→ loop until final text → persist → Memory.observe(turn) (async) → idle
→ loop until final text → persist → Memory.Observer.observe(turn) (a Task under Trinity.Memory.TaskSupervisor, as built at 032) → idle
```

Every state transition is persisted before it is broadcast. A crash between persist and broadcast is safe
Expand Down
1 change: 1 addition & 0 deletions docs/02-tech-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Versions live in `VERSIONS.md`. This file explains *why* each choice was made an
| MCP | decided by Slice 059 (fastest_mcp / gen_mcp / anubis_mcp / own server) | Target is 2026-07-28 with 2025-11-25 compat; anubis is ≤ 2025-11-25 and LGPL-3.0 | ADR-0007 finalised by measurement |
| Embeddings | Bumblebee + EXLA (all-MiniLM-L6-v2) | Local, private, 384-dim | If EXLA binary size is unacceptable on desktop → hosted embeddings via req_llm, or Ortex (risk: stalled) |
| Vector search | sqlite_vec (brute force) behind `VectorStore` behaviour | Fine to ~10^5 vectors; no extra process | Scale → hnswlib (pre-1.0) or pgvector HNSW |
| Vector search, as built at 032 | brute force in Elixir over the row's `embedding` bytes (SQLite); pgvector HNSW (Postgres) | sqlite_vec's one release needs `nx ~> 0.9` and cannot share a tree with nx 0.13 (VERSIONS.md); measured in docs/perf.md | Scale → hnswlib 0.1.10 |
| Shell tool | MuonTrap | Guaranteed child kill on process death; cgroups on Linux | none |
| Sandbox | Luerl (`sandbox` pkg) | In-VM, reduction-limited, no OS access | Untrusted native code → container/microVM (out of scope) |
| Modularity | `boundary` + behaviours + `Registry` | Compile-time enforcement of context deps | none |
Expand Down
16 changes: 16 additions & 0 deletions docs/05-data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ vocabulary is `global | persona:<id> | project:<path> | session:<id>` (a session
`persona`, `global`), `key` is required for the two always-on tiers (`^[a-z0-9][a-z0-9_.-]{0,63}$`, unique with
tier and scope), and the semantic tier's columns wait for 032. Two tables beside it:

As built at slice 032 (the semantic tier): no `memories_vec` virtual table and no `sqlite_vec` (its only release
needs `nx ~> 0.9`; NOTES decision 1). The vector is three columns on the row, on both databases:
`embedding` (binary: float32, little-endian, `dim * 4` bytes), `embedding_model` (the embedder that produced it,
e.g. `bumblebee:sentence-transformers/all-MiniLM-L6-v2`, `fake:sha256-384` in the suite) and `embedding_dim`
(integer), all nullable, under an index on (`persona_id`, `tier`, `embedding_model`). On Postgres the migration
also creates the `vector` extension, a fourth column `embedding_vector vector(384)` and an HNSW cosine index on
it where `tier = 'semantic'`; `Trinity.Memory.VectorStores.Pgvector` keeps it in step with the bytes column and
searches it with `<=>`, and `VectorStores.Brute` (SQLite) loads the filter's rows and scores them in Elixir.
A search takes the scope list as a required argument (M6) and runs only over rows whose `embedding_model` is the
embedder in force's: models are never mixed, a change of embedder is a re-embed. Semantic rows are written by
`Trinity.Memory.Semantic.add/2` (the observer after a turn, `by: "observer"`; the page, `by: "ui"`), keyed
`<slug of the body>-<6 hex of its SHA-256>`, and every write is a `memory_changes` row (`add`, `remove`, `pin`).
The change log's `action` vocabulary grows by `pin` (a semantic memory promoted to `always_on` through
`AlwaysOn.add/2`, so the budget applies) and `by` by `observer`. Recall marks its memory hits' `last_used_at`,
which the retriever's recency decay reads.

### memory_changes (Slice 030)
`persona_id`, `action` (add | replace | remove | promote | consolidate), `tier`, `scope`, `key`, `before`,
`after`, `by` (tool | ui | consolidator), `session_id`, `proposal_id`, `inserted_at`. Every write to the
Expand Down
Loading
Loading