
DeepSeek Harness 会话日志不可变性与开发模式不变式运行时所有权边界的源码级解析【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读DeepSeek Harnessdsh将一切皆插件的架构落地为一个事件溯源式的会话日志系统会话日志是回放、请求重建、持久化与用户可见历史的持久真源。本文基于仓库内的架构决策笔记 2026-06-11-dev-invariants-over-deep-readonly.md深入讲解 dsh 如何用始终开启的存储所有权边界 可选的包级关系不变式取代纯 TypeScript readonly 类型实现历史数据的运行时不可变保护。读完本文你将掌握Session.append()的无损 JSON 快照与深度冻结机制、deriveMessages()的分离式投影缓存、dsh-invariants注册服务的配置与选择算法以及四个包级配套插件session / agent / scope / agent-loop各自守护的关系规则。为什么 readonly 类型救不了会话日志两个需要不同手段解决的问题会话日志需要两种性质不同的保护决策笔记 2026-06-11-dev-invariants-over-deep-readonly.md 对此做了严格区分不可变所有权每条已存储事实都必须是会话拥有的外部代码可以检视历史但绝不能保留一个以后能改写历史的引用调用方传入的输入也绝不能继续连着调用方的可变对象。跨记录关系检查一份日志可以每个记录都完全不可变但序列号乱序、轮次/步骤嵌套错误、工具调用与结果不配对、作用域分发错误、重建的模型请求与日志不一致——这些错误只存在于多条记录或多个服务之间的关系中冻结单个对象永远无法发现。把两者混在一个可选的开发插件里生产环境的历史记录就会失去保护而试图用 TypeScript readonly 类型同时表达两者则既没有运行时边界也描述不了关系规则。TypeScript readonly 的三个根本缺陷决策笔记明确指出原文第 15 行类型在运行时被擦除readonly 只是编译期提示程序一运行就消失类型转换cast可以绕过插件代码as一下就失去了保护递归DeepReadonlyT会污染消费方它会扩散到每个日志和消息消费方而某些下游请求处理 API 恰恰有意使用可变值。因此 dsh 的结论是运行时所有权必须在Session边界建立而不是依赖类型系统传播。Session 拥有不可变历史append 的全链路一次递归遍历完成无损 JSON 快照核心实现在 packages/core/session/src/index.ts 的Session.append()。每次追加事件时事件数据先经过snapshotJsonValue(data)的单次递归遍历该遍历由 packages/core/session/src/json.ts 的walkJsonValue实现采用迭代式任务栈而非递归因此嵌套深度只受内存限制不受 JS 调用栈限制它在同一遍里完成验证与物化分离快照每个属性只读取一次所以有状态的 getter 不可能给验证一个值、给存储另一个值它拒绝所有无法无损往返 JSON 的值BigInt、函数、symbol、undefined、负零、非有限数、循环引用、稀疏数组、以及Map/Set/Date/类实例等异型对象。数组还必须使用原生Array.prototype对象必须是纯对象或 null 原型对象保证与JSON.stringify行为字节一致json.ts 注释。const dataSnapshot snapshotJsonValue(data) if (dataSnapshot undefined) { throw new Error(session event ${type} carries non-JSON-serializable data) }深度冻结后发布同一个对象到达所有观察者快照通过后append()组装完整事件并对整个事件及其所有后代做deepFreezeindex.ts 第 625 行const event deepFreeze({ type, seq: this.log.length, time: Date.now(), data: dataSnapshot, ...(surfaceMetadataSnapshot as { surfaceOp?: unknown; sourceEventSeqs?: unknown }), } as unknown as SessionEventT)由此形成三个一致的读取面append()的返回值就是这个 Session 拥有的冻结事件session/event观察者收到同一记录见 index.ts 事件声明session.events返回一个冻结的数组快照index.ts 第 557-560 行get events(): readonly SessionEvent[] { this.eventsSnapshot ?? Object.freeze([...this.log]) return this.eventsSnapshot }关键语义快照在 append 时缓存、下一次 append 时失效重建之前返回的数组不会因后续 append 而增长调用方持有的旧数组永远停留在历史形态。快照数组是浅冻结 事件元素早已深度冻结所以既不能用普通赋值改写历史也不能用 cast 绕开事件本体已是Object.freeze严格模式下写入会抛错。种子记录走同一条边界回放 / fork / 恢复Session.create/Session.fromRestore的种子事件在构造成功前经历完全相同的验证、快照与冻结边界index.ts 第 506-546 行每个种子事件同样经过snapshotJsonValue无损物化assertSessionEventEnvelope校验固定事件外壳type/seq/time/data/surfaceOp/sourceEventSeqs并拒绝遗留的request/header-delta旧格式强制seq index的连续序列契约从 0 开始种子逐条通过surfaceManager.validateNext()的增量验证后才进入log防止部分变更污染 surface。Session.fromRestore持久化恢复路径走的是freezeRestoredObject——一个迭代式、不消耗调用栈的深度冻结工具index.ts 第 197-210 行因为恢复的对象是调用方转移所有权的全新对象不需要再克隆原地冻结即可。session/end-seed标记事件由构造过程追加标识进程内首批 live 序列号firstLiveSeq。为何这个保证必须属于 Session 本身决策笔记强调原文第 27 行该保证放在Session而非可选监听器里是因为每种组合都依赖可信的历史。生产部署、聚焦测试、自定义嵌入无论是否注册了开发支持插件都得到相同的存储语义——这正是不变式可选与所有权必选分层的根基。派生请求保持分离deriveMessages 的投影与缓存Session.deriveMessages()index.ts 第 724-745 行把日志投影成 LLM 可见的消息序列其设计要点是新数组 共享冻结消息每次调用返回全新数组快照后续 append 不会增长调用方已持有的数组数组内的Message对象是共享且深度冻结的——它们的内容直接复用事件data里已经冻结的持久数据因此投影不需要第二次深克隆消费方却依然无法改写日志投影规则是 packages/core/session/src/surface.ts 的纯函数deriveEventMessage只有user/message、assistant/message、tool/result三种事件参与投影空内容的 assistant 消息仅为承载 usage 而存在返回 null 不入对话流轮次边界、chunk、请求头等事件则天然缺席投影结果带缓存每个 surface 节点只投影一次后续调用是 O(新增节点)surface 发生replace压缩重写时按replaceGeneration整体重建缓存。底层支撑是SurfaceManagersurface.ts 第 398-460 行它把有序 surface维护成增量折叠候选事件先validateNext()做纯验证、零副作用的规划规划失败不会推进任何状态提交后才原子应用。这样deriveMessages()这条最热门的请求路径既拿到了稳定的不可变投影又不需要为每次模型调用重新克隆完整历史。包拥有的不变式dsh-invariants 注册服务服务与配套插件分离deepseek-ai/dsh-invariants是产品无关的 Cordis 服务插件注册ctx.invariantspackages/runtime-diagnostics/invariants/src/index.ts。它本身不包含任何产品检查只拥有配置、注册唯一性、子 fiber 生命周期、包归属的失败报告。它不导入任何 session / agent / scope / agent-loop 包。每个工作区包发布一个./invariant配套插件用完整的 npm 包名注册。配套插件的标准形态以 session 为例packages/core/session/src/invariant.tsexport const name session-invariant export const inject [invariants] export const apply (ctx: Context): Promise() void Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))包根入口不隐式注册任何诊断加载根包不会改变运行时检查、也不要求 invariant 服务存在。配置与选择算法服务配置index.ts 第 15-22 行export interface Config { readonly enabled?: boolean // 全局开关默认 true readonly package_allowlist?: string[] // 允许包名匹配的 regex 源默认 [] readonly package_blocklist?: string[] // 排除包名匹配的 regex 源默认 [] }选择语义与 配套决策笔记 的伪码一致实际实现见 index.ts 第 121-126 行blocklist 优先于 allowlist允许列表命中但被阻止列表命中的包被排除每个列表条目是大小写敏感的 JavaScript regex 源经new RegExp(pattern)编译匹配是非锚定的除非调用方自己写^和$不支持/slashes/写法与 flags启动时拒绝列表中的空白、带首尾空格的、非法的、重复的条目compilePatternsindex.ts 第 75-91 行匹配不到任何已加载包的 regex 源仍然合法注册顺序、后续加载、HMR 重载都不能改变配置有效性——一个零匹配模式可以有意地瞄准之后才加载或 HMR 加载的贡献。注册、事务性与失败归属ctx.invariants.register(packageName, installer)index.ts 第 136-197 行是唯一的公开注册边界规则如下每个完整 npm 包名只允许一个活动注册即使过滤把它禁用了也保留名字保留权直到 dispose 释放被选中的 installer 运行在服务拥有的子 Cordis fiber中InvariantInstaller.inject显式声明子 fiber 需要的服务 API如[sessions]注册表不携带任何产品相关依赖元数据服务会await installer 返回的 promise之后注册才算成功因此异步启动检查保持事务性installer 收到绑定的fail(message)报告器调用它抛出名为InvariantError的Error子类带有稳定错误码INVARIANT和注册包名packageNameindex.ts 第 50-66 行但不继承任何产品包的错误基类super(invariant violated by ${packageName}: ${message})注册设置是事务性的installer 注册完监听器后失败子 fiber 会被完全 dispose、名字保留被释放然后错误才向外传播被过滤的注册不创建子 fiber但保留名字保留权直到 dispose。因此重载一个配套插件总是从干净的 installer 状态开始。回放重建跟踪状态会话配套插件挂到已有会话或以种子初始化的会话时会回放不可变日志重建跟踪状态packages/core/session/src/invariant.ts 第 206-218 行const seedSession (session: Session): SessionTrace { const trace freshTrace() traces.set(session, trace) for (const event of session.events) { applyTransition(trace, validateEvent(trace, event, fail)) } return trace }由于日志本身是不可变的回放重建一定是确定性的服务给每项贡献一个可 dispose 的子 fiber因此轮次中途热重载也是安全的诊断逻辑永远不会获得会话存储的所有权。四个初始关系规则包每个包守护自己的 seam决策笔记给出了初始的四个包级规则原文第 35 行配套清单见 2026-07-19-package-owned-invariant-service.md 表格配套入口注册名拥有的检查deepseek-ai/dsh-session/invariantdeepseek-ai/dsh-session会话序列单调、轮次/步骤闭合、同一步骤内工具调用/结果配对deepseek-ai/dsh-agent/invariantdeepseek-ai/dsh-agentagent 状态合法迁移deepseek-ai/dsh-scope/invariantdeepseek-ai/dsh-scope作用域事件 carrier 存在性与主体一致性deepseek-ai/dsh-agent-loop/invariantdeepseek-ai/dsh-agent-loop模型请求重建一致性dsh-session序列、嵌套与工具配对packages/core/session/src/invariant.ts 维护每个会话的SessionTracelastSeq、openTurn、openStep、nextTurn、nextStep、pendingCalls对每个候选事件做纯验证产生延迟应用的转移序列单调event.seq trace.lastSeq即失败轮次/步骤嵌套turn/start要求无打开的轮次且编号等于nextTurnturn/end要求匹配当前轮次且无打开的步骤step/start要求在当前轮次内且编号连续step/end、assistant/chunk、assistant/message、tool/call都要求命名当前打开的轮次与步骤requireOpenStep工具调用/结果配对tool/call把callId记入pendingCallstool/result在追加语义下必须能在本步骤找到先前的tool/callTOOL_NOT_STARTED的合成结果除外step/end时清空待处理集合请求头必须在轮次内request/header、request/context不允许出现在任何打开的轮次之外。验证与提交分离是这里的关键internal/dispatch阶段先验证并暂存转移session/event发布时才提交。如果后注册的 dispatch 监听器否决了事件暂存的转移直接作废已提交的 trace 不会前移有专门测试覆盖见 packages/core/session/tests/invariant.spec.ts。dsh-agent状态迁移不重复packages/core/agent/src/invariant.ts 用WeakMapAgent, AgentStatus跟踪每个 agent 的最近状态监听agent/status拒绝 no-op 的重复状态迁移const previous lastStatus.get(agent) if (previous status) { fail(agent/status repeated ${status} (no-op transition)) }dsh-scopecarrier 与主体一致packages/core/scope/src/invariant.ts 监听internal/dispatch对每个作用域过滤事件必须带着作用域 carrierisScopeCarrier分发——否则报缺少 scope carrier并提示改用scopeTarget(base, subject)或agentEvents(ctx, agent)carrier 的 key 必须与事件参数命名的主体一致carrierKeyOf(thisArg) ! subjectOf(args)即失败防止carrier 指向 A、事件却是 B 的错配。主体解析来自dsh-scope内生成的语义映射scopedSubjectResolverForscoped-events.generated.ts由gen-scoped-events用根 TypeScript Program 枚举this: ScopedBase声明、从真实scopeTarget(base, key)调用推断路由键类型而生成导入路径不引入任何事件属主包。dsh-agent-loop请求与日志前缀重建相等packages/core/agent-loop/src/invariant.ts 在llm/stream上以prepend: true挂载防止短路的重放监听器压制检查对 loop 构建的请求断言请求对象及其messages数组必须是冻结的必须携带 live 的sessionId日志里必须有step/start和request/header事件JSON.stringify(options.messages)必须等于session.deriveMessages()的序列化结果——即loop 构建的请求与从会话日志前缀重建的请求必须相等防止日志重建脱钩model、system、temperature、maxTokens、stop、tools必须与foldRequestHeader(events)折叠出的请求头一致。这正是决策笔记点名的循环构建的请求与从其会话日志前缀重建的请求之间的相等性检查的落地实现。曾考虑过的替代方案及其否决理由决策笔记 Alternatives considered 记录了三组被否决的路线理解它们有助于把握边界设计的取舍全面的 deep-readonly 类型在公开日志与消息接口上铺DeepReadonlyT能提供编辑器反馈但类型被擦除、插件可以 cast 绕过还会把 readonly 推入有意修改的消费方。运行时所有权在Session边界保护所有调用方无需类型传播。仅在开发模式冻结只有装 invariants 插件才冻结历史会让核心保证依赖组合方式——代码可能通过开发测试却在生产或省略插件的组合中破坏历史。因此存储不可变性永远开启关系检查才是可选的开发支持。仅在派生消息时克隆只分离deriveMessages()保护最热路径但session.events的其他读者、append 返回值、事件观察者仍能改写历史。日志必须保护自己的边界派生投影是额外的隔离边界而非替代品。配套服务的决策笔记 2026-07-19-package-owned-invariant-service.md 还否决了把所有检查集中在 dsh-invariants根入口在ctx.invariants存在时隐式注册运行时自动发现invariant.ts按当前已加载包集校验配置条目四条路线——原因分别是集中注册表会持续导入每个被检查的产品域、根行为依赖组合顺序、文件系统发现不是运行时所有权契约、零匹配模式可能有意瞄准后加载贡献。后果与成本模型决策笔记 Consequences 汇总了这套设计的完整后果每个被接受的实时或种子事件在任何观察者收到之前都已从调用方输入中分离并深度不可变session.events暴露稳定快照而非私有增长数组请求侧修改无法通过派生消息触及已存储历史开发构建可启用关系断言而不改变存储行为dispose 或过滤配套插件不会削弱日志不可变性dsh-invariants配置全局启用 包名允许/阻止 regex 列表每项检查仍由其产品包拥有并测试成本模型清晰每个被接受事件付出一次递归快照 冻结后续读者和缓存投影复用已拥有的不可变记录每个被选中的可执行贡献增加一个子 fiber 及其监听器/状态成本被选中的空贡献无监听器成本被过滤的注册只保留名字所有权。配套决策笔记还补充了工程化约束verify-package-invariants会机械地扫描每个工作区包拒绝缺失配套源码、生成占位、无解释的空 installer、忽略报告器的非空 installer、外来或未解析的注册名、缺失./invariant导出/发布文件、缺失配套依赖与项目引用、以及遗漏配套入口的 bundle 覆盖2026-07-19-package-owned-invariant-service.md 第 69 行——保证包所有权穷尽且可验证新包无法悄悄漏掉诊断接线。服务测试覆盖默认值、全局禁用、allow/block 选择、blocklist 优先级、锚定/非锚定匹配、大小写、非法配置、零匹配模式、迟到注册、重复所有权、dispose、回滚与 HMR 重注册。在实战中如何配置与使用挂载不变式开发/诊断组合在你的 Cordis 组合中显式挂载服务与所需的配套子路径import InvariantRegistry from deepseek-ai/dsh-invariants import SessionInvariant from deepseek-ai/dsh-session/invariant import AgentInvariant from deepseek-ai/dsh-agent/invariant import ScopeInvariant from deepseek-ai/dsh-scope/invariant import AgentLoopInvariant from deepseek-ai/dsh-agent-loop/invariant await ctx.plugin(InvariantRegistry, { enabled: true, package_allowlist: [^deepseek-ai/dsh-], // 可选regex 允许列表 package_blocklist: [], // 可选regex 阻止列表优先于 allowlist }) await ctx.plugin(SessionInvariant) await ctx.plugin(AgentInvariant) await ctx.plugin(ScopeInvariant) await ctx.plugin(AgentLoopInvariant)示例 agent spinepackages/examples/agent-spine-demo与生成的 SDK Cordis 组合都采用了这一挂载方式子路径条目安装其可安装的根 npm 包而不是把子路径当成包名。出厂自带的dshTUI 与 Web 配置树则省略服务与配套插件shipped-config 决策因为存储边界本身永远开启、不依赖它们。为你的包贡献一个不变式如果你在开发一个新包要接入这套诊断体系只需三步在你的包根增加src/invariant.ts配套插件用完整 npm 包名注册可参考 packages/core/scope/src/invariant.ts 的最小实现在 installer 里通过ctx.on(..., { global: true })挂载你需要的检查用fail(message)报告违规在package.json的 exports 中发布./invariant子路径、声明对deepseek-ai/dsh-invariants的 peer/开发依赖与项目引用并确保verify-package-invariants通过——它要求非空 installer 必须真正使用 reporter。触发不变的测试形态会话配套插件的测试packages/core/session/tests/invariant.spec.ts展示了标准的服务 配套测试拓扑const ctx new Context() await ctx.plugin(SessionStore) await ctx.plugin(InvariantRegistry) const fiber await ctx.plugin(SessionInvariant)随后对一个正常轮次序列turn/start→user/message→step/start→assistant/chunk→assistant/message→tool/call→tool/result→step/end→turn/end断言not.toThrow()对违规序列断言抛错。仓库中每个 Vitest 配置都加载一个测试宿主在普通 Cordis 根的第一个插件之前挂载显式启用的服务并加上当前测试包的配套插件。总结DeepSeek Harness 的会话不可变性设计回答了如何在插件化系统里保证历史可信这一核心问题所有权是运行时事实关系是诊断义务。Session边界用无损 JSON 快照 深度冻结把每一次接受变成一次性的、不可逆的物化deriveMessages()用共享冻结投影为最热门的请求路径提供隔离而廉价的读取dsh-invariants用产品无关的服务 包拥有的配套插件把关系检查精确地归还给每个知道规则语义的属主包。这套分层让生产部署始终获得存储保护同时让开发构建可以按包名精确选择、随时热重载关系断言而不会在任何一层削弱日志的不可变承诺。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考