
字节笔记本
2026年10月6日 · 约 7 分钟读完
DeepSeek Harness 定时提醒子系统解析
本文继续 DeepSeek Harness 开源框架的子系统系列,这次拆的是 packages/schedule 里的定时提醒(Schedule)子系统。它回答的问题很具体:如何让一个提醒在指定时间之后,以一条普通对话消息的形式回到原来的会话里,而不是变成一条站外推送通知。

边界先定死:只在原会话内投递
Schedule 的第一个设计决定是把交付模式固定为 session-local:提醒只回到创建它的那个活跃会话,不存在外部通知渠道,也不存在冷会话调度器。原会话必须活着,提醒才有落点。这个约束看起来是限制,实际把大量复杂度挡在了门外:不需要推送服务,不需要为离线用户缓存通知,也不需要处理多端同步。
模型看到的视图由三部分组成:持久记录本身,加上由当前墙钟推导出的 state 字段(scheduled 表示目标还在未来,overdue 表示已经逾期),以及恒为 session-local 的 deliveryMode 字段。
三种持久记录
一条提醒在持久层就是一条 ScheduleRecord,v1 支持三种形态。after 是延迟触发的一次性提醒,延迟必须为正的安全整数;at 是绝对时刻触发的一次性提醒,只存规范化后的目标时刻;every 是固定频率循环提醒,间隔不得低于 300 秒,也就是五分钟。三种记录共享同一个 id,它在会话内唯一且永不复用,外加一段创建时去除首尾空白的 prompt 内容。
创建时,所有首个目标都会被统一规范化为四位年份的 RFC 3339 UTC 字符串 scheduledAt:after 记录保留提交时的延迟值,at 记录只保留最终时刻,every 记录保留固定间隔和下一次目标。规范化意味着记录一旦落盘,语义就不再依赖创建时的任何环境状态。
绝对时间输入:时区必须显式
at 选择器接受两种形态:一条严格带时区偏移的 RFC 3339 字符串,或一个结构化的本地日历对象,后者由 date、time、time_zone 三个字段组成,时区必须是 UTC 或 IANA Area/Location 形式。任何不含偏移的时间字符串都会被直接拒绝。
Web 界面会为每个提醒采样浏览器所在的 IANA 时区,时间上下文会据此告诉模型:本轮请求只有一个无歧义的浏览器时区时,就用它解释未限定时区的自然语言时间;来源混乱或缺失时,就去问用户。但这条指引不是持久的会话默认值,模型最终仍必须在参数里显式传入偏移或 time_zone,而 Schedule 自身从不读取浏览器、会话、进程或模型上下文里的时区。
校验规则同样收紧:非法偏移和时区、无偏移字符串、非未来目标、落在夏令时空隙里的本地时间都会被拒绝;夏令时重叠时取较早的那个时刻。创建成功后只存规范 UTC 时刻,因此故障回放永远不依赖环境时区状态。
固定频率与追赶语义
every_seconds 是每条记录独立的间隔,下限 300 秒,锚定在创建时刻上。它只是固定频率循环:协议里没有日历或 Cron 表达式,没有循环时区,没有跨记录共享的冷却期,也没有跨记录的准入闸门。
当会话冷了一段时间或忙于其他事务,一条 every 记录可能接连错过若干个目标。追赶规则是只补最新一次:派发时直接把记录推进到决策时间之后第一个与创建锚点对齐的目标,不枚举、不持久化、也不重放错过的区间。如果下一个目标已经放不进四位 UTC 年份,最后一次派发会终结这条记录。

多条 every 同时逾期、且没有一次性提醒到期时,每条各贡献一次触发,合并成同一个后续批次,按目标时间和创建顺序排列;批次内所有派发使用同一个决策时间。批量准入限制了模型轮次的消耗,五分钟下限则限制了单条记录的定时器频率。
事件溯源:schedule/change 是唯一权威
Schedule 的持久化走事件溯源:version 1 的 schedule/change 会话事件是唯一持久权威。三类操作覆盖全部变更。create 存入完整记录;delete 是只带 id 的终结转换;一次性提醒的 dispatch 同样只带 id,记入即告终结,而 every 的 dispatch 额外携带挑选最新到期项时所用的墙钟决策时间 acceptedAt,正常情况下推进活动记录而不是终结它。
这里对派发的定义很克制:后续消息被同步入队即为派发,不代表模型回答成功,也不代表用户已读。入队失败就不会记录派发事件,提醒保持活动,等下一次机会再试。
严格解码器与折叠函数会拒绝一切不合规范的输入:未知版本、多余字段、复用的 id、一次性与 every 派发形态不匹配,以及对非活动记录的删除或派发。普通会话折叠完整事件流;fork 出来的会话只折叠 seedLength 之后的事件,保留历史但不接管父会话的活动提醒。
管理接口与错误码
模型通过 schedule_create、schedule_list、schedule_delete 三个工具管理提醒。管理调用与到期工作在同一个 Agent 作用域队列里串行执行:每次读取或决策先等待共享的会话持久化屏障,创建和实际删除在追加事件后还要再等一次。屏障失败时报 persistence_uncertain,而不是猜测预写是否已经提交。其余稳定错误码包括 invalid_prompt、invalid_selector、invalid_rule、invalid_time_zone、not_future、time_out_of_range、frequency_too_high、corrupt_schedule_log 和 internal_error。
实时交付与投递语义
运行时由一个进程本地的 owner 负责触发:它从持久折叠结果推导最早的定时器,并在每次有界等待后重读墙钟。冷会话不做任何工作;会话重新打开时重建定时器,过去的目标直接记为逾期。到期的一次性提醒优先,一次只进入一个后续轮次;没有一次性到期时,所有逾期的 every 记录合并为前述的单个批次。
到期工作要等 Agent 完全空闲并认领维护阶段之后才执行:重新折叠状态、采样决策时间、把一条 followup() 排入队列、再追加对应的派发事件。它从不调用 steer(),也从不打断当前轮次。提醒进入会话的唯一方式就是普通对话记录,Schedule 没有独立的 Web 回执或浏览器渲染层。
投递语义是尽力而为的至少一次。如果消息框架化或同步入队失败,就不会记录派发,提醒保持活动;但在准入成功与持久化派发之间的窄崩溃区间里,恢复后可能把同一条提醒内容重复投递一次。用这个小概率的重复,换掉整套 exactly-once 基础设施,在子系统层面这笔账是划算的。



