跳到主要内容
路径文档

溢出存储

一句话版ctx.spillStore 是工具输出溢出能力缝:超长工具结果经 spill-policy 判定后,把完整文本落盘,模型侧只留下有界预览 + 定位符spill-local 把溢出文本存成会话作用域的本地文件,定位符是文件路径,检索提示让模型用 read/grep

工具输出有时巨大(网页抓取、命令日志)。把全文塞进模型上下文既贵又没用——spill 把全文存下来,只给模型一个"去哪找"的定位符。

一、三个包的职责

角色ctx key
spill定义溢出存储缝(服务 + 词汇类型)ctx.spillStore
spill-local把溢出文本存进会话作用域的本地文件注册在 ctx.spillStore
spill-policy应用工具执行后的溢出策略监听 ctx.tools

三者按关切拆分、可独立演化/替换:存储缝说做什么(WHAT),本地后端说怎么做(HOW),策略决定何时溢出并组装通知。

二、服务定义(spill)

SpillStore(ctx.spillStore)定义溢出后端做什么——持久化工具的超大文本,返回一个面向模型的定位符 + 检索指引——而不说怎么做

Member语义
saveText(input)原样持久化 input.content;resolve 一个 SpillRef(不透明定位符、精确写入字节数、检索提示)。遇到真实存储失败拒绝(权限、ENOSPC、后端不可用),由调用方决定如何降级

存储按请求的 owner 会话在保存时分组为命名空间;后端选择自己的私有表示,可从调用方的 suggestedName 派生名字,但绝不把它当路径信任。这个缝只拥有存储:无保留策略(归 @deepseek-ai/dsh-output-retention)、无工具结果替换(归 spill-policy)、无检索/搜索 API(后端的 retrievalHint 告诉模型怎么用定位符)。

词汇

  • SaveTextSpill(owner、source、suggestedName、content)是请求;SpillRef(locator、bytes、retrievalHint)是结果
  • SpillLocator 是 branded、渲染给模型的不透明字符串——对 spill-local 是本地路径,未来后端可能返回 URI、键或命令 token,而不改 policy/tool 消费者
  • SpillOwner.sessionId 是保存时的存储命名空间:fork 的会话从种子日志继承已有定位符,不复制也不重新归属;fork 后的新溢出用子会话 id
  • SpillSource 记录产生它的 toolNamecallIdlabel,用于后端命名与检视,不是访问控制

三、本地后端(spill-local)

SpillStore本地文件系统实现。注册为 ctx.spillStore,把工具的超大文本持久化到私有、会话作用域的文件;定位符是文件路径,检索提示让模型在该路径上用 readgrep

存储布局:文件落在 <root>/session-<hash>/<random>-<safeName>

含义
root配置的 root(解析为绝对路径);省略时在 OS 临时目录下懒创建私有(0700) 的每进程目录。可预测、可读的 root 会让本机其他用户读到溢出输出或种下符号链接
session-<hash>sha256(sessionId) 的短前缀,让同一会话的溢出文件聚在一起,未来可按会话清理
<random>-<safeName>不可预测的十六进制前缀(防共享 root 下的符号链接种植)+ 调用方 suggestedName 净化成单个安全路径段(防路径穿越;镜像 JSONL 持久化后端的 encodeSegment)。写是独占 + 仅属主(open(path, 'wx', 0o600)):任何已存在路径——无论是否符号链接——都会失败,种下的目标无法重定向它

配置

Key默认含义
root私有 0700 临时目录溢出文件的根目录;设为已知位置可保留它们

saveText 在真实存储失败时拒绝(权限、ENOSPC);溢出策略把拒绝视作 best-effort,保留内联结果。

四、溢出策略(spill-policy)

工具结果溢出策略:一个 tools/post-execute 变换器,把超大的明文工具结果挡在模型上下文之外。当最终结果超过 maxInlineBytes,它经 ctx.spillStore 保存完整文本,把模型侧结果换成有界头尾预览 + 后端定位符 + 检索提示

此插件不注册服务,也不拥有存储或预览机制:预览是 @deepseek-ai/dsh-output-retention(TextRetainer),存储是 ctx.spillStore。它只决定何时溢出并组装通知。

配置

Key默认含义
maxInlineBytes(省略)明文结果的模型侧上下文上限,UTF-8 字节(非负整数,加载时校验)。省略则彻底禁用该策略(插件什么都不注册)。设置后,更大的结果被溢出并替换为按同一预算派生的预览(头/尾切分)

