MusePi

Blob 与 artifact 存储架构

English 中文

本文档描述 coding-agent 如何将大体积/二进制载荷存储在 session JSONL 之外、截断后的工具输出如何持久化,以及内部 URL(artifact://agent://)如何解析回已存储数据。

为什么存在两套存储系统

运行时针对不同数据形状使用两套不同的持久化机制:

两者被刻意分开:

存储边界与磁盘布局

Blob 存储边界(全局)

SessionManager 构造 BlobStore(getBlobsDir()),因此 blob 文件存放在共享全局 blob 目录,而不是 session 文件夹内。

Blob 文件命名:

影响:

Artifact 边界(session-local)

ArtifactManager 从 session 文件路径派生 artifact 目录:

Artifact 类型共享此目录:

子代理可以复用父级 ArtifactManager;这种情况下父级与子代理树共享一个 artifact 目录和数字 artifact ID 空间。

ID 与名称分配方案

Blob ID:内容哈希

BlobStore.put() / putSync() 对其接收到的字节计算 SHA-256,并返回:

不使用 session-local 计数器。

Artifact ID:session-local 单调整数

ArtifactManager 在首次目录支持的分配时扫描现有 *.log artifact 文件,找到最大现有数字 ID 并设置 nextId = max + 1

分配行为:

如果 artifact 目录缺失,扫描返回空列表,分配从 0 开始。

没有 adopted manager 的 non-persistent session 可以将 saveArtifact(...) 内容以数字 ID 形式存储在内存中,但 artifact:// 解析是通过注册的 artifact 目录文件化支持的。

Agent 输出 ID(agent://

AgentOutputManager 按请求的名称分配子代理输出 ID,首次使用时原样使用,仅在同一名称重复时才加后缀(-2-3、…)(例如 AnnaAnna-2)。嵌套输出按父级前缀分组(例如 Parent.Child)。它在初始化时扫描现有 .md 文件,因此 resumed session 永远不会重用会覆盖先前输出的名称。

持久化数据流

1)Session 条目持久化重写路径

在 session 条目被写入之前——增量追加(#appendToSessionFile)或全文件重写(#rewriteSynchronously / #rewriteAtomically)——SessionManager 通过 #lineFor() 将其序列化,该函数在截断管道上运行 prepareEntryForPersistence()

关键行为:

  1. 大字符串截断:超大字符串被截断并附加 "[Session persistence truncated large content]";签名字段(thinkingSignaturethoughtSignaturetextSignature)被清空而不是截断。
  2. 瞬态字段剥离partialJsonjsonlEvents 从持久化条目中移除。
  3. 图片外部化到 blob
    • content 数组中的图片块在 data 还不是 blob 引用且 base64 长度至少达到阈值(BLOB_EXTERNALIZE_THRESHOLD = 1024)时被外部化,
    • provider 风格的 image_url data URL 在以 data:image/ 开头且包含 ;base64, 时被外部化,
    • 图片块 data 以解码后的二进制字节存储,
    • provider data URL 以原始 UTF-8 data URL 字符串存储,
    • 持久化值被替换为 blob:sha256:<hash>

这使 session JSONL 保持紧凑,同时保留可恢复性。

2)Session 加载回填路径

打开 session 时(setSessionFile),在迁移之后,SessionManager 运行 resolveBlobRefsInEntries()

对于带有 blob:sha256:<hash> 的 message/custom-message 图片块,以及带有 blob 引用的持久化 provider image_url 字段:

如果 blob 缺失:

3)工具输出溢出/截断路径

OutputSink 为 bash/python/ssh 及相关执行器提供流式输出。

行为:

  1. 每个数据块通过 sanitizeWithOptionalSixelPassthrough(..., sanitizeText) 清理并追加到内存记账中。
  2. 可选的实时 onChunk 接收清理后的列上限前数据块,如果配置了则做节流。
  3. 每行列上限可能会丢弃面向 LLM 的缓冲区中长行的字节;此时会启动 artifact 镜像,使磁盘文件保留完整的清理后流。
  4. 当内存尾缓冲区超过溢出阈值(DEFAULT_MAX_BYTES,50KB)时,sink 标记输出已截断,并在 artifact 路径可用时启动 artifact 镜像。
  5. 如果文件 sink 已打开,它会先写入当前缓冲区,然后写入所有排队/后续的清理后数据块。
  6. 内存缓冲区被修剪为尾窗口,或者在配置了头保留时修剪为头 + 省略标记 + 尾。
  7. dump() 仅在文件 sink 创建成功时返回包含 artifactId 的摘要。

实际效果:

如果文件 sink 创建失败(I/O 错误、路径缺失等),sink 回退到仅内存截断;完整输出不会被持久化。

URL 访问模型

blob: 引用

blob:sha256:<hash> 是 session 条目载荷内的持久化引用,不是由 router 处理的内部 URL scheme。解析在 session 加载期间由 SessionManager 完成。

artifact://<id>

ArtifactProtocolHandler 通过注册的活动 session artifact 目录处理:

失败行为:

agent://<id>

AgentProtocolHandler 通过注册的活动 session artifact 目录和 <artifactsDir>/<id>.md 处理:

失败行为:

Read 工具集成:

Resume、fork 与移动语义

Resume

Fork

SessionManager.fork() 创建具有新 session ID 和 parentSession 链接的新 session 文件,然后返回旧/新文件路径。Artifact 复制由 AgentSession.fork() 处理:

fork 后的 ID 影响:

fork 后的 blob 影响:

移动到新 cwd

SessionManager.moveTo() 将 session 文件和 artifact 目录重命名为新的默认 session 目录,如果后续步骤失败则回滚。这保留了 artifact 身份,同时 relocated session scope。

失败处理与回退路径

情况 行为
图片块回填时 blob 文件缺失 警告并在内存中保留 blob:sha256: 引用字符串
provider image_url 回填时 blob 文件缺失 警告并在内存中保留 blob:sha256: 引用字符串
通过 BlobStore.get 读取 blob 时 ENOENT 返回 null
Artifact 目录缺失(ArtifactManager.listFiles 返回空列表(可以从头开始分配)
没有注册 artifact 目录(artifact:// 抛出 No session - artifacts unavailable
没有注册 artifact 目录(agent:// 抛出 No session - agent outputs unavailable
注册的 artifact 目录在磁盘上缺失 抛出明确的 No artifacts directory found
Artifact ID 未找到 附带可用 ID 列表抛出
OutputSink artifact 写入器初始化失败 继续使用仅内存的有界输出
Non-persistent saveArtifact 将文本存储在 SessionManager 内存映射中;非文件化 URL 数据

二进制 blob 外部化与文本输出 artifact 的对比

这两套系统仅有间接交集:两者都减少 session JSONL 膨胀,但它们的身份、生命周期和检索路径不同。

实现文件