DSHarness 系统拆解 固定基线 47f943859b · 36 已复核 / 0 撰写中 / 36 章
English
会话与持久化·第 10 章

日志、派生历史、重放与 Fork

从原始 chunk 到可重建模型输入和 UI

已复核上游 47f943859b范围: 追踪 append、deriveMessages、raw chunk、fork boundary、resume、transcript 与 replay fidelity。

结论:这里没有一种“Replay”,而是六套彼此不能替代的重建契约

DeepSeek Harness 把原始流、语义消息、模型 Surface、持久化物理前缀、浏览器窗口和 Fork lineage 全部建立在同一条 seq 编号日志上,但它没有把它们混成一个万能 replay API。一次模型请求可以从日志重建;一次失败尝试可以被审计;一次崩溃可以被确定性闭合;一次断线可以由 history 与 live stream 收敛;一个 Fork 则是稳定前缀的复制,而不是指向父会话的动态分支。

这个设计最重要的品质不是“保存了聊天记录”,而是让不同消费者声明自己需要哪一种真相。代价也正来自这里:逻辑历史可能比物理落盘日志更完整,UI 可能刻意隐藏失败 attempt,而 Fork 已发布也不等于 parent/child 已原子落盘。

1. 六种经常被混称为 Replay 的操作

契约输入输出不保证什么
Model Surface replay完整事件日志当前 deriveMessages()不保留已被 replacement 遮蔽的旧上下文
Request reconstruction请求 dispatch 前的日志前缀messages + 最新完整 request header不重演网络延迟、异常 throw 或挂起
Stream replayassistant/chunk流式 block、usage、finish、局部 UI失败 retry 的局部内容不一定继续展示
Crash recovery有效但未闭合的持久前缀确定性的 synthetic result / step end / turn end不能猜出已启动副作用的真实外部结果
Client convergenceHistory raw window + subscribed baseline + live frames连续的浏览器事件窗口和 UI projection不是精确 stream offset resume
Forkcompleted-turn 稳定前缀拥有独立 Session ID 的冻结副本父会话后续事件不会流入 child
阅读规则

“能从日志 replay”必须带上宾语。能重建下一次 Provider request,不等于能重演一次外部工具副作用;能恢复 UI,不等于 UI 展示了完整 raw attempt;能创建 child,不等于两个日志具有跨 ID 的原子 durability。

2. 每个模型 attempt 先保存 raw stream,成功后才提交语义消息

1

取当前模型视图

每次 attempt 都重新调用 session.deriveMessages(),并结合该 Step 已固定的 system、tools 和 request config。

2

记录每个 chunk

Provider 迭代器吐出的每一块先追加为 assistant/chunk,同时把 seq 收进本 attempt 的 chunkSeqs

3

规范组装

BlockAssembler 折叠交错 block、delta、usage、finish 和 adapter-private replay state。

4

决定 attempt 命运

Error/aborted finish 进入 request-error waterfall;选择 retry 时直接回到循环,不追加 assistant/message

5

提交成功锚点

成功时追加 append-origin assistant/message,其 sourceEventSeqs 精确引用本次 attempt 的全部 chunk。

生产控制流

Raw observation 先于 semantic commit;失败 attempt 因而留下 chunk 证据,却不会污染模型 Surface。成功消息携带 provider、model、可选 replay state 和 usage,再决定是否执行 tool calls。LOG-ATTEMPT-ASSEMBLY

测试源码合同

一个生成 abc 的 fixture 要求七个协议 chunk 全部按原顺序保留并可重新组装。依赖未安装,本研究核对了测试源码但没有执行。LOG-RAW-CHUNK-TEST

3. Retry 同时维护“审计真相”和“用户当前看到的真相”

失败 attempt 后Raw logModel Surface浏览器 assistant node
已收到的 chunks全部保留,包括 terminal failure chunk不进入,因为没有 final assistant/messagellm/retry 到来时清空并隐藏局部 blocks
首 token 时间事件时间仍在无关跨 retry 保留,反映用户实际已等待并见过一次响应
成功 attempt追加自己的 chunks 与 final message只投影最终语义消息final message 取代局部流状态并 settle
客户端定义

