# Agent Note: 有界固定速率 Schedule

Status: implemented

[English](2026-08-09-bounded-fixed-rate-schedule.md) | 中文

## 问题

用户需要简单的重复提醒，但[持久、仅限 Session 内的提醒](../feature/2026-08-05-durable-web-schedule.md)最初采用的周期层把固定间隔和日历表达式当成一个通用子系统。它增加了 Cron 语言与求值器、时区敏感的发生时点搜索、tzdata 回放规则、跨记录的 300 秒准入门控、持久化的门控证据、延迟交付字段，以及门控耗尽状态。即使所请求的行为只是“每 N 秒重复一次”，这些机制仍会扩大持久协议与 live owner。

cold 或 busy Session 也无法有效回放每个错过的间隔。这样做会产生模型轮次积压，其规模取决于停机时长；如果改为按交付时间移动下一个目标，则会使固定速率发生漂移。

## 决策

保留的周期 selector 只有 `every_seconds`，其值必须是至少为 300 的安全整数。创建时会把第一个目标存为创建时刻加上一个间隔。每次 dispatch 都会存储记录 id 和一个由墙钟确定的 `acceptedAt`；纯整数运算会选出不晚于该决策时点、与创建锚点对齐的最新发生时点，并直接推进到其后的第一个对齐目标。系统不会枚举、持久化或回放错过的发生时点。

没有一次性提醒到期时，所有不同的逾期 Every 记录都会按目标时间和创建顺序参与同一个 follow-up 批次。每条记录恰好贡献一个最新发生时点，该批次中的每个 dispatch 都使用相同的决策时点。已到期的一次性提醒仍然优先，因此已经承诺的单次提醒不会被隐藏在周期批次中。

至少 5 分钟是每条 Every 规则自身的属性，而不是全局门控。系统不存在 `lastRecurringAcceptedAt`、`deliveryNotBefore`、冷却、配额、门控耗尽状态或通用周期记录抽象。如果运算无法表示下一个采用四位年份的 UTC 目标，最后一次 dispatch 会终结该记录。

日历表达式与 Cron 表达式，以及相应的求值器依赖、parser、canonicalizer、时区搜索、频率证明、持久记录和 dispatch variant、测试、快照与第三方声明条目均已移除。严格的版本 1 decoder 会拒绝预发布阶段的旧 Cron 记录，而不是迁移它们或通过兼容性残留接受它们。

## 已考虑的替代方案

**保留全局周期准入门控。** 共享门控可以约束模型轮次总数，却会使无关提醒彼此延迟，并需要持久的跨记录历史。批处理已经会把当前所有逾期固定速率记录合并成一个模型请求，而每条规则自身的最小间隔会约束唤醒频率。

**回放每个错过的发生时点。** 这样可以保留每个名义事件，却会在停机后产生无界积压，并不符合提醒的使用习惯。只追赶最新一次可以传达当前到期工作，而不会假装 Session 一直处于 live 状态。

**从 dispatch 时刻开始推进。** 这种运算更简单，却会把固定速率变成发生漂移的延时循环。保留下一个与锚点对齐的目标，才能维持用户设置的间隔。

**把 Cron 保留为可选分支。** 即使隔离在 selector 之后，Cron 仍需要日历语法、依赖、时区与夏令时策略、回放校验和庞大的测试范围。固定间隔可以提供实用的周期场景，而无需扩散这些复杂性。

**每个轮次只 dispatch 一条 Every 记录。** 这会串行处理无关的逾期工作，使后续多个轮次只能处理这组记录。一个批次既能保留彼此独立的提醒，又能约束模型请求数量。

## 验证

严格 decoder 与不变式测试会拒绝不受支持的规则和 dispatch 形状。领域测试与属性测试证明最小频率校验、创建锚点运算、只选择最新一次、推进和范围耗尽。运行时测试证明一次性提醒优先、所有逾期 Every 记录共享一个批次、每条记录只有一个发生时点、固定顺序，以及不会立即循环处理积压。组装 Web 快照证明，一个包含 2 条逾期记录的批次会产生一条普通 assistant 响应，以及两个使用相同时点的持久转换，并且不存在 Schedule UI sidecar。源代码、依赖与生成目录审计会拒绝 Cron 和全局门控残留。

## 后果

- 持久规则 union 包含 After、At 与 Every；工具 selector union 包含 `after_seconds`、`at` 与 `every_seconds`。
- 重新打开长期 cold 的 Session 时只会产生当前提醒工作，不会集中触发大量历史轮次。
- 多条逾期 Every 记录共享一个模型请求，但不共享调度状态，也不会彼此延迟。
- 基于日历的周期性需要未来的产品边界，而不是休眠兼容代码。
