ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OpenClaw Gateway 会话 Transcript 写入规范:理解 Pi 的 parentId 链与 SessionManager.appendMessage 保护机制

OpenClaw Gateway 会话 Transcript 写入规范:理解 Pi 的 parentId 链与 SessionManager.appendMessage 保护机制
  • 人工智能
  • 大模型
  • LLM 网关
  • AI Agent
  • 即时通讯

【免费下载链接】openclaw-zero-token

OpenClaw: Use All Major AI Models NO API Token! Claude/ChatGPT/Gemini/DeepSeek/Doubao/Grok/Qwen/Manus/Kimi

项目地址:https://gitcode.com/gh_mirrors/op/openclaw-zero-token
点击查看免费下载

Gateway 网关层在接收并处理来自 WebChat、客户端 RPC 等入口的对话消息时,需要把"注入式"助手消息、中止残留片段等系统生成内容持久化到 Pi 会话 transcript(*.jsonl)文件中。本指南围绕 gateway/server-methods/AGENTS.md 中的核心工程规范展开:为什么 Pi 会话 transcript 是一条parentId链/DAG,为什么绝不能通过裸 JSONL 追加type: "message"条目,以及如何通过SessionManager.appendMessage(...)及其包装器安全写入。读完本文,你将掌握 Gateway 侧 transcript 写入的完整调用链、底层实现细节与测试保障手段。

一、背景:Pi 会话 transcript 的parentId链/DAG 结构

Pi(@mariozechner/pi-coding-agent,OpenClaw 底层采用的编码代理会话引擎)把每个会话持久化为一个 JSONL 文件,例如sess.jsonl。文件首行是一条type: "session"的会话头记录,包含version、id、timestamp、cwd等元信息;其后每一行都是一条消息记录。

从源码结构看,这些消息记录并不是彼此孤立的追加项,而是通过parentId字段串成一条父链——每条新消息必须显式携带其父消息的 ID,从而形成从会话头一路延伸的叶子路径。多个分支组合起来就是一个有向无环图(DAG):例如用户消息作为根,模型回复挂在其下,后续轮次的输入又挂在模型回复之下。Gateway 侧的测试同样验证了这一点:

  • gateway/server-methods/chat.test-helpers.ts 用CURRENT_SESSION_VERSION写入type: "session"头后,再追加消息条目;
  • gateway/server-methods/chat.inject.parentid.test.ts 断言追加的消息记录必须携带parentId、id、message三个字段,并在注释中点明:"The regression we saw: raw jsonl appends omitted this field entirely."(我们遇到过的回归:裸 JSONL 追加会完全漏掉该字段)。

二、为什么禁止用裸 JSONL 追加type: "message"条目

gateway/server-methods/AGENTS.md 给出的规范原文是:

Pi session transcripts are aparentIdchain/DAG; never append Pitype: "message"entries via raw JSONL writes (missingparentIdcan sever the leaf path and break compaction/history). Always write transcript messages viaSessionManager.appendMessage(...)(or a wrapper that uses it).

如果绕过SessionManager直接fs.appendFileSync(transcriptPath, line + "\n"),写入的条目缺少parentId,其直接后果是:

  1. 切断叶子路径:新写入的消息无法挂接到当前会话链的叶子上,后续基于parentId回溯会话的读取逻辑(如恢复上下文、按链遍历历史)将看不到这条记录,或者从它之后找不到正确的父节点;
  2. 破坏 compaction(压缩/摘要):会话接近 token 上限时,Pi 会把历史摘要"压缩"进链中,摘要本身也是链上的一个节点。一旦链被切断,压缩摘要就无法被后续上下文正确引用,历史内容可能从上下文窗口中被隐藏甚至丢失;
  3. 破坏历史完整性:会话恢复、归档(archive)、重放等依赖整条链的操作都会出现断链或丢失记录。

换句话说,裸追加虽然"写进去了",但它写出的是一条游离于链外的孤儿记录,对依赖链结构的全部下游逻辑而言等于没写——甚至比没写更糟,因为它污染了文件。

三、正确写法:SessionManager.appendMessage(...)与它的 Gateway 包装器

规范给出的标准做法是始终经由SessionManager.appendMessage(...)写入。Gateway 为此封装了一个专用助手函数appendInjectedAssistantMessageToTranscript,位于 gateway/server-methods/chat-transcript-inject.ts。

该函数的职责是构造一条完整合规的 Pi assistant 消息,再交给SessionManager追加:

