Repository navigation
Shared-prefix session lineage: prototype and copy-cost baseline #5439
Description
Problem
Every revision and branch in Maka materializes the sliced prefix into a new Session: transcript messages, ledger events, artifacts, model_call rows, and linked references are physically copied on every fork (see session-revision-coordinator.ts). Fork cost therefore grows linearly with history length, and every Edit-and-resend, Branch, and future retry pays it. There is no measured baseline for what this costs at representative history sizes, so we cannot currently say whether it is a non-issue or the dominant write cost of conversational branching.
This came up in #5132 while deciding the retry model. Regenerate removal proceeds independently; this issue is about the storage model underneath all revision operations.
Desired outcome
A decision, backed by evidence, between two storage models:
A. Materialized prefix (status quo). Each Session is a self-contained unit of ownership: delete means delete its rows, export/import bundles stand alone (load-bearing since #5308), usage attribution is a group-by on sessionId, and ledger repair never crosses session boundaries.
B. Shared-prefix lineage. A revision or branch stores topology metadata (parentSessionId, throughSequence, local suffix) and shares an immutable history prefix instead of duplicating it, giving O(1) forks. The child references a fixed snapshot boundary: parent continuation, repair, or compaction must not change the prefix a child observes. Under this model, deletion becomes reference-counted (history is reclaimable only when no session references it), inherited history creates no new usage accounting facts, export materializes the dependency closure at the boundary, and physical repair preserves logical history identity while logical changes require new versions. Checkpoint materialization bounds ancestor-chain read depth; content-addressed immutable artifacts fit naturally.
Note that exclusive storage is not strict today: the context-offload copyReferences path already creates target-Session references while reusing the same blobId. Mapping that existing sharing boundary is part of the work.
The concrete deliverables, in order:
- Copy-cost baseline. Measure write volume, latency, and storage amplification of a revision copy at representative history sizes. This currently has no owner and no tracking issue.
- Sharing-boundary map. Document which payloads are already shared across Sessions (
blobIdreuse and friends) and which are duplicated. - Prototype. A small storage-layer prototype of shared prefix + local suffix that verifies inherited-history and model-input stability across parent continuation, parent deletion, and repair, plus standalone export/import of the child. Compare write volume, read latency, and recovery cost against the materialized baseline.
- Decision record. The baseline informs urgency and migration timing, not the identity/accounting semantics, which should be settled explicitly.
Alternatives or workarounds
Keep materialized copies and optimize the copy path itself (selective sharing of artifact payloads, deferred copy) if the baseline shows cost concentrated in a few payload types. A hidden-sibling variant inside one Session was considered in #5132 for retry and rejected there; it does not address Edit/Branch copy cost either.
cc @likun666661 @M4n5ter — you both converged on shared lineage in #5132. @M4n5ter would you drive the prototype? @likun666661 would you co-review the lifecycle semantics? I can take the baseline measurement.
Activity
likun666661 commentedon Sep 17, 2026
I think the most useful mature precedent is not a branch-capable database by itself, but a combination of ideas from Git/Nix, Iceberg, and copy-on-write snapshot systems, implemented above the existing SQLite stores.
The key refinement is that lineage should not be represented only as:
A copy currently spans several independently stored domains: messages, Runtime Events, task ledger facts, model-call facts, artifacts, and context references. One sequence number does not necessarily define an atomic boundary across all of them. I would instead make the durable dependency an immutable conversation snapshot:
A settled Turn atomically publishes a snapshot manifest. Edit/Branch then creates a new
SessionRefpointing at that exact snapshot and appends a local suffix. The child does not need the parent Session to remain alive, and later parent appends, repair, or compaction cannot change what the child observes.Storage shape
I would keep SQLite and introduce an application-level immutable object store rather than replace the database:
Messages and Runtime Events should probably be sealed into bounded immutable segments rather than stored as one object per row. The active Turn can continue using a mutable session-local tail; once it settles, the tail is sealed and a new snapshot is published. This matches the current rule that an active Session is not copyable.
The existing context-offload path is already a useful local precedent:
copyReferencescreates a target-Session reference while reusing the same content-addressedblobId. The first migration step can extend that ownership/reference split to the large payloads currently copied bycopyConversationArtifactsandcloneConversationRuntimeLedger, before changing every read path.Lifecycle semantics
I would borrow the following rules:
Deletion should remove the Session ref, not immediately delete shared history. Reference counts may be maintained as an optimization, but I would not make them the source of truth: crashes and partial migrations can make them drift. The authoritative collector should periodically mark from all live Session heads plus temporary roots held by active operations/exports, traverse object edges, and sweep unreachable objects after a grace period.
Usage accounting should preserve stable physical model-call identities. A Session may display inherited usage, but global spend must deduplicate by the original model-call fact rather than charge once per reachable Session.
Export is the correct place to materialize. It should walk the selected snapshot's dependency closure and emit a self-contained bundle. The fact that export must be standalone does not require every internal fork to eagerly duplicate that closure.
Repair and compaction must distinguish logical identity from physical representation. Rebuilding indexes, checkpoints, or packed segments may preserve a snapshot identity; changing logical history must publish a new snapshot rather than mutating content already referenced by children.
Bounded reads
I would avoid making every read recursively traverse an unbounded Session-parent chain. Snapshot manifests should reference immutable history segments directly, and the implementation can add checkpoint manifests or flatten equivalent physical representations when depth or segment count crosses a threshold. This keeps ordinary forks O(1) while bounding reads without changing logical snapshot identity.
Suggested prototype order
This is effectively a small Git/Nix-like object database implemented on top of SQLite. It keeps SQLite's operational advantages while moving Session independence from "owns an exclusive physical copy" to "owns a stable immutable root and can be used, deleted, and exported independently."
简体中文
我认为最值得借鉴的不是直接采用某个“支持分支的数据库”,而是把 Git/Nix、Iceberg 和写时复制快照系统的成熟机制组合起来,继续构建在现有 SQLite 存储之上。
关键点是:lineage 不应只表示为:
一次会话复制横跨 messages、Runtime Events、task ledger、model-call facts、artifacts 和 context references 等多个独立存储域。单个 sequence 不一定能定义它们之间的原子边界。更稳妥的方式是把真正的数据依赖表示为不可变 conversation snapshot:
一个 Turn settled 后原子发布 snapshot manifest。Edit/Branch 只需创建指向该 snapshot 的新
SessionRef,随后追加自己的 local suffix。子 Session 不依赖父 Session 继续存在,父会话后续追加、repair 或 compaction 也不能改变子会话看到的历史。实现上建议保留 SQLite,增加应用层不可变对象存储。消息和 Runtime Events 可以封装成有界的 immutable segments;活跃 Turn 继续写 session-local mutable tail,settled 后 seal 并发布新 snapshot。这也符合当前 active Session 不可复制的约束。
当前 context-offload 的
copyReferences已经是很好的本地先例:目标 Session 创建新 reference,但复用同一个 content-addressedblobId。第一阶段可以先把这种“所有权引用与 payload 分离”的模型扩展到copyConversationArtifacts和cloneConversationRuntimeLedger当前复制的大 payload,再逐步改读取路径。生命周期语义建议如下:
删除 Session 时只删除 Session ref,不立即删除共享历史。refcount 可以作为优化,但不应作为最终正确性来源;权威 GC 应从所有 live Session heads、活跃操作和导出使用的临时 roots 开始 mark,遍历对象依赖,在 grace period 后清理不可达对象。
Usage 应保留稳定的物理 model-call identity。单个 Session 可以展示继承用量,但全局真实成本必须按原始 model-call fact 去重,不能因为多个 Session 可达而重复计费。
导出是合理的物化边界:遍历所选 snapshot 的完整依赖闭包,生成可独立导入的 bundle。导出需要独立,并不意味着每次内部 fork 都要提前复制完整历史。
Repair/compaction 必须区分逻辑身份与物理表示。重建索引、checkpoint 或 packed segments 可以保持同一个 snapshot 的逻辑身份;修改逻辑历史则必须发布新 snapshot,不能原地修改已被子会话引用的内容。
读取路径也不应递归遍历无限深的 Session 父链。Snapshot manifest 应直接引用不可变 history segments,并在深度或 segment 数超过阈值时创建 checkpoint manifest 或 flatten 等价的物理表示。这样 fork 保持 O(1),读取成本也有界。
建议原型顺序:
本质上,这是一个构建在 SQLite 上的小型 Git/Nix 式对象数据库。它保留 SQLite 的运维优势,同时把 Session 独立性的定义从“拥有一份物理独占副本”改为“拥有一个稳定不可变 root,并可独立使用、删除和导出”。
M4n5ter commentedon Sep 17, 2026
@likun666661 I approve of your proposal
Astro-Han commentedon Sep 17, 2026
The snapshot-manifest direction looks right. Making the durable dependency an immutable cross-store manifest instead of
parentSessionId + throughSequenceis the important correction, since a single sequence cannot define an atomic boundary across messages, events, ledger, model_call, and artifacts. Mark-sweep over live heads plus temp roots with a grace period, refcount as optimization only, and extending thecopyReferencesownership/payload split before touching readers are all the right calls.Four things to settle before the prototype:
Read-path blast radius. The object store is four tables, but the expensive part is every history reader moving from
WHERE session_id = ?to manifest → segments → mutable tail assembly. The real consumers are transcript load, the model-history projection, the revision-consistent usage reads from #5387, ledger repair, export/import verification, eval readers, and context offload. A prototype that only proves "child appends a local suffix" measures a fake number. Port one real consumer, ideally transcript, to price the assembly cost honestly.Migration. Agreed this is a full migration, no permanent dual path. One second-order decision to record: seal everything at upgrade (blocking startup on large histories) versus background sealing, where unsealed sessions still read the old way during a converging transition window. The latter is not a dual-path design, but the design should admit the window exists. A conditional benefit worth checking: sibling revisions that were materialized copies of the same prefix would dedup automatically during sealing if segments hash canonical logical content rather than raw row bytes, since copied rows may carry remapped ids. Which identity a segment hashes is itself a design decision.
GC roots must be durable and leased. Temporary roots held by active operations cannot live in process memory. The hazard: an export resolves snapshot S and walks its closure while the user deletes the session; the head ref disappears, GC sweeps S's segments, and the export reads freed objects mid-flight. Same class for a fork that resolved
forkSnapshotIdbut has not committed itssession_headsrow when the source session is deleted. The fix is agc_rootstable (snapshotId plus expiry) that survives crashes and whose leases expire so a crashed operation cannot leak roots. Mark starts from live heads union unexpired roots. This is the same hazard class as the copy-attempt window from #5132: the gap between "the operation decided it needs these objects" and "the objects hang off a permanent root" needs a crash-surviving placeholder.Settle-path overhead. A settled turn atomically publishing a snapshot means every turn completion, the hottest path in the system, seals the mutable tail into segments, hashes payloads (re-reading the bytes), writes object/edge/manifest/snapshot rows, and updates
session_heads. Constant-ish work, but on the latency-critical path. The prototype should measure settle cost separately from fork savings.中文版
snapshot manifest 方向是对的。把持久依赖从
parentSessionId + throughSequence改成不可变的跨存储 manifest 是关键修正:单个 sequence 无法给 messages、events、ledger、model_call、artifacts 定义原子边界。mark-sweep + 宽限期、refcount 只作优化、先把copyReferences的引用/payload 分离扩展到大 payload 再动读路径,这几个判断都对。动工前有四件事要定下来:
读路径爆炸半径。 对象存储只有四张表,但真正贵的是每个读历史的地方从
WHERE session_id = ?换成 manifest → segments → mutable tail 的拼装。真实消费者:transcript 加载、model-history 投影、#5387 的 revision-consistent usage 读、ledger 修复、导出/导入校验、eval 读取、context offload。原型只验证"子 Session 能追加 local suffix"量出来的是假数字,应该移植一个真实消费者(建议 transcript)才能定出真实成本。迁移。 同意全量迁移,不留永久双路径。有一个二级决策要记录:升级时全量 seal(大历史库会阻塞启动)还是后台 seal(迁移窗口内未 seal 会话仍走旧读法)。后者不算双路径设计,但设计里要承认这个窗口存在。一个待验证的收益:如果 segment 按规范化逻辑内容哈希而非原始行字节,物化复制的兄弟修订在 seal 时会自动去重;复制行可能带重映射的 id,所以"segment 哈希什么身份"本身就是一个设计决策。
GC 临时 roots 必须 durable 且带租约。 活跃操作持有的临时 root 不能只活在进程内存里。危险场景:导出解析出 snapshot S 开始遍历闭包,此时用户删除会话,head ref 消失,GC 扫掉 S 的 segment,导出读到一半对象没了。同类场景:fork 解析了
forkSnapshotId但还没提交session_heads行时源会话被删。解法是一张gc_roots表(snapshotId + 过期时间),崩溃后仍在,租约过期保证崩溃操作不泄漏 root。mark 从 live heads 并上未过期 roots 出发。这和 #5132 的 copy-attempt 窗口是同族危险:"操作决定需要这批对象"和"对象挂上永久 root"之间的缝隙需要一个能活过崩溃的占位。settle 路径开销。 "settled turn 原子发布 snapshot"意味着每次 turn 完成(全系统最热的路径)都要把 mutable tail 封成 segment、对 payload 算哈希(重读字节)、写 object/edge/manifest/snapshot 行、更新
session_heads。常数级开销但打在延迟敏感点上,原型要单独测 settle 成本,不能只量 fork 省了多少。likun666661 commentedon Sep 17, 2026
After inspecting the current Session execution and read paths more closely, I think the previous proposal over-models the problem by starting from a generic cross-store snapshot database.
Maka already has a much stronger primitive: the Session's semantic history is append-only.
runtime_eventsis the canonical factual log.event_seq.runtime_session_event_ordinalsgives the Session a durable append order across invocations.runtime_continuation_claimsalready binds execution to an immutable prefix using a high-water mark and prefix digest.That suggests a simpler Session branch model:
or, as a logical history view:
The durable relationship could be approximately:
The checkpoint must refer to a settled, immutable Session prefix. It is the child's persistent checkout of the parent's execution scene. The parent may continue appending after the fork, but the child never observes anything beyond the recorded checkpoint.
This also gives the permission model a precise meaning: the child does not receive general read access to the parent's current state. It receives a durable capability to read exactly the inherited immutable prefix. It cannot append to the parent, follow the parent's moving head, or read later events.
The child then writes only its own suffix. Inherited RuntimeEvents keep their original event, Run, invocation, and model-call identities. They appear in the child's logical history without being rewritten into new physical execution facts.
This is where the current conversation-copy implementation creates most of its own complexity.
cloneConversationRuntimeLedgerphysically clones the source Runs and RuntimeEvents, regenerates IDs, and then rewrites tool-operation IDs, provider trace IDs, artifact addresses, transition references, and other dependent identities. Once we stop pretending inherited facts are newly executed facts, most of that rewrite graph disappears.Context scheduling should operate on the resolved logical history rather than on rows physically owned by one Session:
Parent and child can therefore produce different context projections over their respective histories while sharing the same immutable facts before the checkpoint. Compaction remains a projection; it must not rewrite or duplicate the inherited log.
The word "snapshot" is still useful here, but it should mean a logical checkout checkpoint, not an eager physical copy of every store. Non-log state that is genuinely part of the execution scene can be attached to the checkpoint by immutable version reference:
So the Session topology supplies lineage, the prefix checkpoint supplies the stable checkout boundary, and the history resolver maps that topology into the real context to project and schedule.
A more direct prototype would therefore be:
inherited prefix + local suffix.This is closer to persistent branch/checkout semantics over an event log than to implementing general MVCC over all existing SQLite tables. Append-only history makes the switch natural: changing Session means selecting a different logical head and projecting context from that head.
简体中文
进一步检查当前 Session 的执行和读取路径后,我认为之前的方案从“通用跨 store snapshot 数据库”出发,把问题设计得过重了。
Maka 已经有一个更强的基础:Session 的语义历史本身就是 append-only 的。
runtime_events是权威事实日志;event_seq;runtime_session_event_ordinals给出了跨 invocation 的 Session 级追加顺序;runtime_continuation_claims已经通过 high-water 和 prefix digest 绑定不可变历史前缀。因此 Session branch 可以更简单地表达为:
或者:
持久化关系大致可以是:
checkpoint 必须落在 settled、不可变的 Session prefix 上。它就是子 Session 对父 Session 现场的持久 checkout。父 Session 后续可以继续追加,但子 Session 永远看不到 checkpoint 之后的事件。
这里的权限语义也很明确:子 Session 不是获得读取父 Session 当前状态的通用权限,而是获得读取指定不可变 prefix 的持久 capability。它不能向父 Session 写入、不能跟随父 Session 的移动 head,也不能读取之后追加的事件。
子 Session 后续只写自己的 suffix。继承的 RuntimeEvents 保持原始 event、Run、invocation 和 model-call identity,只出现在子 Session 的逻辑历史中,不被重写成一套新的物理执行事实。
当前 conversation-copy 的大部分复杂度正是由物理复制制造出来的。
cloneConversationRuntimeLedger会复制 Runs 和 RuntimeEvents、重新生成 ID,然后重写 tool-operation ID、provider trace ID、artifact 地址和各种内部引用。一旦不再把继承事实伪装成重新执行的事实,这张 identity rewrite 图大部分都可以消失。上下文调度应该基于解析后的逻辑历史,而不是只读取某个 Session 物理拥有的行:
父子 Session 可以基于各自的逻辑历史产生不同的上下文投影,同时共享 checkpoint 之前完全相同的不可变事实。Compaction 仍然只是 projection,不能重写或复制继承日志。
这里仍然可以使用 “snapshot” 这个词,但它应该表示逻辑 checkout checkpoint,而不是立即物理复制所有 store。真正属于执行现场的非日志状态,可以通过不可变版本引用绑定到 checkpoint:
因此,Session topology 定义 lineage,prefix checkpoint 定义稳定 checkout 边界,history resolver 再从这个逻辑关系映射出真正需要投影和调度的上下文。
更直接的原型顺序应该是:
inherited prefix + local suffix;这更像是在 append-only event log 上建立持久 branch/checkout,而不是给现有所有 SQLite 表实现通用 MVCC。Append-only history 让 Session 切换非常自然:切换 Session 本质上就是选择另一个逻辑 head,然后从该 head 投影上下文。
likun666661 commentedon Sep 17, 2026
Putting the two halves together, I think the complete Session-fork model can be substantially simpler than either a generic snapshot database or a new virtual filesystem.
A fork is defined by two immutable coordinates captured at the same execution boundary:
The context side is a checkout over the append-only Runtime Event log:
The workspace side uses the corresponding filesystem snapshot as the base of an isolated writable checkout:
This means a child Session does not own a copied conversation and does not keep following the parent's workspace. It owns a stable pair:
Everything after that boundary is branch-local: new Runtime Events are appended to the child suffix, and file changes are written only to the child's writable checkout.
Fork protocol
The operation can be made recoverable with a small ordering rule:
S.S.S. This can be lazy because it is reconstructible from the durable snapshot.If the process crashes after step 2 but before step 3,
Sis an unreferenced snapshot that can be reclaimed. If it crashes after step 3 but before step 4, the child checkout can simply be materialized again fromS. The durable state never points at something that cannot be reconstructed.The workspace barrier is the only essential consistency requirement for the V0 backend. A recursive clone while compilers, language servers, or commands are still writing could otherwise capture files from different moments.
V0 workspace backend
The first implementation does not need OverlayFS, a Merkle object store, or content-addressed manifests. It only needs this interface:
A practical V0 implementation is a private immutable snapshot directory plus a writable copy-on-write clone:
sealrecursively reflink-clones the current workspace intosnapshots/<id>/root.checkoutreflink-clones that snapshot intocheckouts/<sessionId>/rootand exposes only the checkout to the agent.clonefile/copyfilecloning.FICLONEorcp --reflinkwhere supported.The semantics stay identical; only the cost changes. On reflink-capable filesystems, unchanged file data blocks remain shared and writes copy only the modified blocks. The operation is still O(number of directory entries), which is acceptable for a first prototype and gives us an honest baseline.
workspaceSnapshotIdcan initially be an opaque UUID mapped to the immutable snapshot directory. It does not need to be a content hash. Filesystem-level COW already provides the important storage sharing; content addressing can be introduced later only if cross-snapshot deduplication, remote transport, or verification justifies it.Git should not define the workspace snapshot contract. A real execution scene may contain dirty tracked files, untracked/ignored files, generated outputs, symlinks, and may not be a Git repository at all. A Git tree/worktree backend can still be an optimization for source-only workspaces, but the logical contract should remain a complete filesystem checkout.
If recursive reflink metadata traversal later becomes the bottleneck, the same interface can be backed by OverlayFS, Btrfs/ZFS/APFS snapshots, or another native snapshot mechanism without changing Session semantics.
Resulting architecture
Transcript projection, model-context assembly, compaction, and export operate on the resolved logical history. Tools and commands operate inside the materialized writable workspace. Both are fixed by the same fork boundary.
Deletion also becomes a reference-lifetime question rather than a copy question: removing the parent Session catalog entry must not invalidate either the inherited event prefix or the workspace snapshot while a child checkout still references them. The exact retention mechanism can be designed independently of the fork/read path.
So the prototype does not need to implement general MVCC, a branch-capable database, or a filesystem. It needs two small resolvers over immutable state:
That appears to be the natural end-to-end consequence of Maka already being append-only.
简体中文
把上下文和文件现场两部分合起来后,我认为完整的 Session fork 模型可以比通用 snapshot 数据库或新虚拟文件系统简单得多。
一次 fork 只需要在同一个执行边界固定两个不可变坐标:
上下文侧是在 append-only Runtime Event log 上做 checkout:
文件侧则以对应的文件系统 snapshot 为基础,建立隔离的可写 checkout:
也就是说,子 Session 不拥有一份复制出来的 conversation,也不会继续跟随父 Session 的 workspace。它拥有的是一个稳定的二元组:
边界之后的状态全部归属于各自分支:新的 Runtime Events 只追加到 child suffix,文件修改只写入 child 的 writable checkout。
Fork 协议
通过一个简单的顺序约束,就可以让操作具备崩溃恢复能力:
S。S绑定。S物化 child 的 writable checkout。因为它可以从持久 snapshot 重建,所以允许延迟执行。如果进程在步骤 2 之后、步骤 3 之前崩溃,
S只是一个没有引用的 snapshot,可以后续回收。如果在步骤 3 之后、步骤 4 之前崩溃,则直接从S重新物化 child checkout。持久状态不会指向无法重建的对象。Workspace barrier 是 V0 后端唯一关键的一致性要求。否则在递归 clone 过程中,编译器、语言服务器或命令仍在写文件时,snapshot 可能混入不同时刻的文件状态。
V0 workspace backend
第一版不需要 OverlayFS、Merkle object store 或 content-addressed manifest,只需要下面的接口:
实际实现就是私有的不可变 snapshot 目录,加一个可写的 COW clone:
seal把当前 workspace 递归 reflink clone 到snapshots/<id>/root。checkout再把 snapshot reflink clone 到checkouts/<sessionId>/root,只把后者暴露给 agent。clonefile/copyfilecloning;FICLONE或cp --reflink;三种实现的语义完全相同,只是成本不同。在支持 reflink 的文件系统上,未修改文件的数据块保持共享,只有被写入的数据块发生复制。整个操作仍然是 O(目录项数量),但作为第一版原型是合理的,也能给出真实基线。
workspaceSnapshotId第一版可以只是映射到不可变目录的 opaque UUID,不需要是内容哈希。文件系统 COW 已经解决了最重要的存储共享;只有在跨 snapshot 去重、远程传输或完整性校验确实需要时,再引入 content addressing。Git 不应该定义 workspace snapshot 的语义。真实执行现场可能包含 dirty tracked files、untracked/ignored files、生成物和符号链接,甚至根本不是 Git 仓库。Git tree/worktree backend 仍然可以作为 source-only workspace 的优化,但上层契约应该始终表示完整的 filesystem checkout。
如果未来递归 reflink 的目录元数据遍历成为瓶颈,同一接口可以替换为 OverlayFS、Btrfs/ZFS/APFS snapshot 或其他原生快照机制,而不改变 Session 语义。
最终结构
Transcript projection、model-context assembly、compaction 和 export 都基于解析后的逻辑历史。工具和命令则运行在物化出的 writable workspace 中。两者由同一个 fork boundary 固定。
删除问题也从复制问题变成引用生命周期问题:只要 child checkout 仍在引用,删除父 Session 的 catalog entry 就不能破坏继承的 event prefix 或 workspace snapshot。具体 retention 机制可以独立于 fork/read path 设计。
因此,原型不需要实现通用 MVCC、支持 branch 的数据库或一个新文件系统。它只需要两个基于不可变状态的小型 resolver:
这应该就是 Maka 已经采用 append-only 模型后最自然的端到端结果。
likun666661 commentedon Sep 17, 2026
I think there is an even simpler product-level solution, and this is the approach I would now prototype first.
Instead of preserving a durable parent-prefix dependency, create a completely new Session and physically seed it with a copy of the source Session's resolved context. Then append one bootstrap message asking the new Agent to reconstruct the workspace itself.
The important distinction is that we copy the context projection the model actually consumes, not the entire graph of Runtime Events, Runs, invocations, model-call facts, task-ledger rows, and rewritten execution identities.
Clone flow
Conceptually:
The new Agent already has the inherited conversation, commands, tool results, commits, patches, and reasoning that are still present in the resolved context. It can inspect the actual environment and choose the appropriate recovery operation:
A successful recovery can append an ordinary durable fact such as
WorkspaceCheckoutCompleted. Failure is not a storage-layer consistency problem; it becomes a normal Agent interaction that explains what is missing or conflicting.Resulting semantics
Every cloned Session is self-contained:
originSessionIdis only tree/provenance metadata. It is not consulted by transcript reads, model-history assembly, deletion, compaction, or export. The source Session may continue, compact, or be deleted without changing the child.The workspace is deliberately not part of the Session clone transaction:
This removes the need for:
The tradeoff is intentional: cloning becomes O(resolved context size) and duplicates context storage. In return, each Session is operationally independent and the long-term topology/read-path complexity disappears. We should measure copy latency and bytes, but this is likely the right place to spend resources if Session contexts are reasonably bounded by compaction.
This makes the tree clone a composition of existing Agent capabilities rather than a new storage system:
简体中文
我认为还有一个更简单的产品层方案,而且现在应该优先原型验证这个方案。
不再让新 Session 持久依赖父 Session 的 prefix,而是直接创建一个完全独立的新 Session,把源 Session 已解析完成的上下文物化复制过去,然后在尾部追加一条 bootstrap message,让新 Agent 自己恢复工作现场。
关键区别是:只复制模型真正消费的 context projection,而不是复制整张 Runtime Events、Runs、invocations、model-call facts、task ledger 和执行 identity rewrite 图。
Clone 流程
概念代码:
新 Agent 已经拥有 resolved context 中保留下来的对话、命令、tool result、commit、patch 和推理。它可以检查真实环境,并自行选择合适的恢复方式:
恢复成功后,可以追加一个普通的持久事实,例如
WorkspaceCheckoutCompleted。恢复失败也不再是存储层一致性问题,而是普通 Agent interaction:说明缺少或冲突的状态,并请求用户处理。最终语义
每个 clone 出来的 Session 都是自包含的:
originSessionId只是树形展示和来源 metadata。Transcript 读取、model-history assembly、删除、compaction 和 export 都不依赖它。源 Session 后续继续、压缩或删除,都不会改变 child。Workspace 被有意排除在 Session clone transaction 之外:
因此不再需要:
这是一个有意接受的取舍:clone 成本变为 O(resolved context size),并重复占用 context 存储。作为交换,每个 Session 在运行和生命周期上完全独立,长期的拓扑与读取路径复杂度消失。如果 Session context 已经被 compaction 控制在合理范围内,那么复制 latency 和 bytes 很可能是值得付出的成本。
最终,树形 clone 只是现有 Agent 能力的组合,而不是一套新的存储系统: