In one sentence: TGMS keeps history in files written once and never edited, with corrections recorded separately and folded in later — because no backend we could adopt met all of what TGMS promises at once.
Why this matters
TGMS makes four promises at once, and the conjunction is the hard part:
- A correction must not erase what was previously believed. Correct an account's country on Tuesday, and a question about Monday's belief must still answer as Monday would have. That needs two clocks per fact — when it was true, and when the database held that version. That pair is what bi-temporal means.
- The same question returns byte-identical answers on a re-run, another machine, a second backend.
- Temporal graph operations are first-class — time-respecting traversal, temporal motifs, interval joins.
- It runs embedded, in the agent's process, with cappable memory.
Most systems deliver any three. We shipped on DuckDB and Kùzu for months and each broke on a different one: versioning was emulated above the storage layer, making the belief clock a query convention rather than a property of the data; identity was re-derived per row; memory was whatever the engine chose. Kùzu's acquisition settled the timing, not the question.
The idea in plain language
The design rests on one choice: finished files are never edited. An accountant correcting a closed ledger does not overwrite the page — the closed book is evidence of what was believed then. They write an adjusting entry superseding the old figure, and later issue a consolidated statement folding the adjustments in.
TGMS stores history that way. A segment is a closed page: relationship versions written once in a compressed columnar layout, never modified. A correction record is the adjusting entry — an append-only note that some version stopped being believed at a given moment, original untouched. A manifest is the index: which segments and correction records constitute the store at one point in its history. A commit is a new manifest; a reader pins one, and because everything it names is immutable, its view cannot shift however many writes land meanwhile.
What immutability buys
"Easier to reason about" is a slogan. The specific list, each item otherwise its own subsystem:
- Snapshot isolation costs one integer — a reader pins a generation: no version chain, no undo log, no reader lock.
- Memory-mapping is safe by construction. Bytes cannot change under a live mapping — mmap hands a file to a program as if it were memory — so files are mapped rather than copied through a buffer pool.
- A checksum verified once stays verified, and the segment cache cannot go stale: a cached segment is the right bytes or absent.
- Replay is deterministic, which makes byte-identical answers testable rather than aspirational.
What we measured
registry-v2-1m-4sys and
registry-v2-10m-3sys.The storage result
The denominator matters more than the numerator. Bytes per relationship version is the whole store directory on disk — segments, manifests, correction records, dictionary — divided by the 1,000,269 relationship versions it holds.
registry-v2-1m-4sys, one run, one host). The comparison does
not hold compression or indexes equivalent: all four are durable, but
ClickHouse is lz4 with no secondary indexes, DuckDB is uncompressed, two
thirds of PostgreSQL's total is covering indexes, and TGMS keeps its query
indexes in memory rather than on disk — so its number buys the least query
readiness, paying for that in a slow first query and a tax on later scans
(post #10).One case study, and it is a null result
Performance work produced a long table of hypotheses that measurement refuted; it lives in engine_lessons.md, because a chronology of misdiagnoses is a reference, not an explanation. One entry earns space here.
A full-window scan at ten million events was slow, diagnosed as bound by materialisation — copying selected rows out of the columnar layout — so materialisation was parallelised across threads. The number went from 811 ms to 819: nothing.
A fifteen-minute stage probe explained why. The parallel path could run only when selected row ranges from different segments did not overlap. That check was all-or-nothing, and it never passed: a correction writes a superseding version into a segment whose key range overlaps the original's, so one correction anywhere fails the check for the whole scan. The fast path was not underperforming; it was unreachable on any store containing a correction — which is every store TGMS is built for.
The fix was to stop treating disjointness as global: group selections into clusters of overlapping key ranges, materialise clusters in parallel, sort-merge only within one. At ten million events 370 of 371 clusters are singletons. The scan fell to 330 ms, and the parallel code that had measured as worthless became the thing doing the work.
What this means in practice
What you gain. Corrections are cheap and never destroy history
(4.66 ms against a 100k-row
store). Readers are lock-free and see a consistent store without coordinating
with writers — a reader opens with read_only=True, pins the
generation it opened, and a live writer costs it
0-3% of query latency.
Answers reproduce across backends and thread counts, which is
what makes post #7's six-system comparison possible. A 10M-event suite fits
under a 2 GB cap
(1.76 GB peak).
What you still pay — three structural costs:
- Commit latency. A commit is several fsyncs plus a fresh manifest, so one-event-per-commit writing is slow: 95 events/s at batch 1 against 44874 at batch 1000. One correction also grows the store by about 64 KB, nearly all manifest.
- Warm-up. Query indexes are built in memory on first read, so the first query in a fresh process costs about 0.3 s at 1M events and 3.1 s at 10M — even a point lookup. Long-lived processes amortise it; one-shot scripts pay it once per run, which at 1M is now no worse than an embedded database's own open.
- Compaction. Folding corrections into fresh segments is fast (0.41 s for a 100k-row store) but writes new files and deletes none, so the store grows until generation collection runs.
What this result does not show
- Not that a general backend could not work — only that ours could not express bi-temporal versioning, deterministic identity and bounded memory at once without building most of this layer above them anyway.
- The case study is one probe run, one host, one query shape, one scale; the 811 → 819 ms step is a null result inside noise, not a regression.
- Nothing here establishes durability under hardware fault, distributed operation, multi-writer concurrency, or format migration — the engine is single-writer today.
Takeaway
The engine exists because bi-temporal correction, deterministic replay, temporal operators and bounded embedded memory are one requirement rather than four, and nothing available satisfied the conjunction. Immutable segments plus append-only correction records makes it cheap: consistency becomes an integer, a correction becomes an append. The bill arrives as commit latency, a slow first query, and a maintenance commitment measured in years.
Evidence and reproduction
benchmarks/results-v1/. The engine is validated
against a 500-case brute-force oracle it was never allowed to modify.Continue reading
- Before this: why an agent needs two kinds of time.
- After this: where TGMS is fast and where specialists still win, and what a resident index costs an unrelated scan.