A session is a DAG, not a straight line. Every branch's conversation should enter memory, while shared prefixes should not enter twice. This page illustrates the fact that determines both. The main document is
docs/reference/design/memory/written-marker.md, with its Chinese counterpart at
written-marker.zh.md; the surrounding memory subsystem is described in
overview.md and its mechanism diagram in
architecture.html.
Layer 1 · Current running mechanism
Position cursor: one integer per session, stored inruntime.json. After branching, it omits an entire branch or writes only a fragment starting mid-conversation while reporting success.
Layer 3 · Finalized, not yet implemented
Mark nodes as written and walk backward from each branch tip until the first marked node. Seven files, about 200 lines changed and 130 added, plus one-time migration.
Layer 4 · Unscheduled
Store no separate tracking state; determine pending work from memory's own content. Indexing and invalidation rules are the obstacle, larger than all of layer 3.
Progress is one integer keyed by session id. Six branches in a session all share that integer.
Diagram details: Writes a fragment starting mid-conversation, then marks the entire range as written.
Ordinals are produced by flattening; they are not message properties._records contains
for index, message in enumerate(messages): the number exists only when the chain becomes a list and disappears with that list. One message reached along two paths can receive different numbers; different messages at equal depth can receive the same number. The ordinal sequence also includes rows memory does not record._records numbers a row by its position across the whole branch, including skipped rows. Tool-call rows and runtime-scheduled turns consume ordinals, so rows beyond the cursor and corresponding turns are not a simple subtraction. The two counts above were measured rather than calculated. This representation even prevents correction.advance_cursorRaises
cursor cannot move backwards when a new ordinal is lower than the stored one, but moving backward is precisely what branching requires. A monotonic counter cannot be repaired into a branching-conversation representation.
02Layer 2 · Other implementations
references/ contains nine directories;claude-code/ contains only five files from one tool, leaving eight frameworks with meaningful answers. Four have genuine long-term memory.
Only two actual patterns
Diagram details: Memory tracks nothing: it runs after each turn, so execution order determines progress. Stateless streaming client; caller supplies the entire message array each time. CLI/ACP backends cache only downstream session ids; HTTP retains the latest twenty turns in memory. None writes “memory consumed this message” onto the conversation record. Diagram details: pi-mono walks to the nearest common ancestor and collects the intervening segment without storing state. Walk from the abandoned leaf to the common ancestor; the two intervening nodes require summaries. This is a real third option besides mutating nodes. Layer 3 rejects it for reasons, not because it lacks precedent.
Others duplicate at forks; we omit.Codex-cli reextracts a forked thread from the beginning, and openclaw rescans a new file from line 0. Neither loses turns; both reread consumed content. Duplication is recoverable: nightly organization merges passages describing the same fact, and writing a turn twice costs one model call. Omission is not recoverable because no process revisits those turns. Two useful behaviors to adopt.① claude-code-leaked recounts all messages if the cursor target is missing; its comment explains that returning zero would permanently stop extraction for the rest of the session. An adjacent counter lacks that fallback and silently remains zero, so this is a deliberate decision rather than a default. ② pi-mono's append-only marker records.
03Layer 3 · Current proposal: node markers
The boundary must use something that identifies a message: the node itself. Record that memory processed a turn on that turn's node.
Where and when to mark
Diagram details: Append-only and content-addressed; archiving twice is byte-identical. Contain block IDs and footnotes; partial installation corrupts state, so install all or none. Measured: twenty turns hit the same rejected edit and reported success while the topic file contained one byte.
All required mechanisms already exist.metadata is an open dictionary. The message adapter puts unknown fields there and readers expand keys back to top level, so keys must not collide with message fields (idrolecontentpredecessorcallertimestampstatus are already reserved). Mutating stored nodes is also established:rewind sets
rewound, revert setsreverted, and turn finalization rewrites node files in place with checkpoint stamps;SessionNodeWriter.update already performs the operation markers need, first passing through
SessionStore._open to rebuild stale indexes, then usingatomic_write_text to rewrite directly, avoiding both forbidden effects above. Measured cost.The session index cache rebuilds once; 289 nodes and 4.2 MB take14–50 milliseconds and the cost is self-limiting. Git grows because the session directory isgit add -A each turn, somark only this batch, never rescan and rewrite the whole session; otherwise every turn adds the entire session's size, producing quadratic growth.
Files to change
One-time migration: mark only verifiable contiguous prefixes
Counterargument: an external set is exact; traversal is not
Mark nodes
Memory stores its own ID set
How to find unwritten turns
Walk backward from the tip until the first marker
Traverse the entire branch and subtract the set
At a fork
Reach the shared prefix, find a marker and stop
Skip prefix IDs already in the set
What to store
One additional field on an existing node
One file per thread containing every written ID
Growth
No growth
One entry per message retained for the session's lifetime
Read cost
A few steps backward from the tip
The thread's entire inventory
Migration
Mark only DAG-contiguous prefixes from trusted archive candidates
Seed the set with the same safe prefix
Lost marker or entry
Traversal stops early and all older turns become unreachable
That turn is retrieved again
Coupling
Memory writes a field into session storage
Memory owns its tracking state
Diagram details: No body length exists; a second valid header could be verbatim user content, so rewrite subsequent records. This argument is valid:The position cursor is an O(1) small-file read entirely outside the session tree. Node markers record the same fact in another location whose directory mtime is used elsewhere for change detection. A set is exact; traversal assumes markers form a branch prefix. Oldest-first batches establish that assumption, but it is not a validated invariant.
The other half determines the choice:External tracking is cheap only while it is one number. To remain correct after forks, it must become an ID set, introducing file placement, migration, permanent ID retention and read cost growing with the session. Session storage already records a turn's origin, whether it is runtime self-talk, whether it is a branch root, and whetherrewind has rolled it back. A memory-written field belongs to that same category.
04Layer 4 · Ideal design
Layer 3 answers what to implement next; this layer asks what a redesign would look like. The following are not phased versions of markers, but another formulation of the same problem.
Diagram details: Three gaps: topic format lacks the field, retrieval lacks branch filtering, and nightly organization lacks mutually alternative branch semantics. Obstacle: head writes are spread across five sites; their sole shared function discards the old value, and no event carries the movement. Implementation status (2026-08-11).Layer 1 records pre-migration implementation, layer 2 audits open-source frameworks, and layer 3 is implemented: session nodes usemetadata.memory_written_scriptorium = <workspace-id> to record writing state; the current branch derives its unwritten suffix from markers. Mark only successful batches that actually modify memory; session completion processes current head before other live branches.
RuntimeState.cursors andadvance_cursor are removed. First write on legacy installations validates legacy headers and the strict valid prefix of
sources/openprogram/_v2/*.md: legacy accepts only the first valid header after the title; v2 stops at invalid frames. Candidate IDs undergo the same record filtering as online writing on real DAG paths; mark only the contiguous prefix before the first gap, allowing later tails to be rewritten. Delete oldcursors only after all session markers succeed, retaining it on failure for retry. Structured speaker fields, trusted v2 frames and speaker filtering are also implemented. Historicalmemory backfill deliberately ignores node markers and selects only trusted Sources not referenced by Topics, excluding pending entries. It neither adds nor removes session markers. Node markers record online-writer processing state; Topic references record whether historical Sources entered retrievable content. These definitions are not interchangeable. Historical backfill has not run in the canonical workspace. Layer 4 remains unimplemented.