Skip to main content

Serverless-native local archive for AI conversations: JSONL truth log + rebuildable SQLite index, searched and read locally, exposed over MCP.

Project description

thread-archive

CI Python Platform License: MIT

Thread Archive is a local-first archive for the AI agents that work on your machine — built by Claude Code, for Claude Code. Preservation is the product: every session your agent harnesses record lands in one durable, append-only archive you own, on your own disk — kept safe past harness rotation, provider format drift, and index corruption, and served back to your agents over MCP. Claude Code is the supported, first-class source; the other harnesses it reads — Codex, Cursor, OpenCode, Grok, and friends — are best-effort and community-maintainable (see When an import drifts). Web chats (claude.ai, ChatGPT, xAI) import too, from account exports you download by hand and drop on the viewer's upload page; the live, self-feeding path is the agent tooling.

Your agent's transcripts are temporary by default. Claude Code writes each session to ~/.claude as JSONL, keeps it for 30 days, and deletes it — a sensible default for a harness's working files, a lossy one for the only record of how the work got decided. Nothing searches them in the meantime either.

Day one still gives you something to search. Point archive at this machine's stores and whatever is still on disk imports as a head start; from then on every session lands in an append-only log you own, and the window stops mattering. Minutes after setup, ask mid-conversation:

"what did we decide about the auth flow?"

Your agent searches, reads around the hits, and comes back with what you decided and why — a few thread_search calls and a couple of thread_reads, the way it would work through a codebase it hadn't seen. What it reads is the conversation itself, not a summary someone made of one. No workflow to adopt, no notes you were supposed to be taking — from here on the record is being written.

Searchable by you — and by your AI.

  • Full-text and semantic search, fused and re-ranked, filterable by time, source, tool, and content type; an empty query browses recent activity.
  • Exposed over MCP (thread_search, thread_read), so Claude (or any MCP client) can search and read your entire history mid-conversation.
  • The same two tools are CLI verbs — thread-archive search "auth flow" --since 30d, thread-archive read <id> — one implementation behind both, so what you get at a prompt is what your agent gets.
  • Search is the access layer over the archive, not the archive itself — an agent typically fires several searches, reformulates, and reads around a hit, and the archive underneath guarantees the conversation is there to find. Quality is measured against public benchmarks somebody else labeled, read beside the baseline their own leaderboard publishes — a deliberate run on a ranking change, and a gate at release time that holds those numbers to a checked-in bar, but not a CI row; what rides CI is a probe that the search arms still load at all. No protocol that labels this archive's own corpus certifies that search is good, and nothing gates on one. The numbers, the protocol, and its limits live in docs/search-quality.md. Your install reports whether search is degraded (thread-archive status, the viewer's health page) rather than a score — a metric with no baseline beside it isn't something you can act on.

Indexed by code, not just by words. Every path your agents' tools named — each Edit, Read, Write, apply_patch header, and path-shaped shell argument, in every provider's spelling — is folded into a structural index. It arrives as two new scopes on the tools that already exist, because the questions are the ones search already had shapes for — list the conversations, or search inside them:

  • thread_search(path='rank.py')which conversations worked on this file. With an empty query it is the list, ordered changes-before-looks, each row carrying its op tally and opening at the touch rather than at the session's tail; with a query it scopes the search to those sessions. A directory asks about a whole repo or module (path='/repo', path_ops='edit,write,delete'), a glob about a file type.
  • thread_search(commit='31bade5')which conversations this commit is made of, the loop back from git blame. Not one session: a commit carries work from several sittings, so it resolves to every session whose edits fall inside the commit's authorship window — after each of its files was last committed, up to this commit — ranked by how much of it they account for. The session that ran git commit is flagged among them rather than standing in for them, which matters most where you commit by hand and it is nobody.
  • thread_read(thread_id, summary='files') — the same index backwards: what this session actually changed.

The index is a disposable projection of the event log: it backfills itself over an existing archive and rebuilds with reindex.