Assistant Definition 以 Turn/Step 为 identity 折叠 chunks;retry reset 不删除日志,只重置展示状态。若 Step 或 Turn 已闭合、存在可见 chunk 证据却没有 final message,它会生成一个 view-only 的 interrupted node。LOG-CLIENT-ASSISTANT

设计评价

用户不会在一次自动 retry 后同时看到两个互相冲突的答案,审计者仍能检查失败 attempt。这是同一事实源上的两种有意 projection,而不是 UI 丢日志。

4. “每次请求可重建”由 Surface 与完整 Header 两部分共同成立

Request at dispatch boundary
= fresh Session(log prefix).deriveMessages()
+ foldRequestHeader(log prefix)
   ├─ provider / model / reasoning effort
   ├─ system prompt
   ├─ ordered tool schemas
   └─ temperature / maxTokens / stop
定理式测试

测试先运行多 Step、工具调用和 prompt/config 变化,再对每个 dispatch 找到首个 response chunk。它用首 chunk 之前的事件创建全新 Session,要求 messages 与真实 request deep-equal;再 fold 最新 request/header,逐字段比较 model、reasoning、system、tools、temperature、maxTokens 和 stop。LOG-REQUEST-RECONSTRUCTION

这里的精确性来自两种互补日志:Message history 由 Surface fold 决定,非消息 request 参数由完整 header snapshot 的 last-wins fold 决定。Raw chunks 证明 Provider 返回了什么,却不参与自己所属请求的输入重建。

5. 空 completion 也可以是耐久的成功锚点

当 Provider 正常停止却没有可见 content,或 max-token 截断使不安全的半截 tool call 被 assembler 丢弃时,Agent 仍追加 content 为空的 assistant/message,并引用这次 attempt 的 chunks。Chapter 09 已说明空 assistant 不进入 deriveMessages();在这里它的作用是记录“请求成功结束过”以及 usage/replay boundary,而不是给模型加入空轮次。

为什么需要它

如果只保留 raw terminal chunk,通用消费者必须理解每个 adapter 的流协议才能判断成功边界。一个统一的语义锚点让 request replay、usage 和 turn completion 不依赖 Provider 方言,同时保持下一请求的 Message 列表干净。LOG-ATTEMPT-ASSEMBLYSESSION-SURFACE-PROJECTION

6. 持久化提供四种不同的读面

APICold source是否提交 recovery是否发布 live Session主要用途
prepare(id)读取、修复、校验,并预留唯一 Session object由调用者随后 publish/rollbackresume 的 ownership transaction
load(id)返回平衡 logical inspection需要 durable recovered view 的读者
inspect(id)返回平衡 logical inspection冷 history、query、fork source
readFrom(id, seq)直接读已存物理 suffix否,也不合成 closers增量存储/复制与 raw 检查
生产语义

四条路径共享 per-session serialization 和格式校验,但刻意选择不同的恢复与 ownership 语义。readFrom 在支持 seek 的 backend 只读 suffix,否则读取完整前缀再 slice;它不借用 prepared cache。LOG-PERSISTENCE-READ-FACES

对 live Session,inspect 可返回仍在 open turn 的内存快照;load 则先 flush,并拒绝把 open live turn 包装成“完整 durable inspection”。同一个函数名不能只看“是否读到了 events”,还要看它承诺的 logical balance 与 ownership。

7. Crash repair 不伪造成功,只确定性地封闭未知状态

崩溃尾部事实合成结果给后续模型的指引
Assistant 请求了 tool,但还没有 tool/callTOOL_NOT_STARTED error result,无 source seq如果仍需要,可以重试
已有 tool/call,但没有 tool/resultTOOL_OUTCOME_UNKNOWN,引用 call event seq先查外部状态或询问;仅只读/幂等操作可安全重试
Step 仍开着step/end恢复结构不变量,不声称完成
Turn 仍开着turn/end {kind:'interrupted'}下一轮看到一次明确中断
确定性算法