行为

  1. 让工具跑完(经 next() 委托,所以它约束下游 hook 接受的任何东西)

  2. 跳过:嵌套执行(exec.parent 存在,其持久副本由下面的 dispatch-log 臂约束)、被接受的值替换(注册表必须重新校验并重渲染它们)、read(避免 read → 溢出 → 再 read 循环)、任何非 accept 决策(block 的纠正反馈透传)

  3. 仅在内容是纯文本(全是 text 块)时扁平化;含任何非文本块的结果原样不动

  4. UTF-8 大小 ≤ maxInlineBytes → 不变

  5. 否则保存全文,把结果替换为预览 + 通知,尺寸让整个替换(预览 + 空行 + 通知)保持在 maxInlineBytes 内——通知的字节成本从预算里预留,所以预览收缩以适应,模型侧结果永不超上限

    <保留的头/尾预览>

    (Omitted N bytes. Full formatted result stored at: /…/session-…/…-web_fetch.txt. Use read with offset/limit, or grep this path to search within it.)

    通知单独就占满预算(上限极小或定位符很长)时,预览为空、只返回通知。若连这个"仅通知"的替换都会超过 maxInlineBytes,策略就保留内联结果——它绝不发出超上限的替换(而合规替换总比原文小,所以溢出也绝不增加字节)。

Best-effort:没有会话 owner、没有 ctx.spillStore 后端、或 saveText 拒绝 ⇒ 策略记警告并返回原结果。溢出失败绝不把一次成功调用变成 isError 或隐藏内联结果。成功的替换只改 content,规范的程序值被保留。

dispatch-log 臂:第二个监听 tools/code-dispatch-log 的监听器,把同一上限、替换管线、best-effort 兜底应用到每个 run_code 子调用结果的持久副本(artifact 标签 dispatch,按子调用 id 键控)。程序值不动(它已整体越过 worker 边界),read 子调用也受限:日志副本不是模型上下文,read → 溢出 → 再 read 循环不会发生,而 read 恰恰是产出巨大日志的工具。

范围:策略只看到最终的格式化模型侧结果——不是工具的内部资源或规范值。若 provider 已截断(如 web-fetch-http.maxBodyChars),溢出产物保存的是工具返回的完整格式化结果,不是完整原始源。provider/资源上限保持强制且独立。glob/grep 自己拥有条目级呈现溢出(其完整获取值在渲染前仍存在);bash 流自己拥有采集时溢出。通用策略把它的 waterfall 监听器前置再委托,所以普通工具自有的异步投影先完成,再做通用字节约束,与插件加载顺序无关。

五、挂载状态

默认挂载默认配置
spill-localbase 默认挂载无(root 省略 → 私有 0700 临时目录)
spill-policybase 默认挂载maxInlineBytes: 50000

六、对模型的可见性

超长明文结果≤ maxInlineBytes 的结果、嵌套结果、read 结果、被 block 的决策、含非文本块的结果都不变。超长明文模型侧结果变成一个有界头尾预览 + (Omitted <bytes> bytes. Full formatted result stored at: <locator>. <retrievalHint>);存储或属主失败则保留原结果可见。

  • Token 效应:成功的替换至多 maxInlineBytes UTF-8 字节,并在历史里保留到压缩;完整溢出文本不重发给模型
  • KV cache 效应:append-only;新可见内容跟在可复用请求前缀之后,不失效既有 KV cache 条目

七、已知限制

  • 只有最终明文结果可溢出——混合内容结果、被 block 的反馈、read 透传;早先发生的 provider 截断或工具自有保留无法在这里恢复
  • 放不下的通知为该调用禁用替换——极小上限或长定位符会让超大原文保持内联,尽管后端已存了一份未引用的溢出
  • 本地溢出文件在外部清理前一直存在——后端没有会话生命周期删除或按龄保留策略(因为持久化、恢复、fork 的会话可能仍引用路径)
  • 定位符要求同居文件系统的消费者——远程或虚拟部署需要另一个 SpillStore 后端,其定位符与检索提示在那里才有意义

验证

# spill-local / spill-policy 已默认挂载(base)
dsh web --dump-config | grep -iE "spill"
# 会话里看被溢出的工具结果(模型侧是预览 + (Omitted ...) 通知)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"Omitted ' | tail

下一步