Time-Travel Chat: First-Principles Spec
Table of Contents
1. Scope
Objects, invariants, and proof obligations for conversation-tree forking — independent of implementation. The requirements fall out from the math; the empirical exploration and the schema archaeology supply the data.
Sequence:
- Spec (this file) — objects, invariants, proof obligations.
- Dafny — encode the contract, verify the obligations.
- TypeScript — port the verified core.
2. Objects
2.1. Entry (a chat "sha")
An entry is a record:
Entry = { uuid : UUID
parentUuid : UUID | nil ;; nil = root
type : "user" | "assistant" | "summary" | "system"
message : { role: string, content: Content }
version : SemVer ;; e.g. "2.1.170"
timestamp : ISO-8601
... } ;; ~19 additional fields (see version-drift)
uuid is opaque (not content-addressed); parentUuid links to the
preceding entry. The pair forms a singly-linked list per session.
2.2. The conversation DAG
A session is a sequence of entries: e_0, e_1, ..., e_n where
e_i.parentUuid = e_{i-1}.uuid (or nil for e_0).
A fork creates a second session sharing a prefix:
Session A: e_0 → e_1 → e_2 → e_3 → e_4 Session B: e_0 → e_1 → e_2 → f_3 → f_4 (forked at e_2)
The shared prefix (e_0, e_1, e_2) is file-level: session B copies
the JSONL bytes of entries 0–2, then appends new entries. This is
path-copying (Driscoll et al., 1989), not pointer sharing.
2.3. Schema version
Each entry carries a version field. The
version-drift analysis found:
- 38 distinct versions (2.1.2 through 2.1.170)
- Hard breakpoint at 2.1.78: pre-stabilization entries lack ~19 keys
- Convergence: 2.1.161 is -1 key, 2.1.168+ is 0 keys vs current
A fork crossing a version boundary must backfill the missing keys for the current client to accept the replayed prefix.
3. The fork contract
3.1. Resumption point
Legal fork points are tool-balanced turn boundaries: the prefix must
end after a complete assistant turn, never inside a tool_use /
tool_result pair.
∀ prefix P: |tool_use entries in P| = |tool_result entries in P|
3.2. Append-only / immutable past
A fork only adds entries after the resumption point. The prefix is immutable — the grandfather paradox is unrepresentable.
fork(session, uuid) = let prefix = entries(session)[0..indexOf(uuid)] in newSession(prefix ++ [freshEntry(...)])
prefix is byte-identical to the original. The fork diverges only
after the fork point.
3.3. Version migration at the boundary
When a fork from version v_old is resumed under client v_new:
∀ e in prefix: missingKeys(e, schema(v_new)) must be backfillable from defaults
The version-drift migration table quantifies backfill cost per era.
4. The merge contract (speculative)
Merge is partial, not total — stated honestly as a fold over diverged branches, not a confluent join.
A merge of session A and session B (forked at e_k) produces a new
session C:
C = prefix(A, k) ++ interleave(suffix(A, k), suffix(B, k))
Open: the mergeable unit is the artifact (code, file, test result), not the conversation turn. Merge operates on what the turns produced, not the turns themselves. See the harness inversion thesis in the companion note.
Obligations a merge must discharge:
- R1: merged session is well-formed (all invariants below hold)
- R2: no entry from either branch is lost
- R3: the resulting session replays cleanly under
claude --resume
5. Properties to prove
Proof obligations (target: Dafny, mirrored by test.check PBT):
| ID | Property | Statement |
|---|---|---|
| P1 | unique-uuids | No two entries share a uuid |
| P2 | no-dangling-parents | Every parentUuid resolves or is nil (root) |
| P3 | tools-balanced | Every root→leaf path has \vert tool_use\vert = \vert tool_result\vert |
| P4 | fork-preserves-ancestry | Forking off u leaves ancestry(u) unchanged |
| P5 | append-only | A mutation never removes or rewrites an existing entry |
| P6a | version-backfill | Backfilled keys validate against the target schema |
| P6b | content-preserving | Backfill adds keys only; existing content is byte-identical |
P1–P5 are structural invariants checkable on the JSONL. P6a/P6b require the version-drift data (the per-version key-set union from the schema archaeology).
6. Toolchain
- Spec (this file) — math + obligations, no implementation.
- Dafny — encode the contract;
dafny verifydischarges P1–P6. - TypeScript — port the verified core (
~/.dotnet/toolsfor Dafny, node/tsc for TS).