Built like a database, not a folder of exports.

  • Plain JSONL files are the source of truth — human-readable, greppable, yours. The search index is disposable and rebuilds from them at any time.
  • Crash-safe writes with intent journaling, fsync discipline, and automatic recovery. Your history survives power loss, killed processes, and corrupted indexes.
  • Built-in backup, integrity verification, and restore drills: recovers from corruption or an errant delete, and the nightly pipeline checks that the backup actually restores. The archive is ordinary files on disk — whatever backs up the rest of your data covers it the same way.

Fixes itself where it broke. A provider's transcript format drifts on the provider's schedule, not a maintainer's. Archive makes that drift loud and locally repairable: drift ledgers and a nightly coverage check catch the degradation, the raw source files are quarantined before the provider prunes them, the in-session search notice names the remedy, and thread-archive source fix <provider> scaffolds an override patch — module, tests, evidence, real samples, and the repair protocol — so the fix gets written on the machine that has the samples, by you or by an agent you hand the scaffold to. The patch goes live only when its scaffolded test suite passes in a fresh subprocess, then re-import recovers everything consumed during the gap. The supported provider's worst case is preserved but partially modeled until fixed — and the fix doesn't wait on a release.

No hosted backend. No cloud. No subscription to lose your history to. A background watcher keeps it current; every process — the MCP server, the web viewer, the daemons — runs locally, on your machine.

