之前 nanobot 的 cron 把结果发到聊天窗口后,这条消息并不会进入这个窗口的上下文。
假设有个定时任务每天早上查天气,9:30 告诉我:“今天会下雨。”我顺手回一句:“太糟了。”agent 却问:“什么太糟了?”
对用户来说这是同一段对话,对系统来说,cron 和聊天走的却是两套隔离的上下文。agent 接不上用户的回复,下一次 cron 触发时也看不到这个窗口里后来聊了什么。
最开始我们把注意力放在了“这条消息要不要发”上。nanobot 曾经用一个 evaluator 对 cron 结果再做一次 LLM judge,避免后台监控每次都往 channel 里发送没有意义的结果。它解决的是通知噪音,却解决不了对话连续性:evaluator 可以决定发不发,不能让发出去的消息自然成为 session 的一部分。
第一反应很容易是把 cron 结果直接塞回消息列表,或者作为 runtime context 注入当前对话。但继续往下想就会遇到更多状态:用户可能正在和 agent 聊天,也可能已经几天没有打开 nanobot。前一种情况下,新的 agent turn 会抢占正在进行的回复,像聊到一半突然被后台任务夺舍;后一种情况下,又根本没有一个正在运行的 context 可以注入。
后来采用的方案更简单:创建计划任务时,把它绑定到当前 session;触发时,把它当作这个 session 里一次正常的 agent turn。这样 cron 的触发和结果都会进入同一套 history,用户可以直接追问,下一次任务也能看到这段对话。
如果目标 session 正在运行,cron 不再注入当前 turn,而是先进入队列,等这个 session 空闲后再执行。这里真正需要的不是让模型理解一次特殊的后台插入,而是让同一个 session 的多个 turn 保持串行。
所以 cron 和 session 的关系最后收束成了一条很明确的规则:cron 应该共享目标 session 的上下文,但不能抢占正在运行的 turn。这个方案未必覆盖了所有自动化场景,但已经比原来两套上下文各走各的自然很多。issue #1445 里的后续说明 也正是基于这个变化关闭的。
“没有变化时不要通知”则是另一个问题。现在普通 cron 更适合提醒、定时汇报这类每次都应该产生结果的任务;需要周期性检查、只有出现有效变化才通知的监控任务交给 heartbeat,由 evaluator 决定是否值得打扰用户。把这两种自动化拆开后,不必再让一个 deliver 开关同时承担通知策略和上下文一致性。
绑定 session 之后,生命周期也必须一起处理。Codex 的自动化任务可以绑定会话,因为会话只能归档,触发时还能重新拿出来。nanobot 的状态都在本地,用户可以真的删掉 session,也可以直接清理存储,不能假装这个绑定永远可以恢复。
现在删除带有计划任务的 session 时,WebUI 会先列出关联的 automation 并阻止直接删除;用户确认后,再把这些任务一起删掉。WebUI 也增加了独立的 automation 管理页面,可以集中查看所有任务,再从任务回到它绑定的聊天。
这也让我重新理解了“计划任务应该独立于聊天窗口”这件事。它的管理入口可以独立,执行上下文却不应该独立。前者解决任务怎么被找到和管理,后者决定一条自动消息能不能继续成为对话。