export function appendInjectedAssistantMessageToTranscript(params: { transcriptPath: string; message: string; label?: string; idempotencyKey?: string; abortMeta?: GatewayInjectedAbortMeta; now?: number; }): GatewayInjectedTranscriptAppendResult { const now = params.now ?? Date.now(); const labelPrefix = params.label ? `[${params.label}]\n\n` : ""; // 零 usage 标记:这是注入消息而非模型输出 const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { /* 全 0 */ } }; const messageBody = { role: "assistant", content: [{ type: "text", text: `${labelPrefix}${params.message}` }], timestamp: now, stopReason: "stop", // 兼容 Pi 的严格 stopReason 枚举 usage, api: "openai-responses", // 显式标记,避免下游把它当模型输出 provider: "openclaw", model: "gateway-injected", ...(params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}), ...(params.abortMeta ? { openclawAbort: { aborted: true, origin, runId } } : {}), }; // 关键:通过 SessionManager.open + appendMessage 写入, // 让条目通过 parentId 挂到当前叶子 const sessionManager = SessionManager.open(params.transcriptPath); const messageId = sessionManager.appendMessage(messageBody); emitSessionTranscriptUpdate({ sessionFile: params.transcriptPath, message: messageBody, messageId }); return { ok: true, messageId, message: messageBody }; }

这段实现里有几个值得注意的工程细节:

  • 零usage与stopReason: "stop":注入消息不是真实模型输出,但为了让其正常参与会话链,仍以 assistant 角色、标准 stop 语义写入,同时把 token/cost 全部置零,避免污染用量统计;
  • api/provider/model三字段显式标记:api: "openai-responses"、provider: "openclaw"、model: "gateway-injected"的组合让下游工具链能一眼识别这是 Gateway 注入的合成消息,而不会误判为模型输出(源码注释原文:"Make these explicit so downstream tooling never treats this as model output.");
  • openclawAbort元信息:中止场景(abortMeta)下会携带aborted: true、origin("rpc" | "stop-command")与runId,记录这次中止的来源;
  • emitSessionTranscriptUpdate:写入成功后同步向 transcript 监听器广播更新事件(实现见 sessions/transcript-events.ts),让订阅了onSessionTranscriptUpdate的组件(如会话历史推送)能即时感知新消息。

四、Gateway 侧完整写入流程:appendAssistantTranscriptMessage

在实际的 Gateway RPC 处理中,助手消息的写入经由chat.ts中更上层的appendAssistantTranscriptMessage编排,形成一条完整链路(gateway/server-methods/chat.ts):

  1. resolveTranscriptPath(chat.ts L746-L766):根据sessionId、storePath、sessionFile、agentId解析出 transcript 文件的实际路径,解析失败返回null;
  2. ensureTranscriptFile(chat.ts L768-L792):文件不存在时,若允许createIfMissing,则创建目录并以0o600权限写入type: "session"头(使用CURRENT_SESSION_VERSION);
  3. 幂等去重transcriptHasIdempotencyKey(chat.ts L794-L810):若调用方携带idempotencyKey,先扫描文件判断该 key 是否已存在,存在则直接返回成功,避免网络重试/RPC 重复调用导致同一消息被写两遍;
  4. 调用appendInjectedAssistantMessageToTranscript:即上一节所述的SessionManager.appendMessage写入路径。

这个上层函数被三处核心场景调用,全部遵循"经由 SessionManager"的规范:

场景调用位置说明
chat.abort(多 run 中止)chat.ts L1070中止后被授权取消的 run,把缓冲区内已生成的部分文本(partial)持久化为 assistant 消息,idempotencyKey为${runId}:assistant,附带openclawAbort元信息
chat.abort(单 run RPC 中止)chat.ts L1285中止单个 run 时,同样把chatRunBuffers中的部分文本以abortOrigin: "rpc"写入
chat.send(WebChat 最终回复)chat.ts L1707回复管线完成后,把最终合成回复追加进 transcript;写入失败时降级为仅广播内存中的消息对象并在日志中告警

其中中止残留(partial)的持久化尤其能说明"为什么必须写进链里":用户中止对话时,模型可能已经吐出了半截回复,若直接丢弃,会话历史会显得残缺;但若裸追加,又会破坏链结构。persistAbortedPartials的正确做法是携带openclawAbort标记写入一条完整 assistant 消息,既保留了中止前的产出,又不破坏后续 compaction 与历史完整性。

五、测试与静态护栏:如何保证规范不被回归破坏

这条规范不仅写进了文档,还被固化成两层测试,防止未来有人误用裸 JSONL 追加:

1. 行为级测试(gateway/server-methods/chat.inject.parentid.test.ts):创建真实 transcript fixture,调用appendInjectedAssistantMessageToTranscript,然后逐行解析 JSONL 文件断言最后一条记录确实带有parentId、id、message字段——即验证"写入结果真的挂在了链上",而不是只验证函数没有报错。

2. 源码静态护栏(gateway/server-methods/server-methods.test.ts):直接读取chat.ts与chat-transcript-inject.ts的源码文本,断言:

expect(chatSrc.includes("fs.appendFileSync(transcriptPath")).toBe(false); expect(chatSrc).toContain("appendInjectedAssistantMessageToTranscript("); expect(helperSrc.includes("fs.appendFileSync(params.transcriptPath")).toBe(false); expect(helperSrc).toContain("SessionManager.open(params.transcriptPath)"); expect(helperSrc).toContain("appendMessage(messageBody)");

即:Gateway 的 transcript 写入代码中不允许出现对 transcript 路径的fs.appendFileSync直接调用,且必须经由appendInjectedAssistantMessageToTranscript→SessionManager.open→appendMessage这一固定链路。这种"源码级 guardrail"测试从编译层面拦截了规范回归。

六、配套配置:compaction 行为可调

既然链路被破坏的直接后果是"compaction 摘要从上下文中消失",那么理解 compaction 的配置项有助于运维长会话。OpenClaw 在agents.defaults.compaction下暴露了完整调优参数(schema 见 config/schema.base.generated.ts,默认值逻辑见 config/defaults.ts):

参数类型/取值说明
mode"default"|"safeguard"压缩模式,未显式配置时默认注入safeguard
reserveTokens整数 ≥ 0压缩后为回复生成与工具输出预留的 token 余量
reserveTokensFloor整数 ≥ 0reserveTokens的下限护栏(0 表示禁用护栏),防止 token 估算波动导致过度压缩
keepRecentTokens整数 > 0压缩时保留的最近对话 token 预算
maxHistoryShare0.1–0.9压缩后历史占用总上下文预算的最大比例
identifierPolicy"strict"|"off"|"custom"压缩标识符策略,custom时配合identifierInstructions
recentTurnsPreserve整数 0–12压缩时保留的最近轮数
qualityGuard.enabled/maxRetries布尔 / 整数压缩质量护栏与最大重试次数

相关行为由 config/config.compaction-settings.test.ts 验证,例如reserveTokensFloor生效、mode默认safeguard、qualityGuard.maxRetries的可配置性等。配合"正确写入 parentId 链"的规范,这些配置共同保证长会话在接近 token 上限时仍能稳定延续。

七、实践总结:Gateway 侧写入 transcript 的 Checklist

结合 gateway/server-methods/AGENTS.md 的规范与本文梳理的源码证据,在 Gateway 层(或任何需要向 Pi transcript 注入消息的地方)应遵循以下清单:

  1. 永远不要对 transcript 文件直接fs.appendFileSync追加type: "message"条目——这条被源码 guardrail 测试明文禁止;
  2. 总是通过SessionManager.open(transcriptPath)+appendMessage(messageBody)写入,或复用 Gateway 现成包装器appendInjectedAssistantMessageToTranscript(gateway/server-methods/chat-transcript-inject.ts);
  3. 注入消息要显式声明身份:api/provider/model三字段标记为 Gateway 注入,usage置零,避免被下游误判为模型输出并污染用量统计;
  4. 重试场景务必带idempotencyKey:appendAssistantTranscriptMessage已内置transcriptHasIdempotencyKey去重,重复调用不会写重消息;
  5. 中止场景携带abortMeta:记录origin与runId,让中止残留(partial)成为链上的合法节点;
  6. 写入成功后调用emitSessionTranscriptUpdate:同步通知 transcript 监听器,保证会话历史推送实时一致。

遵守这条"parentId 链优先"的规范,是确保 OpenClaw Gateway 长会话历史完整、compaction 摘要可用、归档与恢复不丢记录的前提——这也是该规范被同时写入AGENTS.md、CLAUDE.md并被三层测试固化的原因。

  • 人工智能
  • 大模型
  • LLM 网关
  • AI Agent
  • 即时通讯

【免费下载链接】openclaw-zero-token

OpenClaw: Use All Major AI Models NO API Token! Claude/ChatGPT/Gemini/DeepSeek/Doubao/Grok/Qwen/Manus/Kimi

项目地址:https://gitcode.com/gh_mirrors/op/openclaw-zero-token
点击查看免费下载

相关推荐

上一篇:TypeSpec 生成 Java 客户端报错 "Name from TCGC is empty." 的诊断与修复
下一篇:Karmada 成员集群退出时的传播资源清理:RemoveStrategy 策略设计提案与 unjoin 清理机制深度解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进