trackinizer🐾
Epistemological database for agent and human efforts, beliefs, and findings.
Watch the 27-second tour: the graph, a root's island and a Belief in it, Issues and Artifacts, and a message to an agent in the console, answered live.
Quick Start
# Mac:
# Required for quick install.
brew install uv
# Optional for a real Postgres backend; default uses pglite engine.
brew install postgresql@18 pgvector
Ubuntu/Debian:
# Required for quick install.
sudo apt-get install -y curl
curl -LsSf https://astral.sh/uv/install.sh | sh
# Optional for a real Postgres backend; default uses pglite engine.
PG_MAJOR="$(apt-cache depends postgresql | grep -m1 -oP 'postgresql-\K\d+')"
sudo apt-get install -y postgresql "postgresql-$PG_MAJOR-pgvector"
uv tool install trackinizer
Local server at http://127.0.0.1:8765.
trackinizer
CLI to trackinizer server.
trax
Centralized agent database for inquiries (Issues + Artifacts), work, and
knowledge. Three core tables (inquiries, edges, change_log) backed
by Postgres (real or PGlite). FastAPI on top.
types/ is the design contract. Every other module is a realization of
that contract over Postgres + HTTP.
The UI
The web app browses the same records the API serves. Its source is web/
(web/README.md), and the package ships it
built: trackinizer serves it at /app/, and / leads there. To use it on
your own machine, run the server in single-user local mode, then open
http://127.0.0.1:8765/app/:
trackinizer --no-auth
--no-auth makes every request an admin, so keep the server on localhost, its
default --host. Without it, the API accepts only API tokens, which is how
trax and agents connect, and /app/ follows the same rule: a browser gets
the sign-in page, and the package ships no browser sign-in.
--app-dir DIR serves the app built in DIR in place of the packaged one. In
a source checkout the server serves web/dist without it, once ./npm run
build has run in web/; the flag is for a build kept elsewhere, such as a
deploy's symlink to its current build. The server reads DIR on every
request, so switching the link puts a new build live without a restart.
Graph -- the inquiry web (Issues, Beliefs, Papers, Experiments, …) as typed nodes and edges, each root's subgraph gathered into an island and the roots listed.
Console -- the agents' live sessions: saved views, filters by agent and room,
messages alone or every tool call, and a line to message agents, with @
suggestions.
Belief -- a claim with its evidence for and against, its parents and children, a graph of what lies within two hops, and its activity.
Paper -- abstract, authors, and the papers it cites and is cited by.
Experiment -- its outcome, config and metric charts, and the Belief it proves.
Chat
A canvas's Chat talks to a partner, which is either the server's shared assistant or your own helper:
- An admin sets the shared assistant:
trackinizer --assistant ACTOR=EMAIL. - Or you pick "My local helper" in Settings, for your canvases only.
- Run your helper in a terminal, with your Claude or Codex CLI:
uv tool install trackinizer && trax profile url to <server URL> && trax profile token to <TOKEN>
trax helper claude
With "My local helper" chosen and no helper running, Chat says so and sends
nothing. docs/api.md has the rules.
The model
Everything in the system is an Inquiry, which has two variants: an
Issue is a unit of "work" and an Artifact is the output of that work.
Giving both a single type is what lets the same edges relate them -- work
can produce knowledge, and knowledge can elicit more work, without crossing a
type boundary.
Each row below lists the fields that class adds; every kind also has everything above it.
Inquiry # An effort, ongoing or completed.
│ id
│ seq
│ owner
│ account
│ status
│ title
│ description
│ labels
│ marginal_cost
│ subscribers
│ superseded_by
│ supersedes
│ produces
│ produced_by
│ created
│ modified
│
├── Issue # Work to pursue.
│ issue_kind
│ validation
│ priority
│ narrows
│ narrowed_by
│ requires
│ required_by
│
└── Artifact # Knowledge produced and cited.
│ proves
│ favors
│
├── Experiment # Empirical measurement.
│ codechanges
│ outcome
│ config
│ proved_by
│ favored_by
│
├── Belief # Proposition.
│ judgement
│ confidence
│ proved_by
│ favored_by
│
├── Paper # Bibliographic source.
│ abstract
│ authors
│ publication_type
│ venue
│ subvenue
│ publish_date
│ source
│ google_scholar_cluster_id
│ google_scholar_cites_id
│ cites
│ cited_by
│
├── CodeChange # One git commit.
│ sha
│
├── WebSearch # One query.
│ query
│ provider
│
├── WebResult # One URL.
│ url
│
└── AgentSession # A captured CLI run.
cli
cli_session_id
started
ended
rooms
opened_by_api_key_id
Relationships are directed and every one has an inverse view, so a parent and a child describe the same edge from either end:
OLDER (parent)
{narrows,requires} {produced_by,supersedes} {proves,favors}
▲ ▲ ▲
│ │ │
Issue ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄▷ Inquiry ◁┄┄┄┄┄┄┄┄┄ {Belief,Experiment}
│ │ │
▼ ▼ ▼
{narrowed_by,required_by} {produces,superseded_by} {proved_by,favored_by}
NEWER (child)
- Parents are always older than children, on each edge's own clock:
requires, which is completion-time.
- Any Inquiry can be
produced_byolder ones (its origins) and
superseded_by others (M:N knowledge surgery).
- An Issue can be
narrowed_by(broader to narrower) orrequired_by(it
proves/favorsgo from any Artifact to aBelieforExperiment
proves votes in the
proof predicate; favors is context that informs but does not vote.
cites/cited_byis Paper to Paper and stands apart from the six
See docs/epistemy.md for why each verb
is named what it is.
Storage
Three tables carry the model and other tables support them.
| table | holds |
|---|---|
| inquiries | one row per Inquiry, all kinds. kind discriminates; (kind, seq) gives the short ref (Issue#7) from a per-kind sequence. Optional columns are nullable, so NULL is the single encoding of "unset". |
| edges | every relationship, as (from_id, to_id, edge_kind). Citation edges carry valence. A CHECK constrains which kind pairs each edge kind admits, and cycles are rejected on insert. |
| change_log | append-only audit. Each row is one change with (old_, new_) snapshot pairs; milestone rows carry no delta, their signal is that they exist. Drives the trax recent feed and idempotency replay. |
Per-kind detail that does not fit one row lives in its own table, keyed
back to inquiries(id) and deleted with it:
| table | holds |
|---|---|
| experiment_metrics | (key, step, value) time series for an Experiment. CHECKs mirror the wire's validators -- non-blank bounded key, non-negative step, finite value -- so a stored row can always be read back. |
| session_records | the ordered record log of an AgentSession, (session_id, part, idx), each holding one trackinizer.lib.agent IR record as JSON. part is one source FILE (a session spans several); idx is DERIVED from position in it, which is what makes re-ingest idempotent. Backs the live console feed. |
| session_manifests | one row per part: what the file was called, what it declared, and how much of it is live. |
| session_ciphertext | Thinking.encrypted, split off so retention can drop it without touching what search indexes. |
| session_slash_commands | commands the human typed into the TUI. Not records: never written to any session log, so they hold no idx. |
| inquiry_embeddings | one vector per (inquiry_id, model) for semantic search. |
Auth is three more: users, api_keys (scrypt-hashed, prefix-indexed),
and allowlist.
The typed fields on Inquiry (produces, supersedes, citation lists)
are projections the Store fills by reading edges -- the edge table is the
real storage.
trax export > graph.jsonl writes the graph as JSON lines: the rows of the
tables above, minus embeddings, ciphertext, and auth. Unlike a copy of the
PGlite datadir it is not tied to the storage internals that wrote it, and an
unchanged graph exports the same bytes twice. Line format:
docs/api.md section 3.24.
Package dependency graph
Four layers, two legs sharing one contract spine. An arrow means "imports / depends on" and points toward the dependency.
┌──────────────┐ ┌──────────────┐
│ trax │ │ server │ leaves: nothing
│ (CLI) │ │ (__main__) │ imports these
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ client │ │ api │ server leg adds
│ (httpx SDK) │ │ (FastAPI │ Store; client leg
│ │ │ handlers) │ adds httpx
└──────┬───────┘ └──────┬───────┘
│ │
└──────────────┬──────────────────────┘
│ both import
▼
┌───────────────┐
│ wire │ transport contract = THE API
│ bodies │ definition (request/response
│ routes │ models, route table, filters, refs)
│ filters │
│ refs │
└───────┬───────┘
│
▼
┌───────────────┐
│ types │ domain dataclasses;
│ inquiries │ imports nothing internal
│ edges │
│ change_log │
│ cost │
└───────────────┘
Two legs, one shared spine:
- client leg:
trax→client→wire→types - server leg:
server/__main__→server/api→wire→types
types/ + wire/ + client/ form a
self-contained client distribution. wire/ and client/ never import
api, server, store, web, fastapi, asyncpg, or trax, so a
published Python client carries no server dependencies.
Layering rules
types/is a closed island. Nothing intypes/imports anything
ColumnSpec metadata in types/ are the
data design contract; the rest of the package realizes it.
- **
wire/is the API contract; it is the single definition of the
FieldSet[T], FieldOp[T], FieldMutation, Submit*, edge
bodies), the Filter/Ref shapes, and the route table the server
registers from and the client builds requests from. It imports
types/ and nothing else internal. Server and client both derive
from it, so neither hand-writes a path or a body shape -- that is
what prevents server/client/doc drift.
client/is the standalone SDK;traxis a thin CLI over it.
client/ is wire/ + httpx. trax owns only CLI concerns
(grammar, parsing, rendering) and calls client.Client. Neither
client/ nor wire/ may import the server side or the CLI.
- No cycles. Each arrow above goes one direction.
server/primitives.py imports server/setter_dispatch.RUNTIME_HOOKS
and mutates it at import time (late-binding the results /
codechanges validators). server/store.py imports primitives,
so the side effect lands before any Store instance is constructed.
server/sql.pyis orthogonal. It loadsassets/schema.sql
server/schema_gen.py substitutes generated bodies into
the loaded text at bootstrap; the two never import each other.
server/api/is the HTTP boundary. Routes are thin:
Store method, serialize
the result. Anything non-trivial belongs in server/store.py, not in
a route.
Reading order
Pick one of these orders depending on what you came in for.
- "What does Trackinizer model?"
types/. Read inquiries.py, then edges.py, then
change_log.py. Read docs/design.md for the model and philosophy.
- "What is the HTTP API / how do I avoid drift?"
wire/routes.py: the route table is derived from the
ColumnSpec metadata in types/inquiries.py. The server registers
handlers by iterating it (server/api/edit.py, edge.py), the client
builds requests from it (client/client.py), so neither hand-writes a
path. server/api/routes_drift_test.py fails if a handler diverges
from the table.
- "How does a submit reach the database?"
server/api/submit.py → wire/bodies.py (body validation) →
server/store.py (submit_X) → server/primitives.py
(insert_inquiry, insert_edge).
- "How does an edit fan out notifications?"
server/api/edit.py → server/store.py (set_X → _set_field) →
server/setter_dispatch.py (RUNTIME_HOOKS[col]) → server/notify.py
(post-commit buffer + LISTEN/NOTIFY).
- "How does the schema get built?"
server/__main__.py → store.bootstrap() → server/sql.py
(schema_migrations()) → server/schema_gen.py
(substitute_schema_placeholders()) → the four _generate_*
functions. Generated text is derived from ColumnSpec metadata on the
dataclasses in types/. See docs/db_schema_migration.md for running
migrations, squashing, and deploying schema changes.
- "How does a
dependency_changedcascade work?"
store.emit_change → store._cascade_dependency_changed →
store._parent_edges → types/edges.EdgeKindPolicy. The policy
table is the single declaration site for which endpoint of each
edge kind is the dependent.
Running
trackinizer # pglite (default)
trackinizer --engine pg --dsn ... # against real Postgres
trackinizer --no-auth # single-user local mode: everyone is admin
trackinizer --app-dir DIR # the web app built in DIR at /app/
trackinizer --assistant ACTOR=EMAIL # the shared Chat partner (or $TRACKINIZER_ASSISTANT)
trackinizer --no-web # without /api/web, /app/ or the login page
trax is the client half and talks to any reachable server, so it is
useful on its own:
trax help # grammar and subjects
trax issue # list issues
trax env # list the org's environment variables
trax machine # list the machines campaigns may run on
From a source checkout, both are uv run python -m trackinizer.server
and uv run python -m trackinizer.trax.
See example.sh for a worked end-to-end submit/edit/query session.
System dependencies
Integration tests (@pytest.mark.integration) use a Postgres server backend
via pytest-postgresql and also requires pgvector.
PGlite bundles its own vector extension, so the default pglite engine has
no system deps.
Ubuntu/Debian
# Extension packages are always named postgresql-NN-<ext> -- there is no
unversioned alias -- so derive NN from the metapackage instead of hardcoding
it (18 above 24.04, 16 on 24.04).
PG_MAJOR="$(apt-cache depends postgresql | grep -m1 -oP 'postgresql-\K\d+')"
sudo apt-get install -y postgresql "postgresql-$PG_MAJOR-pgvector"
macOS
# Homebrew's pgvector supports postgresql@17 and @18.
brew install postgresql@18 pgvector
See also
Sibling projects in the rekursiv-ai family:
- sagent — The self-mutating multi-provider coding-agent CLI and typed Python library.
- wesearch — Web search, resilient page fetch, and scholarly-paper lookup without a browser stack.
- madcatter — Rich-based Markdown renderer for the terminal; ships the
mdcatCLI. - priml — Composable PyTorch building blocks: models, optimizers, losses, and a step-based training loop.
- configgle — Hierarchical experiment configuration in typed pure-Python dataclasses instead of YAML.
- copybarista — Bidirectional source sync for publishing OSS-ready trees from a monorepo.
Citing
If you find our work useful, please consider citing:
@misc{rekursivai2026trackinizer,
title={Trackinizer - Epistemological database for agent and human efforts, beliefs, and findings.},
author={Joshua V. Dillon and Dan Kondratyuk},
year={2026},
howpublished={Github},
url={https://github.com/rekursiv-ai/trackinizer},
}
