Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md
Tianyi Cui a2d0f7f411 refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
2026-08-13 00:54:38 +08:00

8.1 KiB
Raw Blame History

Agent Note: 设有强制脱敏点和 OTel 后端的会话遥测 seam

Status: implemented

English | 中文

问题

每个想把 harness 会话接入可观测性体系的部署方都得手写一个会话日志消费方:订阅、生命周期交接、以及最难的脱敏——原始日志携带文件内容与命令输出,可能内嵌凭据。遥测 seam 和 OTel 后端曾在 session-telemetry-otlp-rfc 分支PR #222/#231上完成过一版但从未进入 master该提案将原始会话事件原样导出法务评审未予通过。捕获侧设计后端约定、coordinator、handoff 游标、分片投影)本身合理且经过评审;导出侧的立场才是阻塞点。

决策

packages/session/(原 telemetry/)以 SDK 立场复活这两个经过评审的包——harness 提供能力,部署方配置上报去向并对导出内容负责:

  • @deepseek-ai/dsh-session-telemetry —— seam 本体。SessionTelemetrySinkemit/flush?/shutdown)、服务注册形态的 SessionTelemetryBackend、以及拥有捕获侧的 SessionTelemetryCoordinator:带游标回读的实时纳管与逐 append 的 firehose投影 → structuredClone → 脱敏 → emit,零 I/O、从权威日志进行的无缓冲按需回放、固定的每个轮次、步骤组合首分片投影、实时 agent/error 转发,以及实时 dispose资源释放时的 shutdown 记录。
  • session-telemetry/record waterfall瀑布式事件 —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何后端前必经此处seam 自身不带任何规则——最内层 next() 原样透传,部署方以监听器挂载自己的规则(通过变换 next() 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本canonical log 永不改写。
  • @deepseek-ai/dsh-session-telemetry-otel —— 参考后端OTel JS SDK 日志流水线(LoggerProviderBatchLogRecordProcessor → OTLP/HTTP exporterexporter/processor passthrough 原样配置。DISABLED 是默认值,且不构造任何传输;反馈门控遥测决策定义了需显式启用的 FULLFEEDBACK_ONLY 投递模式,这两种模式要求 exporter.url,且不移动脱敏或后端边界。无缓冲反馈回放避免在内存中创建会话前缀的第二份副本。

边界公理保持不变harness 的职责止于 emit()。批处理、重试、排队与丢失策略属于 reporting SDK经 passthrough 配置——投递是尽力而为(崩溃时至多一次),两份 README 对此如实陈述。

考虑过的替代方案

实现 runtime-telemetry RFC 的 outbox落盘 spool、每 sink 游标、at-least-once、持久化 seam 的 readCommitted 方法)。 推迟而非否决SDK 立场使投递语义归属 reporting SDKOTel SDK 自身的批处理流水线是诚实的默认。outbox 是纯增量层(emit() 约定不动);待某个部署提出遥测必须满足的崩溃丢失要求时再复活。

不设进程内脱敏点,交给接收端 collector processor。 否决——接收端脱敏是先把秘密发出去再擦除。waterfall 在字节离开进程前提供一个可审计、可堆叠的擦除点分支版本PR #222 交付的形态)完全没有脱敏点,如今每条记录都必经该脱敏点。

在 waterfall 最内层 next() 内置一套保守规则集。 否决:作为 SDK 我们无法预知某个部署里什么模式算秘密,内置列表只覆盖已知形状却会带来「脱敏已开启」的虚假信心,且误报会破坏未提出此要求的消费方所接收的导出 body。seam 拥有机制,部署方拥有策略——最内层 next() 原样透传,规则以监听器挂载。

映射到 OTel spanGenAI 语义约定)而非日志。 本次复活否决分支实现的日志映射已经过评审、形态可交付span 模型对可 fork、可中断的会话有损留给将来真正有 span 查询需求的消费方。

handoff 游标未存活时全量回放日志(重新导出构造函数种子)。 首轮复活曾交付此方案,其后收窄:接管操作现在从会话的构造边界起回放(Session.firstLiveSeq,即构造函数种子长度,这一事实会话早已校验过却未曾暴露;header.seedLength 不能胜任:它是持久保存的 fork 谱系lineage而恢复会话的构造函数种子是其完整的已存储日志。恢复会话的历史已由上一个进程以同一 id 发出fork 继承的前缀也已在父会话的流中发出;再次导出任何一者,都会让每次恢复为其完整历史重复付费,并在没有原生摄取去重的 OTLP 后端上使查询时的计数翻倍。接收端基于 session.parent_id + session.seed_length 拼接 fork 谱系。此次收窄放弃的内容与至多一次立场一致:恢复不再回填上一个进程未能投递的记录(彼时遥测未挂载,或崩溃时仍在队列中)——这本是全量回放唯一的真实收益,代价却由常见情形承担。提出回填要求的部署需要的是上文已推迟的 outbox而不是回放。该边界同样吞掉 SessionPersistence.load() 修复被崩溃打断的日志时写入的合成轮次关闭事件(它们落在 firstLiveSeq 之前尽管在上一个进程中从未存在过。这是有意为之而非附带效果远端轮次的真实尾部记录已随崩溃进程的队列一同消亡导出合成关闭事件无法补全该轮次只会让一个未完成的轮次看起来已经关闭。导出的流忠实于崩溃进程实际发出的内容接收端会把恢复后的流中一个从未关闭的轮次读作「上一个进程死在了该轮次之内」OTel README 陈述了这条规则),其后干净的 shutdown 标记也只证明恢复后进程自身的退出。若为让修复以实时事件的身份导出而将修复前边界贯穿 load/prepare 传递,将使三个包相互耦合,只为抹除这一信号。

将 seam 的轮次边界 flush() 提示转发到 OTel 提供方的 forceFlush() 首轮复活曾交付此转发其后移除三条不同的静默丢失路径共用同一份包装层状态——dispose 与进行中的 flush 之间的竞态SDK 的并发 flush 防护会令 shutdown 的内部排空被跳过)、相互重叠的提示顶掉留存的 promise、以及提供方固定的 30 秒 flush 超时在批处理器仍在排空时便 reject。这些路径存在的唯一原因是该转发让这个后端成为进程内第二个执行 flush 的组件面对的还是上游实验性experimental源码树中未见诸文档的 SDK 内部行为;不实现 flush() 时,批处理器就是唯一执行 flush 的组件,其 scheduledDelayMillis(已可由部署方经 processor passthrough 调优)决定导出节奏,shutdown() 的排空从构造上就是完整的。仅当某个部署提出 scheduledDelayMillis 无法满足的轮次边界延迟要求时才恢复此转发——且届时应调用留存的 BatchLogRecordProcessor 自身的 forceFlush(),绝不调用提供方那个带超时包装的版本。

后果

部署方在 cordis.yml 加一个带 OTLP endpoint 的 Cordis 配置项,并显式选择 FULL,即可把会话流接入任何 OTel 兼容体系;选择 FEEDBACK_ONLY 则会在记录反馈时回放权威日志前缀。DISABLED默认值,且不构造上报流水线;删除该配置项仍是静默退出方式,而禁用模式会保留本地反馈警告。未挂载规则的部署导出的记录与捕获时完全一致,包括文件内容与命令输出中内嵌的任何凭据。因此,跨信任边界的部署必须挂载 session-telemetry/record 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是真源。崩溃持久性在上述 outbox 决定重新审议前明确不在范围内。