- Python 82.6%
- TypeScript 13.4%
- JavaScript 4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Qwen3.8-27B is the intended worker model; the sample config still named the Ornith-1.5 model. |
||
| docs/superpowers | ||
| plugin/opencode | ||
| reflect | ||
| systemd | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| AGENTS.md | ||
| icon.png | ||
| icon.svg | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| reflect.toml | ||
| uv.lock | ||
Reflect
Autonomous nightly extraction of "lessons learned" from Opencode session history, served to future sessions via an MCP server. Sessions are required to judge every lesson they read as useful or not useful, and that judgement — not mere retrieval — is what raises or lowers a lesson's strength. Lessons climb to core memories when repeatedly judged useful and decay when never needed again.
Cost note: Reflect deliberately spends tokens to build and maintain memory — the nightly worker reads session transcripts and an LLM summarizes them, and future sessions can consume lessons. Only run it where token cost is low, e.g. with local LLMs (like llama.cpp) rather than a metered hosted model.
Prerequisites
Setup
Pick the section that matches your deployment. All three share the first two steps (clone + config); they diverge at step 3.
1. Single machine (local)
Everything runs on one computer. The nightly worker reads that machine's opencode.db and
writes to a local lessons.db. Future opencode sessions on the same machine read lessons back
via a local MCP server.
# 1. Clone and install
git clone <repo-url> reflect
cd reflect
uv sync # installs dependencies into .venv
# 2. Configure
cp reflect.toml ~/.config/reflect/reflect.toml
# (edit: nothing mode-specific needed; defaults are local)
# 3. Register the read-only MCP server so opencode sessions can query lessons
opencode mcp add reflect -- uv run --project <path-to-reflect> reflect mcp
# 4. Enable the nightly timer
systemctl --user link <path-to-reflect>/systemd/reflect-nightly.service
systemctl --user link <path-to-reflect>/systemd/reflect-nightly.timer
systemctl --user enable --now reflect-nightly.timer
# 5. Verify a run end-to-end before trusting it
uv run reflect run --dry-run
uv run reflect run
2. Hub
The hub owns the shared lessons.db and serves two MCP servers over HTTP so client machines
can connect. The hub also runs the nightly extraction for its own sessions.
# 1. Clone and install (same as local)
git clone <repo-url> reflect
cd reflect
uv sync
# 2. Configure — set mode = "hub"
cp reflect.toml ~/.config/reflect/reflect.toml
# Edit ~/.config/reflect/reflect.toml:
# mode = "hub"
# (optionally set mcp_host to a non-loopback address if clients are remote)
# 3. Register the hub's own MCP server for this machine's opencode sessions
opencode mcp add reflect --url http://127.0.0.1:4010/mcp
# 4. Enable the nightly timer (extracts lessons from the hub's own sessions)
systemctl --user link <path-to-reflect>/systemd/reflect-nightly.service
systemctl --user link <path-to-reflect>/systemd/reflect-nightly.timer
systemctl --user enable --now reflect-nightly.timer
# 5. Start the two MCP servers that clients connect to
# (edit the %h paths in the .service files to match your layout first)
systemctl --user link <path-to-reflect>/systemd/reflect-mcp-read.service
systemctl --user link <path-to-reflect>/systemd/reflect-mcp-worker.service
systemctl --user enable --now reflect-mcp-read.service reflect-mcp-worker.service
# Optional but recommended when the hub serves non-loopback clients:
# echo 'Environment="REFLECT_MCP_TOKEN=<generated>"' >> ~/.config/systemd/user/reflect-mcp-read.service.d/token.conf
# echo 'Environment="REFLECT_MCP_WORKER_TOKEN=<generated>"' >> ~/.config/systemd/user/reflect-mcp-worker.service.d/token.conf
# systemctl --user daemon-reload
The hub's read server listens on mcp_host:mcp_port (default 0.0.0.0:4010).
The worker server listens on mcp_host:mcp_worker_port (default 0.0.0.0:4011).
Clients connect to the worker URL for writes and the read URL for queries.
Auth: set
REFLECT_MCP_TOKEN(read server) andREFLECT_MCP_WORKER_TOKEN(worker server) in the hub's environment to requireAuthorization: Bearer <token>on every request. Leave them unset for trusted-network-only deployments (no auth). Clients that setREFLECT_MCP_WORKER_TOKENsend it to the hub automatically, and the generated nightly agent config references it via{env:REFLECT_MCP_WORKER_TOKEN}— export the variable on the client so it resolves.
3. Client
A client (spoke) has no local lessons.db. It reads its own opencode.db, runs the nightly
worker against the hub's worker MCP server, and registers the hub's read server so its own
opencode sessions can query the shared lessons.
# 1. Clone and install (same as local)
git clone <repo-url> reflect
cd reflect
uv sync
# 2. Configure — set mode = "client" and point at the hub
cp reflect.toml ~/.config/reflect/reflect.toml
# Edit ~/.config/reflect/reflect.toml:
# mode = "client"
# hub_url = "http://<hub-hostname>:4011/mcp"
# If the hub requires tokens, export REFLECT_MCP_WORKER_TOKEN in the shell
# profile that launches the nightly timer (systemd user units inherit the
# user environment via ~/.config/environment.d/ or default.target env).
# 3. Register the hub's read server for this machine's opencode sessions
opencode mcp add reflect --url http://<hub-hostname>:4010/mcp
# 4. Enable the nightly timer (runs the extraction driver against the hub)
systemctl --user link <path-to-reflect>/systemd/reflect-nightly.service
systemctl --user link <path-to-reflect>/systemd/reflect-nightly.timer
systemctl --user enable --now reflect-nightly.timer
# 5. Verify
uv run reflect run --dry-run
uv run reflect run
Clients do not need the MCP systemd services — the hub serves those. Clients only need the
nightly timer (which drives the extraction) and the opencode mcp add entry (so sessions can read
lessons).
4. OpenCode plugin (optional, any deployment)
The plugin pushes matching shared lessons into a session's system instructions without being asked. It needs a hub that serves the matcher (see Shared matcher); register it on any machine whose OpenCode sessions can reach that hub.
# 1. Register the plugin directory. OpenCode resolves <dir>/index.ts (or index.js),
# NOT package.json's main, and skips silently a directory with no root entrypoint.
# Add it to the "plugins" array in ~/.config/opencode/opencode.jsonc:
# "plugins": [
# "/absolute/path/to/reflect/plugin/opencode"
# ]
# 2. Give the OpenCode background service the hub endpoints. A path-registered
# plugin receives no options and reads its environment ONCE, when the service
# starts — a variable exported for a single `opencode run` never reaches it.
# Persist them in the managed service configuration, which every service
# start reuses. (Setting a managed variable stops the running service.)
opencode service set env REFLECT_HUB_URL http://<hub-hostname>:4010
opencode service set env REFLECT_HUB_WORKER_URL http://<hub-hostname>:4011
opencode service set env REFLECT_MCP_TOKEN <read token>
opencode service set env REFLECT_MCP_WORKER_TOKEN <worker token>
# (REFLECT_PROJECT, REFLECT_MATCH_TIMEOUT_MS and REFLECT_PLUGIN_DISABLED are optional.)
# 3. Start the service again so the new environment takes effect.
opencode service start
REFLECT_HUB_URL is the read app that serves POST /match; REFLECT_HUB_WORKER_URL is the
worker app that serves POST /attribution (defaults to the read URL when unset). With no
REFLECT_HUB_URL the plugin still loads and logs one line, but registers nothing — the usual "it
never injects" cause. opencode service get env prints the stored values including any tokens, so
do not share that output. This configures the shared background service only: --standalone and
opencode serve use their own process environment, so export the variables in the shell that
launches those. On the hub, provision the embedding bundle once (uv run reflect embed fetch,
which writes ~/.local/share/reflect/models/all-minilm-l6-v2-onnx/); without it the hub ranks by
keyword instead of embeddings. Nothing is injected until a lesson is promoted to the shared tier
or pinned. See plugin/opencode/README.md.
Configuration
Config is a TOML file. Resolution order (first hit wins):
--config <path>passed toreflect run|mcp|lifecycle|statusREFLECT_CONFIGenvironment variable (path to a file)~/.config/reflect/reflect.tomlif it exists- Built-in defaults
Every option lives under a [core] section — flat top-level keys are silently ignored. To
create your config, copy the shipped sample and uncomment what you need:
cp reflect.toml ~/.config/reflect/reflect.toml # then edit
Invalid or unknown keys are ignored and fall back to defaults.
| Key | Default | Purpose |
|---|---|---|
opencode_db |
~/.local/share/opencode/opencode.db |
session history (read-only) |
lessons_db |
~/.local/share/reflect/lessons.db |
lesson store (created on first run; clients ignore this) |
worker_model |
"" |
model used by the nightly agent (empty = opencode's default) |
worker_agent |
reflect-worker |
reserved agent name (self-excluded from extraction) |
exclude_agents |
["reflect-worker"] |
agents never processed |
disabled_projects |
[] |
project names hidden from search |
core_threshold |
80.0 |
strength at which a lesson becomes a core memory |
archive_threshold |
15.0 |
strength below which a lesson is archived |
base_strength |
30.0 |
strength of a newly created lesson |
decay_per_day |
1.0 |
daily strength decay |
reoccurrence_bonus |
10.0 |
strength gain when a lesson re-occurs |
retrieval_bump |
1.0 |
strength gain on each search retrieval (stopgap so retrieved lessons survive until judged) |
useful_bonus |
10.0 |
strength gain when an agent judges a lesson useful (rate_lessons) |
not_useful_penalty |
15.0 |
strength loss when an agent judges a lesson not useful (rate_lessons) |
rescan_interval_days |
30 |
re-review older sessions this often |
rescan_quota |
20 |
max old sessions re-reviewed per night |
max_sessions_per_run |
0 (0 = unlimited) |
per-night cap on sessions processed; the shipped sample sets 50 to drain the first-run backlog gradually |
batch_size |
15 |
sessions processed per LLM run (each run starts with a fresh context) |
consolidate_interval_nights |
7 |
run the consolidation pass (merge duplicates, supersede outdated lessons) every N nights |
shared_enabled |
true |
render the shared knowledge block (SHARED.md) and install its pointer each night |
attribution_enabled |
true |
reconcile client-reported lesson usage into the hub's usage table |
shared_min_projects |
2 |
distinct projects a lesson must have been judged useful in before it is shared |
shared_strength_floor |
45.0 |
minimum strength a lesson needs to enter the shared block |
shared_max_entries |
12 |
ranked members the shared block holds |
shared_evict_slack |
3 |
extra slots last night's members keep before they are evicted |
shared_max_pinned |
3 |
most pinned lessons the block renders |
shared_entry_max_chars |
200 |
maximum length of one rendered block entry |
shared_path |
~/.config/opencode/SHARED.md |
rendered shared block (the hub writes it; clients mirror it) |
shared_pointer_path |
~/.config/opencode/AGENTS.md |
global file that receives the SHARED.md pointer |
embed_model |
"minilm-l6-v2" |
model name vectors are stored under and handed to a remote embedder |
embed_dim |
384 |
vector width; must match the embedder |
embed_url |
"" |
optional HTTP embedding endpoint; empty = use the local ONNX bundle |
embed_model_dir |
~/.local/share/reflect/models/all-minilm-l6-v2-onnx |
local ONNX bundle directory (see "Shared matcher") |
embed_model_repo |
"onnx-models/all-MiniLM-L6-v2-onnx" |
the repository reflect embed fetch downloads the bundle from |
embed_refresh_batch |
500 |
most lessons the nightly embeds in one run; a backlog drains over several nights |
match_min_similarity |
0.35 |
cosine floor; a lesson below it is not offered |
match_max_pointers |
3 |
most lessons a match returns (and the FTS5 candidate pool size) |
match_query_max_chars |
600 |
query characters embedded; the rest is truncated |
match_shared_lift |
1.15 |
score multiplier for lessons already in the shared block |
purge_ttl_days |
90 |
hard-delete archived lessons after this long |
run_timeout_s |
3600 |
nightly agent timeout |
transcript_max_chars |
12000 |
per-session transcript budget fed to the agent |
mode |
"local" |
"local" (single machine), "hub" (local + serve the MCP over HTTP), or "client" (spoke that writes to a remote hub) |
hub_url |
"" |
hub worker MCP URL for mode = "client", e.g. http://<hub-hostname>:4011/mcp |
mcp_host |
0.0.0.0 |
bind host for the reflect mcp streamable-http transport |
mcp_port |
4010 |
default HTTP port for the read MCP server |
mcp_worker_port |
4011 |
default HTTP port for the worker MCP server (reflect mcp --worker) |
workdir, opencode_bin, uv_bin, and path_extra default to this machine's paths; adjust
them if you run reflect on a different machine.
Sizing the token budget (rule of thumb). Sessions are processed in batches of batch_size,
and every batch runs in a fresh LLM context (the driver launches one short agent run per
batch, so context never accumulates across a night — no compaction, no overflow). To pick a
batch_size that fits your model's window, roughly one batch can hold
batch_size ≈ (context tokens × 0.85) ÷ (transcript_max_chars ÷ 4 + ~750)
tokens per session before overflowing — a token is about 4 characters, the 0.85 reserves
~15% of the window for the prompt, tool definitions, and output, and the ~750 accounts for
the agent's own reasoning per session. For a 140K-token model with transcript_max_chars = 12000 that is (140000 × 0.85) ÷ (12000 ÷ 4 + 750) ≈ 32 sessions per batch; the shipped
default of 15 is a safe choice. If a batch aborts with "request exceeds the available
context size", lower transcript_max_chars (each session costs transcript_max_chars ÷ 4
tokens) or lower batch_size, rather than raising max_sessions_per_run — smaller batches
keep each run within the window. run_timeout_s applies to each batch run.
Commands
uv run reflect run— run the nightly pipeline once (worklist → LLM agent → verify → lifecycle).--config <path>is optional and only needed to point at a config other than the default (~/.config/reflect/reflect.toml). Inmode = "client"this drives the spoke against the hub instead.uv run reflect run --dry-run— print the session worklist without running the agentuv run reflect mcp— MCP server (read-only tools;--workeradds extraction tools).--transport stdio|streamable-http,--host, and--portselect the transport; the HTTP defaults come frommcp_host/mcp_port/mcp_worker_port.uv run reflect lifecycle— apply decay/archive/purge onlyuv run reflect status— recent run historyuv run reflect stats [--days N]— usefulness dashboard (retrievals, judgements, reoccurrences, coverage, per-project, shared tier)uv run reflect shared [show|pin <lesson_id>|unpin <lesson_id>|drop <lesson_id>|undrop <lesson_id>]— show the shared block with each member's reason and the candidates next in line, or pin/drop a lesson (defaults toshow)uv run reflect similar "<text>" [--limit N]— rank the lessons most similar to a query, the same ranking the shared pointer uses. Read-only: it records no retrieval, so the threshold can be tuned without moving the figures that decide whether the matcher earns its place.--limitdefaults tomatch_max_pointersand may exceed it to widen the ranking.uv run reflect embed fetch— download the embedding model bundle intoembed_model_dir(see Shared matcher)
status,stats,lifecycle,shared, andsimilaroperate on the hub'slessons.db; running them on a client returns an error.
Shared matcher
The shared tier injects a SHARED.md pointer into every session; the matcher decides which
lessons that pointer offers. Everything below is optional — with no embedder the matcher falls
back to FTS5 keyword ranking and the nightly run is unaffected.
- Install.
pip install 'reflect[embed]'(oruv sync --extra embed) addsonnxruntimeandtokenizers, the local ONNX embedder. Without the extra, or without a model bundle, embeddings are disabled. - Hub-side. Embeddings and vectors belong to the hub. The nightly run (
mode = "local"or"hub") refreshes the vector index immediately before the shared pass, and amode = "client"spoke never embeds.reflect similarand the hub'sPOST /matchread the same index. A refresh failure is reported on stderr and never changes a run's status or exit code. - Model bundle. The ~90 MB bundle is provisioned once per machine with
reflect embed fetch, which needs egress to the Hugging Face host and to the CDN itsresolveURL redirects to. Where that egress is unavailable, copy an existing bundle intoembed_model_dirinstead. To use a remote embedding endpoint rather than the local bundle, setembed_url.
Matcher REST endpoints
Both endpoints share the MCP servers' transports and bearer auth (see the auth note above):
/match rides the read server (mcp_port, REFLECT_MCP_TOKEN), /attribution the
worker server (mcp_worker_port, REFLECT_MCP_WORKER_TOKEN). Paths are relative to the
server root, e.g. POST http://<hub-hostname>:4010/match.
POST /match — rank the lessons nearest a query. Same ranking as reflect similar, and every
call appends one match_stat row.
// request
{"query": "how do I open a forgejo pull request", // required; blank -> 400
"project": "reflect", // optional, recorded on the row
"session_id": "ses_abc", // optional, recorded on the row
"exclude": ["<lesson-id>", "…"]} // optional: already-injected ids
// 200
{"pointers": [{"id": "<lesson-id>", "title": "Forgejo PRs use fj", "score": 0.82}],
"degraded": false} // true = keyword fallback, score null
A missing or blank query is 400 {"error": "query required"}. Any other failure (a malformed
body, a non-list exclude, a store error) is answered 200 {"pointers": [], "degraded": true}
rather than 5xx, so a session's prompt is never blocked by the matcher. A lesson already in
exclude is dropped from pointers but still counts as clearing the similarity floor.
POST /attribution — report usage the client observed, as the record_usage tool does. Each
record is {"lesson_id", "project", "session_id", "action"}, action being one of inject
(the lesson was pointed at a session), retrieve_observed (the session pulled it) or
judge_useful / judge_not_useful (a reconciled judgement). kind is accepted as an alias for
action, with the spec's retrieve mapped to retrieve_observed; an explicit action wins.
// request
{"records": [{"session_id": "ses_abc", "project": "reflect",
"lesson_id": "<lesson-id>", "action": "inject"}]}
// 200
{"recorded": 1, "skipped": 0} // skipped = malformed/duplicate/unknown
Reporting is idempotent — a record whose lesson, session, action and project already exist is
skipped, so an overlapping window may be re-reported — and failed or disabled reports answer
200 {"recorded": 0, "skipped": 0} rather than an error.
OpenCode plugin
plugin/opencode/ is a zero-dependency OpenCode plugin that pushes
these lessons into a session unprompted: once per prompt, capped and de-duplicated, it injects a
small block of lesson pointers into the model request's system instructions and reports inject
usage to the worker app. Register the directory — OpenCode resolves its root index.ts, not
package.json's main — in the plugins array of ~/.config/opencode/opencode.jsonc; set
REFLECT_HUB_URL (the read app serving POST /match)
and, when the worker app is elsewhere, REFLECT_HUB_WORKER_URL (serving POST /attribution) in
the environment the OpenCode background service starts from, not on an opencode run
command line. See plugin/opencode/README.md.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ HUB machine │
│ │
│ nightly timer ──> reflect run (extracts lessons) │
│ ──> writes to local lessons.db │
│ │
│ reflect mcp (port 4010) ──> read-only lessons for anyone │
│ reflect mcp --worker (port 4011) ──> write tools for clients│
└─────────────────────────────────────────────────────────────┘
▲ ▲
│ hub_url (worker) │ opencode mcp add (read)
│ │ (localhost on hub)
┌──────────┴──────────┐ ┌───────────┴──────────────┐
│ Client machine 1 │ │ Client machine 2 │
│ │ │ │
│ nightly timer ──> │ │ nightly timer ──> │
│ reflect run │ │ reflect run │
│ (writes via hub) │ │ (writes via hub) │
│ │ │ │
│ opencode mcp add ───┘ │ opencode mcp add │
│ (reads from hub) │ (reads from hub) │
└───────────────────────┘ └──────────────────────────┘
On each machine the lesson decay/purge lifecycle is claimed at most once per UTC day across all
machines, gated by the hub's shared lessons.db: whichever reflect run or finish_run call
runs first that day applies it, and the rest skip — so spokes and the hub's own night never
double-run it.
Notes
- Reflect sessions are excluded from extraction (agent
reflect-workerand descendants). opencode.dbis read-only; all writes go tolessons.db.- Search is FTS5 keyword search; the optional shared matcher (above) additionally ranks candidates by embedding similarity.
- The nightly worker is confined to
reflect_*MCP tools only; all other tools (bash, edit, webfetch, …) are denied via a deny-by-default agent permission allowlist. max_sessions_per_runcaps how many sessions a single night processes (built-in default0= no cap). Sessions not processed stay in the worklist and are picked up the next night. The shippedreflect.tomloverrides this to50as a safety valve: the first run faces the entire history of past sessions as one huge backlog, so the cap drains it gradually instead of in a single enormous run.
License
This project is released to the public domain under the CC0 1.0 Universal license. See the LICENSE file for the full legal text.