tarek.kharsa

home / lessons / pi · lesson 4

Data modelPi · lesson 4 of 83 min read

Never mutate history

Sessions as an append-only tree: branches, summaries and even edits are new entries.

Problem

Agents rewrite the conversation to branch, compact or hide a message, and lose what actually happened: you can't go back, audit it or replay it.

Idea

Store each session as an append-only tree of entries, and build the model's context as a projection of the active branch.

An agent session looks like a list of messages, and most tools store it as one. Then you want to go back three messages and try something else, or compact a long conversation, or hide a huge tool result from the model, and the list gets rewritten. What actually happened is gone.

This repo never rewrites a session. It's an append-only tree, and the conversation the model sees is a projection of it.

Try it: branch, compact, hide

every action appends an entry; the right side is what the model sees

Branching moves the leaf to an earlier entry and appends from there; the old branch stays in the file. Compaction appends a summary entry that records the first entry it kept. Hiding a message appends a context edit. The model's context is rebuilt by walking from the leaf to the root.

The data model

A session is a JSONL file. After a header line, every line is an entry with an id and a parentId:

{"type":"message","id":"a1b2c3d4","parentId":null,...}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4",...}
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8",
 "summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}
{"type":"context_edit","id":"g6h7i8j9","parentId":"f6g7h8i9","targetId":"c3d4e5f6","replacement":null}

The "leaf" is the current position. Branching from an earlier entry just makes a new child of it, so alternatives live side by side in one file.

Every change is a new entry

Compaction

Stores a summary, firstKeptEntryId and a full system-prompt and tool checkpoint. Older entries are replaced in the context, not in the file.

Branch summary

When you leave a branch, a summary of the abandoned path is attached where the new branch continues, so its findings aren't lost.

Context edit

Hides or replaces one earlier entry in future model context. "The target entry and its metadata remain unchanged in raw history, UI, exports, and session accounting."

System messages

Prompt and tool changes are appended as patches (sections by name, tools added or removed). Replaying them yields the current prompt; there's no separate mutable state.

The projection

  1. Walk the pathFrom the leaf back to the root, collecting every entry on the active branch.
  2. Apply the latest compactionUse its checkpoint and summary, then the entries from firstKeptEntryId up to it, then everything after it.
  3. Apply context editsFor each target, the latest edit on the active branch wins. Edits are branch-relative: go back before an edit and the original reappears.
  4. Convert to messagesEntries that don't participate in context (usage, extension state, labels) are kept for the UI and accounting but produce no model message.

What you get for free

  • Undo across everything: any earlier point is reachable, including before a compaction.
  • Honest accounting: usage and cost include summaries and cache warming, because those are entries too.
  • Safe evolution: the format is versioned (v1 linear, v2 tree, v3 renamed roles) and old files are migrated on load.
  • Grep-able history: a session is a text file you can parse in a few lines.

Steal this

Never mutate history. Make every change (a branch, a summary, an edit) a new entry pointing at its parent, and build each view as a projection of the active path. Undo, audit and replay stop being features you have to add.

Use it in your code

A prompt for your coding agent

Run it from the root of your repo. It checks whether the idea fits before it changes anything.

Make this app's history append-only, and derive what each consumer sees as a projection.

The idea: never update or delete history in place. Every change (a branch, a summary, an edit, a hide) is a new entry pointing at its parent. The current view is built by walking from the active leaf back to the root and applying those entries, so you can always go back, audit and replay.

1. Find the data this app mutates in place that users or the system may need to revisit: conversation or chat history, documents, orders, workflow state. Show me the tables or files and the code paths that UPDATE or DELETE them.
2. Tell me whether an append-only model fits. If the data is truly disposable or the volume makes it impractical, say so and stop.
3. If it fits, propose the smallest version: entries with id, parentId, type and timestamp; a pointer to the current leaf; and a projection function that walks leaf to root and applies summary or edit entries. Edits should be new entries that target an earlier one, and the latest one on the active branch wins.
4. Implement it for one entity, with tests for: branching from an earlier entry without losing the old branch, an edit that changes the projection but not the raw history, and rebuilding the same projection after a restart.

Plan migration of existing data separately, and keep the change small.

Read the source

Where this comes from

Pi (earendil-works/pi), the minimal, extensible agent harness. The files behind this lesson: