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:

  1. Spec (this file) — objects, invariants, proof obligations.
  2. Dafny — encode the contract, verify the obligations.
  3. 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

  1. Spec (this file) — math + obligations, no implementation.
  2. Dafny — encode the contract; dafny verify discharges P1–P6.
  3. TypeScript — port the verified core (~/.dotnet/tools for Dafny, node/tsc for TS).