扫描器每遇 Turn boundary 重置 pending calls,只修复最后一个 open turn;synthetic seq 从最后真实 seq 连续递增,time 沿用最后真实事件,Map insertion order 保持调用顺序。Balanced 或空日志不产生任何事件。LOG-REPAIR-CLOSERS

Torn write 合同

Backend 可以返回“有效提交前缀 + torn marker”。恢复先截掉不完整物理残片,再保留所有已提交的 open-turn 事实并追加 closers,而不是回滚整个 Turn。LOG-TORN-TAIL-CONTRACT

8. Cold history 可以逻辑完整,而磁盘此刻仍物理未闭合

两个都是真的

inspect() 的 inspection 使用 storedEvents + synthetic closers,但不调用 commitRepairload()/prepare() 才在 revision 仍匹配时提交恢复。契约测试明确要求 inspect 前后 backend revision 不变,而 load 后 revision 变化。LOG-REPAIR-CONTRACTLOG-PREPARED-COMMIT

physical artifact:  turn/start → tool/call ───────────────┐
                                                         │ inspect only
logical history:   turn/start → tool/call → unknown result → step/end → interrupted turn/end

load / prepare:    compare revision → commit repair → reload exact committed graph

因此冷浏览器 History 可以安全显示一次中断闭合,而同一时刻 readFrom 或 raw artifact 仍展示 open physical tail。前者回答“按恢复规则,这段会话逻辑上是什么”;后者回答“后端确实存了哪些字节/事件”。把这两者强行统一,反而会丢失诊断能力。

9. Write-behind 把热路径提交与磁盘 durability 分开

队列合同

Persistence listener 克隆每个 committed event 进入 per-Session write-behind。固定 deadline 形成稳定 batch;写失败时 batch 以原顺序放回队首并暂停自动重试,避免后台紧密重试。显式 flush() 取消 timer、加入共享 barrier,并重试/排空直到 active write 与 pending queue 都静止。LOG-WRITE-BEHIND

语义 checkpoints

标准 policy 在模型 stream 真正构造前、顶层 tool body 前以及每个 pre-step 前 flush。失败则不 dispatch adapter 或副作用体;嵌套 tool 调用复用已经耐久的外层 call。LOG-CHECKPOINT-POLICY

边界此前必须耐久的事实防止的歧义
Provider request 前用户输入、request header、step start 与动态上下文模型已被调用,磁盘却无法重建输入
顶层 tool body 前assistant tool call 与 tool/call副作用发生,但日志连调用开始都没有
下一 Step 前上一 Step 的响应与 tool results下一请求依赖尚未耐久的历史

10. Core Fork 是 live source 的 inclusive prefix copy

Core primitive

SessionStore.fork 只接受当前 store 中的精确 live instance。Boundary 省略时取最后事件;显式值必须是存在的连续 seq,inclusive slice 不能结束在 open Turn 中。Child header 继承 cwd,并记录 parentSession 与复制前缀的精确 seedLengthLOG-CORE-FORK

  • 空 source 可以产生带 lineage 的空 child。
  • Source 后面已有 open Turn 时,仍可选择它之前的旧稳定 boundary。
  • Turn 结束后的 title、plugin marker 等 log-only event 可以包含在 prefix 中。
  • 稳定性只以 Turn bracket 为硬边界;跨 prefix 的 plugin bracket 可随 seed 继承,并由新的 end-seed marker 划开 ownership。
  • Seed 被 detach、snapshot 和 freeze;父子不共享可变 event array。
测试源码矩阵

测试覆盖空 fork、冻结 seed、父日志 open tail 前的早期 cut、各种 turn-end reason、trailing log-only 事件,以及不存在/陈旧 instance/非法 boundary/open turn/重复 child ID 的拒绝。LOG-FORK-CONTRACT

11. Host Fork 把消息锚点提升为“包含整轮”的产品语义

