Thanks for considering a contribution! Loop Memory is a small, dependency-free
core that grows through opt-in extras. The goal of this guide is to get a new
contributor from git clone to a merged PR in under an hour.
- The core
loop_memorypackage stays dependency-free at runtime. Optional integrations (LLM SDKs, vector stores, embedders) live under[project.optional-dependencies]inpyproject.tomland are pulled in viapip install ".[openai]"etc. Do not add a hard import for an optional dependency inside the core package. - Public functions and HTTP handlers must keep type hints and a short
docstring. If you change the schema of any
MemoryItem/WikiPage/EvolutionStatsfield, update both the relevant doc andtests/. - Every PR must pass
pytest -qlocally (see below). - Keep PRs small and one-logical-change-per-commit. AI-generated code is welcome as long as a human has read it.
loop_memory/
├── cli/ # `loop-memory` console entry-point (typer-style)
├── serve/ # FastAPI app + static UI
│ ├── app.py # all @app.get/@app.post routes live here
│ ├── handlers.py # request/response helpers (no route decorators)
│ └── static/ # Vue 3 CDN ESM frontend — no build step
├── jobs/ # background work
│ ├── evolution.py # 5-stage distillation pipeline (Stage 1→5)
│ ├── consolidate.py # legacy single-pass consolidator
│ ├── llm_consolidate.py
│ └── scheduler.py # cron / after-ingest / realtime scheduler
├── llm/ # LLM provider layer
│ ├── providers.py # PROVIDERS dict + ProviderSpec + validation
│ ├── base.py # LLMClient protocol
│ └── openai_adapter.py
├── memory/ # dataclasses (MemoryItem, WikiPage, Entity, …)
├── graph/ # knowledge-graph extract + build
├── storage/ # SQLite-backed MemoryStore
├── security/ # keychain / local-secret store
└── mcp/ # optional Model-Context-Protocol server
tests/ # pytest, mirror-tree under loop_memory/
docs/ # architecture, auto-capture, API, providers
git clone https://github.com/<you>/loop-memory.git
cd loop-memory
python3.10 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,serve,openai]"
pytest -qThen launch the server (the production launchd plist is ~/Library/LaunchAgents/com.loopmemory.server.plist):
python -m loop_memory.cli.main serve --port 7767
# or, to develop the frontend with hot-edit:
python -m loop_memory.cli.main serve --reloadThe dev UI is served at http://127.0.0.1:7767/. The interactive OpenAPI document is at http://127.0.0.1:7767/docs.
API keys are never stored in SQLite or in the settings blob. They go to:
- macOS: the user-login Keychain via
loop_memory/security/secrets.py. - Linux/headless:
~/.loop_memory/secrets.json(mode 0600), with the OS keychain as a fallback when present.
If you add a new secret, store it through security.get_secret(name) /
set_secret(name, value) so the same fallback chain is used everywhere.
LLM providers are registered in a single dict — no plugin loader required.
-
Open
loop_memory/llm/providers.pyand add an entry toPROVIDERS:PROVIDERS["my-co"] = ProviderSpec( label="MyCo", default_model="myco-3-mini", default_base_url="https://api.myco.example/v1", needs_api_key=True, adapter="openai_compat", # reuse OpenAI-compatible adapter notes="OpenAI-compatible chat-completions endpoint.", )
-
If the wire format is not OpenAI-compatible, add a dedicated adapter in
loop_memory/llm/<myco>_adapter.pyimplementing theLLMClientprotocol (seebase.py), and setadapter="myco"on the spec. -
Add tests in
tests/test_llm_myco.py— mock the HTTP layer; do not call the real provider in CI. -
Update the docs/providers table in
docs/providers.md. -
Open a PR — the "Test provider" button in Settings → Models will pick up the new entry automatically.
Each source (Codex, Claude, Hermes, OpenClaw / clawx) is a small module in
loop_memory/serve/watcher.py (or a sub-module it imports). To add a new one:
- Implement a
Watcherclass withstart(),stop(), and apoll_once()method that emitsIngestItemrecords. - Wire it into the CLI:
loop-memory hook --source <new> --watch <path>. - Document the expected transcript layout in
docs/auto-capture.md. - Add a fixture under
tests/fixtures/<source>/and a test that ingests it.
The 5-stage pipeline lives in loop_memory/jobs/evolution.py:
| Stage | Where | Purpose |
|---|---|---|
| 1 | EvolutionEngine._rescore() |
Signal-aware re-scoring (importance × recall × feedback) |
| 2 | EvolutionEngine._cluster() |
Semantic batching into clusters |
| 3 | EvolutionEngine._distill() |
Per-cluster atomic-fact distillation (prompt: _CLUSTER_SYSTEM) |
| 4 | EvolutionEngine._synthesize_wiki() |
Hierarchical wiki page build (prompt: _WIKI_SYSTEM) |
| 5 | EvolutionEngine._evolution_memo() |
Cross-page synthesis + contradiction notes |
Two non-negotiable policies from v2:
- Completeness over compactness. The distillation prompts (
_CLUSTER_SYSTEM/_WIKI_SYSTEM) and the call-sitemax_tokensmust never reintroduce hard character caps. If a bullet is incomplete, the fix is a better prompt, not a smaller cap. - No truncation mid-fact. Bullet points are atomic; do not "save space" by stripping numbers, names, or constraints.
When you change a prompt, also update the matching snapshot test under
tests/test_evolution_prompts.py (if present) and add a new entry to
CHANGELOG.md.
pytest -q # fast suite, no network
pytest -q -m "network" # only tests that hit real APIs (skipped without keys)
pytest -q tests/test_evolution.py # one moduleTests must not write to the user's real ~/.loop_memory/loop_memory.db —
every test either uses an isolated tmp_path fixture or injects an in-memory
store.
- One logical change per commit.
- Commit subject:
feat: …,fix: …,docs: …,chore: …,test: …,refactor: …. Keep it ≤ 72 chars. - Reference any issue number in the commit body (
Refs #123). - PR title mirrors the commit subject; PR body explains why, not just what, and includes screenshots for any UI change.
Please do not open a public issue. Follow SECURITY.md.
Only the maintainer (smartfind) and the GitHub dependency bots
(dependabot[bot]) appear on the contributors graph. AI coding
assistants (Codex, Claude, Copilot, …) must NOT author their own
commits, even when they are the only contributor to the change.
How this is enforced:
git config user.name "smartfind"andgit config user.email "44515814+smartfind@users.noreply.github.com"are the canonical identity — set them globally so every session inherits them..git/hooks/pre-commitrewrites any non-whitelisted author / committer identity tosmartfindat commit time and refusesCo-authored-by:trailers that name an AI assistant.- The remote
mainwas rewritten withgit filter-branch --env-filteronce to consolidate legacy AI-attributed commits under the maintainer; see CHANGELOG H6 for the data-migration details and thebackup/pre-author-rewritebranch for the pre-rewrite history.
If you add a future integration that genuinely needs its own bot
identity (e.g. a new release helper), extend the KEEP_NAMES_RE
regex inside the pre-commit hook.