Dev tooling for one well-provisioned workstation. macOS and Linux are the supported platforms — the always-on daemons are launchd LaunchAgents on macOS and systemd --user units on Linux (public CI runs the Linux lane; the launchd lifecycle is verified on the maintainer's own machine, which is also where the product has the most mileage) — and the archive is single-user, single-machine. It assumes workstation-class headroom, too: optional semantic search keeps a multi-GB torch model resident, a normal cost on the machine this is for.

How it works

Serverless-native, single-user, single-machine: it watches this machine's agent-harness stores and imports provider transcripts into one event model.

It is a standalone package — no hosted backend, no cloud service, no host application it depends on. The supported interfaces are the retrieval tools (over MCP, or as the search / read CLI verbs), the documented on-disk format, and the provider plugin API (see Stability) — there is no other public Python API.

Install

Python ≥ 3.12, macOS or Linux:

pip install thread-archive        # or: uv tool install thread-archive
thread-archive setup

setup is the onboarding. On first run it discovers this machine's conversation stores and shows what it found — counts, sizes, date ranges — before touching anything, then asks: import (all, a selection, or skip), install the always-on watcher (launchd on macOS, systemd on Linux; includes the web viewer at :8787), schedule a nightly backup (a second question — where should backups go? — that installs the daily backup → verify → restore-drill pipeline to a disk you name), wire the MCP server into detected clients (the claude CLI, or it prints the JSON block for any other client), and — with the watcher serving it — open the archive in your browser. Every choice is skippable and persists in <home>/config.json; a disabled source stays disabled across every ingest path — and across later runs of setup, which only changes a source's policy where you state a new one (the edit selection). The end state is a populated, searchable archive served over MCP, plus the thread-archive operator CLI and the pre-built web viewer (no node at any point). thread-archive status shows status; thread-archive setup revisits the choices. Non-interactive (agents, scripts): thread-archive setup --yes accepts every default — without --yes, a non-TTY run only prints guidance and never ingests, and neither shape opens a browser.

Local semantic search is optional and heavy (pulls torch — sized for a dev machine): pip install 'thread-archive[embeddings]'. It brings the corpus-graph ranking stack with it (the [leiden] extra: leidenalg + igraph), since that signal is computed over the vectors. [all] is every runtime feature under one name; the base install is lexical-only and pulls no C extension beyond what numpy and mcp already need.

Without the wizard. The same pieces by hand: thread-archive watch --once runs one ingest pass over this machine's stores (or thread-archive source import <path> --provider <name> brings in a single transcript or store), thread-archive status confirms it landed, thread-archive service install upgrades to the always-on watcher, and the JSON block under MCP wires the server into any client — archive-mcp lands on the same PATH as thread-archive. Restart the client so it loads the server.

Skipped the watcher? Still covered: setup-generated MCP entries explicitly set THREAD_ARCHIVE_MCP_INGEST=1, opting archive-mcp into lazy catch-up ingest. A background pass at startup and (throttled) around tool calls imports whatever landed in your local AI-tool stores since the last pass. A bare archive-mcp invocation without that setting is fully read-only. On a fresh archive give the first pass a minute to chew before expecting search hits; the watcher install (thread-archive setup, or thread-archive service install) is the always-fresh upgrade. With the daemon installed, opted-in MCP passes degrade to no-op lock probes — exactly one process ingests at a time, however many clients are open.

From source. The development install is a clone with an editable venv:

git clone https://github.com/ellamental/thread_archive.git thread-archive && cd thread-archive
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'          # add -e '.[embeddings]' for local semantic search
.venv/bin/pytest tests/ -q                 # confirm green (add `-m package` for the wheel/sdist release lane)

# wire the read MCP server into this clone's .mcp.json (absolute venv path)
sed "s|ABSOLUTE_REPO_PATH|$(pwd)|g" .mcp.json.example > .mcp.json

Or let the agent do it: open the clone in Claude Code and say "install this, following claude-install.md" — venv, tests green, MCP wiring, first import, asking you exactly once (embeddings or not). On Ubuntu, hand it claude-install-ubuntu.md instead — same flow, systemd for the always-on pieces.

A clone's absolute path is baked into its .mcp.json wiring and any service units installed from it, so relocating the clone means re-running that wiring plus thread-archive service restart, not a plain mv. A clone updates by fast-forwarding to a release tag (thread-archive self-update); a pip install updates with pip install -U thread-archive.

Uninstall. thread-archive uninstall takes back everything setup put on the machine — the service agents, the MCP wiring in your client, the family manifest and monitor heartbeat, and setup's own record in config.json — and never touches the archive. Conversations, index, source choices, logs and exports stay where they are, and search / read keep answering from them with nothing installed. It closes by naming every place the data still is — the home, a truth dir or index pointed outside it, every backup mirror any stage ever recorded (plus one scheduled but not yet run), a home an earlier restore --replace set aside, the ~/.thread_archive compat symlink — and then how to finish: pip uninstall thread-archive, or the clone to delete when the install runs from one. Deleting any of the data is yours to do. --dry-run reports what would go without changing anything, --yes skips the confirmation. An agent or client entry serving a different archive home is reported and left alone.

CLI

One namespaced command. thread-archive setup runs the wizard (discover → consent → import → watcher → backup → MCP wiring); thread-archive status is the status view. search and read are retrieval — the MCP tools at a terminal.

What gets typed stays flat. Everything else acts on something — a source, the index, a backup, a service agent — and lives under that noun:

# retrieval
thread-archive search [query]   # the thread_search tool: filters by time, source, tool, content type,
                                #   file (--path) or commit; no query browses recent threads;
                                #   --group browse lists matched threads, --output linkable emits JSON
thread-archive read <id>        # the thread_read tool: replay a thread (--mode user|chat|full|last|ends,
                                #   --summary files, --around-event <id> to open a search hit);
                                #   takes a ULID, a legacy integer id, or a provider session uuid
thread-archive web              # open the cohosted viewer (the watcher serves it)

# ingest
thread-archive watch            # watch local AI-tool stores and import incrementally
thread-archive source list      # registered providers (built-in + installed plugins)
thread-archive source import <path>          # import a transcript / provider store
thread-archive source import-account <path>  # import a downloaded claude.ai / ChatGPT / xAI export
thread-archive source mirror    # mirror raw harness stores into <home>/source-mirror
                                #   (verbatim, gzip; nothing ever deleted)
thread-archive source coverage  # capture-coverage check: source stores reconciled against the archive
thread-archive source loads     # load progress: the in-flight load and recent runs, by phase
thread-archive source fix <provider>  # scaffold an override patch for a drifted import

# the index — everything rebuildable from the JSONL truth
thread-archive index rebuild    # rebuild index.db from the JSONL truth directory
thread-archive index embed      # embed user/text events still missing a vector (incremental
                                #   catch-up; --rebuild re-embeds everything)
thread-archive index migrate    # migrate older truth, then rebuild and verify
thread-archive index verify     # integrity check: truth parses + matches the index
thread-archive index repair     # quarantine damaged truth lines; restore committed content from the index

# durability
thread-archive backup run <dest>      # mirror the truth dir (hardlink generations under <dest>/.generations)
                                      #   + the recovery bundle under <dest>/.recovery (config, retained
                                      #   exports, health/ledger snapshots)
thread-archive backup nightly <dest>  # the scheduled pipeline: backup → verify (age-gated escalation)
                                      #   → restore drill → coverage
thread-archive backup drill <dest>    # prove the backup restores: rebuild an index from the mirror
                                      #   + smoke read/search
thread-archive backup restore <mirror> --to <home>  # actually restore: staged rebuild + verify, then
                                      #   atomic publish (--generation <stamp> picks a retained
                                      #   snapshot; --list-generations shows them)

# this machine
thread-archive status           # archive health / counts / last verify + backup + drill + coverage outcomes
thread-archive service <action> # install/uninstall/restart/status a service agent (launchd on macOS,
                                #   systemd --user on Linux) — the always-on watcher (default), --mcp
                                #   the shared server, --backup the nightly pipeline
                                #   (`service install --backup --dest <path> [--at HH:MM]`)
thread-archive self-update      # source clones only: fast-forward to the newest release tag — operator-
                                #   driven, nothing updates on its own (--check reports without applying;
                                #   a pip install updates with `pip install -U thread-archive`)
thread-archive uninstall        # remove this machine's archive machinery — agents, MCP wiring, manifest,
                                #   heartbeat, install record; the conversations are never touched
                                #   (--dry-run reports, --yes skips the confirmation)

Every pre-group spelling still resolves — reindex, nightly <dest>, daemon install, backup <dest> and the rest run exactly what they always did. They are listed nowhere: what keeps working is a machine already wired to them, not a second documented way to type a verb.

search and read are supported surface — the same implementation archive-mcp serves, so a query typed here and the same query asked mid-conversation return the same answer. Everything else in the CLI is private operational tooling (see Stability below): the process seam the service agents, cron, and operators use. The third door onto the same archive is the web viewer, cohosted by thread-archive watch --web.

Layout

src/thread_archive/
  _api.py           # internal coordination layer the CLI / MCP / web call into
  cli.py            # the `thread-archive` command — every verb, incl. `setup`
  _setup/           # what the archive puts on a machine and takes back off it: the wizard
                    #   behind `thread-archive setup` (first-run setup + status), and the
                    #   `thread-archive uninstall` flow
  _config.py        # truth dir + index path resolution, config.json (source opt-outs)
  _store/           # SQLite store + schema
  _truth/           # JSONL truth log + reindex
  _ops/             # backup kit: backup/mirror + restore drill, verify tiers, nightly,
                    #   health records, the action queue + its silences (notices.py)
  _importers/       # incremental import orchestration
  _retrieval/       # FTS5 + vector search, read reconstruction, the code axis (code.py)
  _knowledge/       # storage seam for the truth format's extension region — an
                    #   external knowledge layer's records, stored and backed up
                    #   here, written and specified elsewhere (docs/format.md)
  _watcher/         # local-source watcher (self-feeding ingest)
  _mcp/             # the library-native read MCP server
  _web/             # the viewer: stdlib server + built bundle (cohosted by `watch --web`)
  _service/         # `thread-archive service`: the watcher / MCP / nightly-backup agents behind a
                      #   platform registry — launchd (macOS) and systemd --user (Linux) backends
  _thread_import/   # vendored provider parsers (a clean, dependency-free island)
  _providers/       # the provider registry: built-in descriptors + plugin discovery
  provider/         # PUBLIC: the plugin API a third-party provider is written against
frontend/           # the viewer's React+Vite source (dev-only; builds into _web/static/)
host/               # operator layer: Makefile over `thread-archive service`, family-manifest writer
scripts/            # repo tooling (coverage gate, frontend-build check, license notices)
search_lab/         # the search lab (never shipped): the scoring core, quality + calibration
                      #   harnesses, corpus freezing, run ledgers — see
                      #   search_lab/README.md
tests/install/      # from-nothing install proofs: clean-container Docker + realistic
                      #   discovery-driven first run (~/.claude-style stores, macOS + Linux)

Stability

The public API is exactly four things:

  • the retrieval toolsthread_search and thread_read, served to agents by archive-mcp and to a person by the thread-archive search / thread-archive read verbs. One implementation, two front doors: their parameters, defaults, and output are the same contract either way;
  • the on-disk truth format — versioned by manifest.json's version and specified in docs/format.md. Data written by one release stays readable by the next; a reader refuses a truth directory newer than it understands. Programmatic read access to the documented stores (index.db is plain SQLite; the truth directory is documented JSONL) rides on this contract.
  • the provider plugin APIthread_archive.provider and its parse / testing submodules, documented in docs/providers.md. A provider maintained outside this repo is written against it and cannot follow the private tree's churn, so these names keep working.
  • the web viewer's URLs — the local UI at http://127.0.0.1:8787 (below): its page routes and the two JSON endpoints other programs call. Editor buttons, sibling navbars, and health probes link these from outside the repo, so they keep working.

Everything else is private support machinery and may change without notice: the rest of the thread-archive CLI, the viewer's bundle and markup, and every other Python module. More surface gets exposed deliberately as it matures. tests/test_public_api.py ratchets the boundary, with the viewer's page routes pinned in frontend/e2e/route-coverage.spec.ts against the route table itself.

Releases (changelog compression, version bump, release commit, annotated tag) follow docs/releasing.md.

When an import drifts

Providers change their on-disk formats without notice, and no maintainer runs a harness that exercises every variation at every provider release. Archive's answer is a support tier plus a repair loop, not a promise nobody can keep:

  • Claude Code is first-class. Its parser carries the full drift ledger (validators, residual preservation, the version tripwire), so its worst failure mode is soft: content is preserved — unmodeled fields ride along under annotations, skipped files land on the audit ledgers — but partially modeled until fixed. Everything else is best-effort: same machinery where it reaches, community-maintainable via the plugin API.
  • Drift is loud. The skip and validation ledgers plus the nightly coverage check produce per-source degradation verdicts (thread-archive source coverage prints them; the MCP search notice prepends a one-liner naming the remedy the next time you search, which is the moment you care).
  • Preservation doesn't wait for the fix. A degraded source's recently active raw files are snapshotted into dumps/drift/<source>/ — bounded, incremental, never auto-deleted — so a fix that comes months later can still recover everything the provider has since pruned.
  • The user's own agent writes the fix. thread-archive source fix <provider> scaffolds an override patch under <home>/plugins/ (module, tests, collected samples, drift evidence, per-provider quirk notes, and a PROTOCOL.md written to be handed to an agent), leaving one job open: the parse logic. Archive runs no agent itself — you work the scaffold, or point yours at it under whatever scope you choose, remembering that the samples are transcript data an agent should treat as untrusted input (see SECURITY.md). Activation is deterministic — the scaffold's tests must pass in a fresh subprocess (including a dedup re-import guard) before the override is enabled and the ledger-driven re-import recovers the gap.
  • Patches are temporary by default. The next self-update retires them (a core release is the proper fix's vehicle; if drift persists, the notice re-fires and the fix re-runs against the new core). thread-archive source fix <provider> --pin keeps yours forever. Every lifecycle step is audited in patch-log.jsonl, and thread-archive source list shows patched / patched (pinned) state.

Not supported

The scope is deliberately narrow. These are design decisions, not gaps waiting on a release:

  • More than one machine — and merging archives. An archive belongs to one machine. There is no merge tool, no sync, and no federated search across archives — a deliberate single-machine scope, not a technical wall. Thread ids are globally-unique ULIDs, so two archives never collide there; what a merge would still have to reconcile is the locally-minted event id space wired through the truth layer — the append-only event log, causality links, amendment records — by remapping one archive's ids past the other's. Mechanical, but unbuilt. Moving an archive to another machine is supported — carry the directory, or thread-archive backup restore <mirror> --to <home>; running two and reconciling them later is not.
  • Anything but macOS and Linux. The always-on pieces — watcher, scheduled backup, shared MCP server — are launchd LaunchAgents on macOS and systemd --user units on Linux. Public CI runs on Linux, including the systemd lifecycle against a real user manager. The launchd lifecycle is not something a hosted macOS runner can exercise — it drives the gui/<uid> domain, which needs a login session no hosted runner has — so it is verified on the maintainer's machine instead: macOS has the most mileage in daily use and the least in public CI. Windows is not supported, and no other platform exists here.
  • More than one user. No accounts, no authentication, no per-user scoping. The web viewer binds to 127.0.0.1 and assumes whoever reaches it owns everything in the archive.
  • Live capture of web chats. claude.ai, ChatGPT, and xAI arrive from account exports you download by hand — drop the ZIP on the viewer's /upload page (or into <home>/dumps/, or run thread-archive source import-account). The self-feeding path is the local agent harnesses.
  • Driving a conversation. The archive preserves and retrieves. It never writes back to a harness store and never sends a message.

Web viewer

The always-on watcher cohosts a local search + reader UI: thread-archive watch --web (the shipped watcher service passes it) serves at http://127.0.0.1:8787 — a stdlib HTTP server handing out a pre-built React bundle plus a few JSON endpoints, in the watcher's own process. One process, one SQLite engine — the viewer reads concurrently with the watcher's writes, which WAL makes safe (_store/_base.py). No second daemon: the viewer exists where the persistent URL is.

thread-archive web opens that URL in a browser. An opener, not a server. thread-archive web dev turns on the dev panels (below) and opens it.

The URLs are a supported interface. Other programs link into the viewer — editor "open in archive" buttons, sibling consoles' navbars, health probes — so these paths keep working:

Path What it is
/ landing: recent threads, global search
/search search results
/threads every thread
/stats token/cost analytics (/stats/model/<model> drills in)
/health the archive's own status page
/upload import an account export: drop the ZIP, and where to get one
/archive/<thread_id> one conversation, rendered
GET /api/health {ok, home} — cheap liveness for probes
GET /api/archive-link?id=<session-uuid> resolve a provider session id to its thread (below)

Every page route has a real-browser case in frontend/e2e/ — one Chromium navigation per route, asserting its landmark renders with no console errors and no unmocked fetch — and route-coverage.spec.ts keeps that a bijection, so a new route without a browser case reds the suite.

Dev panels are the exception to the table above, and deliberately not part of its contract. /retrieval reports on the search pipeline — served latency by warm and cold regime, per-stage costs — and /lab reports on what the bench has to measure it with. Both are maintainer's instruments rather than anything the archive is for, so a viewer does not have them unless its operator asks: one line in the home's config.json,

{ "dev_panels": true }

which thread-archive web dev writes and web --no-dev clears. The server stamps that answer onto every shell it serves and the app mounts their routes only when it is there, so without the line those addresses route nowhere — not hidden behind an unadvertised link, absent. It is read per request: flipping the line lands on the next page load, with nothing to restart. Their data comes from search_lab/, which lives in the source repo and not in an install, so a pip install serves a 404 there even with the line set, and the page says so.

Everything under /api/ other than those two backs the viewer's own bundle and is private — it changes with the frontend. So is the markup: the interface is the URL, not the DOM. No page view touches the conversation record; the stats pages do fold a derived rollup into the index, which is the rebuildable projection. The server binds loopback only, since it serves the whole archive with no auth (a non-loopback bind needs THREAD_ARCHIVE_WEB_NONLOCAL=1).

One endpoint writes: POST /api/upload takes an account-export ZIP and puts it in the drop zone the cohosting watcher already imports from, which is what /upload is a page for — the import itself stays the watcher's, with the settle and retain/quarantine rules it already owns. Reading is what a GET can do; every other method on every other path is a 405. A write carries two guards past the Host check every request passes: a loopback Origin, and an X-Archive-Upload header — which no cross-origin form can set, so a page this server did not serve must first win a preflight that is never answered.

Runtime is node-free: the bundle is built ahead of time and committed under _web/static/, so the install never touches node. Node is a build-only tool:

# rebuild the bundle after editing the frontend (node only here):
cd frontend && npm install && npm run build   # → ../src/thread_archive/_web/static/

Archive-links. With that persistent server, the archive owns the editor "open this conversation" link itself: GET /api/archive-link?id=<session-uuid>&source=claude-code resolves the session to its thread via ImportState and returns {thread_id, url}, or &redirect=1 → a 302 to /archive/<id>. (Local — no separate backend involved.) id may repeat — a caller that cannot tell which uuid it holds is the session id sends every candidate, best guess first, and the first that resolves wins; ids that were never imported are skipped, not fatal.

MCP

One server, two explicit process modes. thread-archive (archive-mcp) serves the read-only thread_search / thread_read tools. The process is also read-only by default. Setting THREAD_ARCHIVE_MCP_INGEST=1 opts it into local lazy catch-up ingest, throttled and cross-process-safe via the ingest-owner lock. This server exposes no write surface. Client config with catch-up enabled:

{
  "mcpServers": {
    "thread-archive": {
      "command": "archive-mcp",
      "env": {
        "THREAD_ARCHIVE_HOME": "~/.thread/archive",
        "THREAD_ARCHIVE_MCP_INGEST": "1"
      }
    }
  }
}

The shared HTTP server daemon is read-only by default too. Install it with thread-archive service install --mcp --mcp-ingest only when it should own catch-up; leave the flag off when the watcher already owns ingestion.

Similar and related projects

Preserving and searching AI conversation history is a crowded space, and a lot of the work in it is good — session-search neighbors (CASS, ctx, deja-vu, episodic-memory, synty, and more), agent memory layers (mem0, Letta, Zep), and the prior art outside AI (notmuch). The annotated survey — including where each neighbor leads and how thread-archive differs — lives in docs/related.md. The short version of the difference: most tools treat the harness's own files as the record and their index as a cache over it; archive treats preservation as the product — its own append-only truth log, backup with restore drills, and unmodeled provider fields preserved verbatim.

License

MIT — see LICENSE. The pre-built web viewer bundle contains third-party open-source packages (all MIT/ISC/BSD); their license texts ship in src/thread_archive/_web/THIRD_PARTY_NOTICES.md (regenerate with scripts/gen_third_party_notices.py when frontend dependencies change).

Origin

thread-archive is the standalone member of a larger personal project ("thread"), built to stand on its own — self-contained, no hosted backend or external services. It's young, though: expect the occasional rough edge.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

thread_archive-0.0.8.tar.gz (1.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

thread_archive-0.0.8-py3-none-any.whl (1.0 MB view details)

Uploaded Python 3

File details

Details for the file thread_archive-0.0.8.tar.gz.

File metadata

  • Download URL: thread_archive-0.0.8.tar.gz
  • Upload date:
  • Size: 1.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for thread_archive-0.0.8.tar.gz
Algorithm Hash digest
SHA256 650dfc12c130cf98a54cf87f1b9d9bd13c16ba7d10c56142a86765ea4b1f83d2
MD5 44870b100f884c3011798dacd8e33d63
BLAKE2b-256 8f13ce3a928313f74ca581be0ff4efc14b5c24ecbfd906a5a58b405b4fc24f80

See more details on using hashes here.

Provenance

The following attestation bundles were made for thread_archive-0.0.8.tar.gz:

Publisher: publish.yml on ellamental/thread_archive

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file thread_archive-0.0.8-py3-none-any.whl.

File metadata

  • Download URL: thread_archive-0.0.8-py3-none-any.whl
  • Upload date:
  • Size: 1.0 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for thread_archive-0.0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 934dac5371f99a3667acd10a8eab5a0d4e57cf71be0f6e4ddefc1113e6991bbe
MD5 103f94b622431887aaee6e09717534d7
BLAKE2b-256 66460a1b876e94de006d1d0ff13ebe3ae928aec6931a00a53c1b3c491e4f90df

See more details on using hashes here.

Provenance

The following attestation bundles were made for thread_archive-0.0.8-py3-none-any.whl:

Publisher: publish.yml on ellamental/thread_archive

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page