输入情况选择的 completed boundary
atSeq 位于已完成 Turn 内第一个 turn/end.seq ≥ atSeq,所以消息按钮不会截掉同轮后半段
atSeq 位于当前 open Turn拒绝,不会悄悄退回更早 Turn
atSeq 省略或超过日志末尾最后一个 completed Turn
Boundary 后有独立事件继续纳入,直到下一个 turn/start 之前

Host 可以从 attached live state 或 cold inspect 读 source,不需要先 resume Agent。它复制 prefix,继承 cwd、lineage、seedLength 和 source 当前 composition,创建新 Agent,再尝试继承 Workspace。若 Workspace attach 失败,响应明确返回已创建 child ID;child 不会因为外围归组失败而被隐式撤销。LOG-HOST-FORKLOG-HISTORY-CONTRACT

语义

这是 snapshot copy,不是 Git 式共享对象数据库,也不是 event branch pointer。父子各自拥有从 seq 0 开始的完整前缀,后续独立 append;lineage 只由 Header 关联。

12. Fork publication 与跨 Session durability 之间存在明确 crash window

源码级高置信推断,未做故障注入

Host 读取 live parent 时不先 flush,创建 child 后 session/created listener 以异步 initFor() 开始持久化 seed;parent 和 child 的写入只在各自 session ID 链上串行,没有一个跨 ID transaction。因而 API 返回“forked”证明 child 已在 live registry 发布,却不证明 parent prefix 与 child seed 已原子共同耐久。LOG-PERSISTENCE-ADMISSIONLOG-HOST-FORK

这并不会破坏正常进程中的 child 内容:child 已复制完整内存前缀。风险只出现在恰当时刻进程崩溃或 backend 写失败时,可能出现 child 尚未物化,或 child 已写入而 parent 的同一尾部仍在 write-behind 的非对称状态。当前设计的权威 durability primitive 是显式 flush,而 Fork API 没有在返回前对父子做双 barrier。

13. History 分页保留连续 raw range,而不是返回 Message DTO

分页算法

Host 从窗口尾部向前数 append-origin user/assistant messages。Model-only replacement 不占 maxMessages;assistant/message 的 sourceEventSeqs 把 cut 推到该 attempt 的最早 chunk,所以一页不会从流中间开始。返回值仍是所有类型的连续 raw event range。LOG-HISTORY-PAGINATION

Tail page 自然包含尚无 final message 的 partial chunks;较早页面只按 beforeSeq 截断。每个 entry 可携带一个 Host 当前根据 presenter registry 计算的 tool view,但 view 从不持久化,也不属于 event envelope。调用参数若在更早页,当前页 backscan 找不到时会降级成 generic presentation;事实本身仍完整。LOG-HISTORY-VIEW

同一时刻的响应

所有异步 source/composition 准备结束后,Host 同步复制 events 并读取 projection watermark,避免一个 history 响应把 N 时刻的事件与 N+1 时刻的 baseline 拼在一起。Cold source 使用 logical inspect,且整个读取不会发布 Agent。LOG-HISTORY-CUTLOG-HISTORY-CONTRACT

14. 浏览器通过“拉尾页 + 缓存 live + seq 补洞”收敛

subscribe starts ────────────────┐
                               ├─ liveBuffer
history tail request ──────────┘
            ↓ install raw window
drop seq overlap → append contiguous live tail
            ↓
if next.seq > tail.seq + 1: buffer + refetch tail + restitch
Session client

Open 时先拉 History;若 session/subscribed.lastSeq 已超过 page tail,立刻再拉一次。安装窗口后按 seq 拼接期间收到的 frames:重叠丢弃,hole 不直接 append,而是缓存并触发 tail repair。LOG-CLIENT-STITCH

UI 增量 engine

ConversationNodeAssembler 在完整替换时重建 Context,在 live append 时只匹配一个 tail event,在 prepend 较旧页面时保留已有 Context/View identity 并局部 replay dependency。具体业务 Definition 决定 assistant、tool、turn 等如何成为可见节点。LOG-CONVERSATION-ASSEMBLER

