Architecture¶
Two decisions are the base of the full design:
- Logical identity, never physical. The key of each item is
project_id/run_id/phase/entry_id. Redthread records hostnames, absolute paths, and server IPs only as provenance metadata. It never uses them as addresses. - The git remote is the hub, not a node. All nodes are clients of the same type, and you can replace one with a different one. If you lose or replace a node, you lose no data, because the node was never the source of truth.
Data model¶
Project A long-lived body of work. id: slug
└─ Run One end-to-end attempt. id: ULID
└─ Phase arbitrary, project-declared (e.g. build | test | present)
├─ ContextEntry append-only events (the raw memory)
├─ Artifact pointer records (config, plot, checkpoint, log)
├─ summary.md rolling, agent-maintained digest of the phase
└─ handoff.json curated contract published to the next phase
The run_id is a ULID. You can sort ULIDs in time sequence, and a node
can make one without a central service. Thus, each node can make a new ID
offline, and no two IDs are the same.
ContextEntry¶
A context entry is the smallest unit of memory, and it does not change.
Each entry is one JSON file with the name <seq>-<entry_id>.json. The ULID
entry_id identifies the entry. Thus, when two nodes write at the same
time, they do not use the same filename, and merges have no conflicts.
Artifact¶
An artifact record is a content-addressed pointer. It does not contain the data.
- Redthread commits small text artifacts inline.
- Redthread pushes large binary artifacts (checkpoints, datasets, build outputs) to an object store. A sha256 hash identifies each artifact.
handoff.json¶
The handoff is the curated contract that a phase publishes for the subsequent phase. A downstream phase uses only this schema. It never uses the raw entries. Thus, the phases stay loosely coupled. The final output (report, deck, docs site) is also clear, and it is not a dump of all the data.
Store layout¶
The store is a git repo. It can be one of these two types:
- A separate repo with its own remote. Use
redthread init. - An orphan-branch worktree of a repo that exists, usually your code
repo. Use
redthread init --worktree-repo <path>.
Worktree mode never checks out or moves the active branch of the host repo.
It attaches a second working directory to an orphan branch. This branch
has no shared history with your code. Thus, git status and git log in
your working tree show no changes. Refer to Worktree mode
below.
redthread-store/ (its own git repo, or an orphan-branch worktree)
├── project.yaml # phase pipeline declaration
├── runs/
│ └── <run_id>/
│ ├── run.yaml # status, phase states, node lineage
│ ├── phases/
│ │ ├── build/
│ │ │ ├── entries/ # 0001-<ulid>.json, ...
│ │ │ ├── artifacts/
│ │ │ ├── summary.md
│ │ │ └── handoff.json
│ │ ├── test/ ...
│ │ └── present/ ...
│ └── artifacts.index.json
└── memory/
└── <namespace>/ # portable long-term agent memory
Sync¶
Metadata and context sync through the git remote. This data is small, and a clone takes only some seconds. Large artifacts sync through a content-addressed blob backend.
A small auto-commit daemon collects entry writes. After a short delay, it
does pull --rebase → commit → push.
To move a phase to a different machine, do clone + resume. No data stays
on the old machine, because no node ever owned the data.
Worktree mode¶
LocalStore.init_worktree(host_repo, worktree_path, branch, project_id,
phases) makes the store as an orphan branch of host_repo. Usually,
host_repo is your code repo. The function attaches the branch with
git worktree add --orphan. Thus, the branch has no shared commit history
with your code. The branch that is checked out in the host repo does not
move.
redthread.store.gitio.ensure_worktree is the only entry point for this
procedure. It does one of these three steps:
- If the orphan branch does not exist, it makes the branch.
- If this machine already has the branch locally, it attaches to it.
- If a different machine made the branch first, it fetches the branch from
originand attaches to it.
redthread resume does the same three steps for stores that are plain
repos.
A git worktree shares the objects, refs, and remote configuration of its
parent repo. Thus, the sync functions (gitio.sync, pull_rebase,
push) operate on a worktree path without changes. The sync layer has no
special cases for worktrees.
resume_worktree(host_repo, worktree_path, branch, run_id) is the
worktree-mode version of resume(). It does not use a separate --remote
argument. The remote of the store is the origin of the host repo (your
code repo).
Worktree mode compared with a separate store repo¶
The auto-commit daemon of the store pushes each 5–15 seconds, after a short delay. These commits go into the same repo as your code, on a different branch.
- This is convenient for one person or a small team. You have one repo, one remote, and no more setup.
- But a store with many commits can show in the branch list of the repo. With some hosting providers, it can also show in the activity feed.
A separate store repo prevents these problems. Use a separate store repo if the store must have its own access control or lifecycle, independent of the code.
Discovering a store on a fresh machine (.redthread.yaml)¶
redthread init and redthread init --worktree-repo do not record the
mode of a project. Both are only CLI flags. Thus, no file on the disk
records "the store of this project is a worktree of this repo on branch
X". A person or an agent cannot find this information later.
Without more data, a second machine cannot know which flag to use:
--remote, --worktree-repo, or no flag. Only the person (or the
documentation that the person wrote) knows.
The .redthread.yaml file gives this information. It is a small marker
that is committed in the host (code) repo, near
AGENTS.md/.mcp.json. It records the mode of the store and how to get
to it:
schema_version: 1
store:
mode: worktree # or "repo"
path: ./redthread-store
branch: redthread-store # worktree mode
# url: git@github.com:you/project-memories.git # repo mode instead
LocalStore.init_worktree writes this file automatically. Worktree mode
always knows its host repo, so no more configuration is necessary.
Plain LocalStore.init writes the file only when you call it with
host_repo=... (CLI: redthread init --host-repo PATH). In repo mode, the
url is usually not known yet when you run init.
redthread.hostconfig.attach(host_repo, store_path, allow_clone=False)
reads the marker and makes store_path:
- Worktree mode attaches without conditions.
gitio.ensure_worktreedoes its usual three checks for thebranchin the marker. It tries the local branch first. Then it fetches from theoriginof the host repo. Then it makes a new orphan branch. This is safe to do automatically. The "remote" is the code repo that you already cloned. It is not a new security boundary. - Repo mode must have
allow_clone=Trueto clone a missing store from theurlin the marker. When Redthread runsgit cloneon a URL from a committed file, this is a real security boundary. A dangerous repo can point to a poisoned store, and agents do what they read in memory. Thus, Redthread never crosses this boundary without permission. - If the store already exists locally,
attachdoes the opposite. It copies theoriginURL of the store into theurlof the marker. If you make a repo-mode marker before the remote exists, do these steps: rungit remote add origin ..., then runredthread attachagain. This fills in theurl. A separate update command is not necessary.
Two commands use this function:
redthread attach [--store PATH] [--host-repo PATH] [--allow-clone]: A person or a script uses this command to make the store immediately.redthread mcp-serve: The server uses the same function when a tool first uses the store. If--storedoes not exist but--host-repohas a marker, the server attaches the store before it opens it. The default--host-repois the working directory of the server, usually the root of the project.
store_init also obeys this procedure. If the attach shows that a
different machine already filled the store, store_init returns the
manifest of that store. It does not return an "already exists" error.
The result: on a second machine, you only clone the code repo and register the same MCP server command as all other users. You do not have to remember flags, and you do not clone the store manually.
Phase adapters & handoff contracts¶
Each phase reads from and writes to the shared store through a thin
adapter. Adapters are the only phase-specific code in the system. The
store and the sync stay generic. An ML pipeline (train → eval →
present) and an app pipeline (build → test → present) use the same core.
redthread.adapters.base.PhaseAdapter is the generic lifecycle for all
domain adapters. It does these steps:
- When the phase starts, it sets the phase to active.
- It collects metrics and writes them as one entry. It does not write one entry for each call.
- It registers artifacts.
- It updates the rolling summary.
- It publishes the handoff.
The adapter always writes its data immediately on publish_handoff and on
exit. Thus, the curated output does not depend on the sync daemon. If an
unhandled exception occurs, the adapter logs an error entry and sets the
phase to failed. Then it raises the exception again.
redthread.adapters.examples contains two thin pipelines that use
PhaseAdapter: ml_train/ml_eval and app_build/app_test. They show
that the core contains no domain-specific words. A CI test parses each
module outside adapters/examples/. If an ML or app term (epoch,
checkpoint, coverage_pct, ...) occurs outside a docstring, the test
fails.
redthread.adapters.present is the domain-neutral output side. It uses
only the handoffs from the phases before present in the pipeline of
the project (store.manifest.phases). From these handoffs, it makes a
markdown report, a slide deck (python-pptx), and a docs-site markdown
tree. The output is the same for train/eval and for build/test upstream
phases. This module also passes the CI test for domain-specific words.
Agent memory (MCP)¶
redthread.mcp makes the store available as an MCP server (stdio
transport). The server has 19 tools for runs, context entries,
artifacts, summaries, and handoffs. It also has a memory/<namespace>/
tree for long-term agent memory that is not related to a run.
Set the MCP configuration of a coding agent to this server, not to its
local .claude//.agent/ folder. Then each machine that clones the store
shows the same memory. This is the same portability that the remaining
parts of Redthread give to scripts.
mcp/tools.pycontains the operations as plain functions that you can test directly.mcp/server.pyis a thinFastMCPwrapper around these functions.
Redthread validates memory keys against path traversal (../, absolute
paths, backslashes) before it writes to the disk. The memory keys are the
only part of the store schema that an LLM agent writes in free form.