因此重连不是“从 server offset 继续推”;它是用 durable/raw History 建基线,再用 seq 对 live frames 去重和补洞。Seq 同时承担 ordering、dedup、gap detection 与 UI correlation。

15. Query trace 和 Raw export 服务不同的审计问题

能力做什么证据边界
Event recordsCanonical fold 后把每个事件标为 current / shadowed / log-only状态是本次完整日志 observation 的结果
Event trace给出 direct replacement chain、被替换节点、直接 source 与后来直接引用者除 replacement chain 外不自动计算完整 transitive provenance
Session trace沿 Header parent 建祖先与递归后代树缺少 parent 时显式 complete:false,不伪造 root
Raw ZIP exportFlush live logs 后逐字节导出 backend artifact、可选后代与引用媒体不把 UI view 或当前 Surface 重新写进 artifact

Query 是对逻辑 event graph 的可解释计算;Export 是保全物理证据。前者帮助回答“这个节点为何被遮蔽、从哪里来”,后者帮助独立工具重新检查原始文件。LOG-QUERY-TRACELOG-RAW-EXPORT

16. 已确认的边界、缺口与规模风险

Gap 1:History 请求没有上限

maxMessages 的 wire schema 只要求正整数,没有 maximum;分页还会先复制/过滤整个 event log,而一个 message group 自身可以包含大量 raw chunks。因而调用者可请求很大的页,单个超大 attempt 也可能让响应远超“消息数”的直觉。这是源码可见的资源边界;实际可利用程度还取决于上游日志/传输限制。LOG-HISTORY-SCHEMALOG-HISTORY-PAGINATION

Gap 2:一次 gap repair 失败后没有定时主动重试

客户端 catch 后只记录错误并清除 stitching;已缓存 live events 留在 buffer。如果此后没有新 frame 再次触发 gap、也没有 reconnect/resync,它们可以长期不可见。下一次相关事件或重连会再收敛,但当前实现没有 backoff timer。LOG-CLIENT-STITCH

Gap 3:Fork 没有跨 ID durability transaction

这是上一节的 crash-window 推断。若产品文案把响应解释为“已永久创建”,需要额外 parent/child flush 或 backend transaction;若只承诺 live child admission,当前语义应在 API 文档中说清楚。

  • 有意取舍:retry 失败内容在 UI 隐藏、在 raw log 保留。
  • 有意取舍:tool render view 是当前代码的即时 projection,跨页缺少 call args 时允许 generic fallback。
  • 有意取舍:logical inspect 可提前给出 deterministic recovery,不强迫只读页面修改磁盘。
  • 未知:本研究没有执行故障注入、超大历史响应或真实断线实验;不能把源码推断写成已复现事故。

17. 设计评价与验证状态

选择收益成本
Raw attempt 与 semantic message 双层记录失败可审计,模型上下文不受失败输出污染消费者必须理解哪层才是当前 truth
Repair 只表达 unknown/interrupted不会把副作用猜成成功或失败恢复后的模型仍需查询外部世界
Logical inspection 与 physical read 分开冷读无副作用,同时可提供平衡 UI“历史里看见”不总等于“磁盘已提交”
Fork 复制完整 prefixChild 自包含、可独立恢复和导出空间重复,跨 ID 原子性需另行实现
History convergence 而非 stream offset resume断线、重叠和漏帧都能回到 durable truth需要重复拉页,失败重试策略仍不完整
本章验证范围

本章交叉核对了 Agent loop、repair、SessionStore Fork、Host History/Fork、persistence coordinator/write-behind、client Session/Conversation assembler、query 与 export 的生产控制流,并阅读了 request reconstruction、fork、repair、torn-tail 和 persistence contract 测试源码。Upstream 没有安装依赖,所以没有声称这些测试在本机通过;下一章继续拆解后端、数据库 schema 与数据所有权。

我的学习体会

内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。