AGENTS.md@src/host/scheduler · git:20260922.cf8e5a9 · 2026-09-22 · sha256 141224a36c6bdbb9

AGENTS.md@src/host/scheduler git:20260922.cf8e5a9A

Immutable. This exact content never changes and is served at /api/v1/blob/141224a36c6bdbb9.

# Scheduler Module Rules

## 适用范围

适用于 `src/host/scheduler/`。本文件定义定时任务模块特有的稳定治理边界与修改规则;通用规范遵循根 `AGENTS.md`,测试规范遵循 `tests/AGENTS.md`。

本文件描述治理原则,不是源码结构说明。文件移动、重命名或内部重构后,只要职责、事实源、依赖方向与对外契约未变,本文件不随之修改。

## 核心职责

按用户配置的**执行窗口**与**触发策略**,驱动既有编排运行时运行所选工作流模板的实例:

- 任务配置的校验、规范化与持久化;
- 时间语义:时区换算、触发点推算、窗口判定;
- 决策与推进:何时触发、何时挂起、何时续跑、何时跳过;
- 触发时的实例化语义(模板 → 实例)与会话策略分派。

判断归属:该修改是否改变**定时任务的配置契约、时间语义或触发/挂起/续跑决策**?若主要属于时间基元之外的其他领域(编排推进、Agent 语义、持久化原语、UI 展示、HTTP 端点),应放到对应模块。

## 非职责

- 不实现编排状态机、运行锁与断点续跑机制——这些归编排运行时,本模块只**调用**其既有能力;
- 不实现会话与 Agent 的创建/恢复机制——那属官方运行时能力适配层;
- 不拥有持久化原语(原子写、锁、崩溃恢复)——只按自己的数据文件复用它们;
- 不决定 HTTP 语义(状态码、端点形态)与用户界面;
- 不直接修改其他模块的内部状态。

## 时间语义契约

时间语义必须确定、可测、与时区显式绑定:

- 时间的解析/格式化/换算/推算一律为**纯函数**:相同输入 → 相同输出,不读系统时钟、不读环境时区、不依赖全局状态;需要"当前时刻"时经参数传入;
- 绝对时间在存储与比较层面统一用 UTC 毫秒;用户可见的时刻("HH:mm"、日期)一律按任务自身声明的时区解释——**禁止**使用进程本地时区或隐式时区;
- 跨天区间(结束不晚于开始)必须与跨天触发点、跨日期窗口扫描的语义一致,不得各处自行解释;
- 调度语义分两层且语义固定:第一层「执行窗口」决定何时允许运行,第二层「触发策略」决定何时应当触发;**触发成立的条件是两层同时满足**,不得用其中一层代替另一层。

## 单一事实源

同一概念只允许一处本体:

- 时间解析、时区换算、窗口判定、触发点推算各自只有一处实现,使用方引用本体而非重写;
- 任务字段的**形状收敛**、**业务校验**、**保存前规范化**是三个独立步骤,各有唯一实现且顺序固定(收敛 → 校验 → 规范化);调用方不得自行拼装任务实体;
- 任务的持久化形态(含运行时字段)只有一处定义,读写经同一条通道。

## 子域边界

模块内部存在变化原因不同的子域:时间基元、窗口判定、触发计算、任务配置契约、持久化、决策引擎、实例化。

- 子域之间只通过明确的输入输出协作,不得互相承担对方职责;
- 时间基元不理解"任务";窗口判定与触发计算不理解持久化;配置契约不读盘、不决策;决策引擎不重复实现时间算法;
- 新增能力先判断属于哪个子域,再放到该子域的职责中,不得为就近而堆进已有位置。

## 状态与生命周期

- 本模块持有两类状态:**磁盘上的任务定义与运行时游标**(跨重启保留)与**进程内的引擎运行态**(定时器、内存缓存、启动基准);
- 进程内运行态归引擎实例所有,随实例的启动/停止/清理走完整生命周期;任何外部模块不得直接读写它;
- 启动必须幂等;停止/清理后不得再启动(终止即终止,不得因再次调用而复活);
- 定时扫描必须**可重入安全**:扫描耗时可能超过扫描间隔,重叠触发时必须合并为一次扫描,禁止对同一任务并发决策(否则会出现重复触发、重复挂起、游标互相覆盖);
- 单任务失败不得影响其余任务:失败必须记录到该任务的可观测结果上,然后继续;
- 丢弃触发(并发策略)必须**只推进游标而不改变运行**,不得中断正在执行的轮次。

## 副作用显式

本模块的副作用只有:读/写自己的任务文件、创建会话、启动/挂起/续跑编排运行、写日志。

- 副作用必须出现在有明确职责的位置,不得藏进看似纯计算的辅助函数;
- 纯计算与状态修改必须分离:决策所需的全部事实由调用方或参数传入,决策本身不产生副作用;
- 外部能力(编排运行时、数据层、会话创建、时钟、日志、扫描间隔)一律经**显式依赖缝**注入,便于替换与测试;本模块不得直接 import 具体运行时实现。

## 持久化边界

- 任务文件只有一个写者(本模块的存储实现);其它模块不得绕过它写同一文件;
- 读改写必须在同一临界区内完成;运行时字段的更新必须合并写,不得整文件覆盖导致并发丢更新;
- 磁盘状态与进程内状态不一致时,以「可恢复的最小假设」处理(例如游标缺失按启动基准推进),且该假设必须可在注释与测试中追溯。

## 公共边界

- 模块外只允许经模块的**公共入口**导入,禁止直接引用模块内部文件;
- 只把真实、稳定、可维护的契约放进公共入口(引擎与其依赖缝、存储、任务配置契约、稳定的时间/窗口/触发纯函数、实例化);
- 内部实现(决策的状态迁移细节、任务文件的磁盘形态与读取守卫、内存缓存与定时器结构)不进入公共入口;禁止「全量 barrel」;
- 新增公共符号前先判断它是否表达对外稳定契约;放宽入口比收紧入口的代价更高。

## 依赖边界

- 只允许依赖共享契约层(任务实体与窗口/触发配置的形状)与本模块内部;
- 不得依赖 api / tools / storage 的具体实现 / agent / client;
- 对外部能力的依赖必须是抽象缝(接口 + 注入),方向单向:调用方依赖本模块的决策,本模块不反向依赖任何调用方。

## 契约稳定性

- 任务字段、运行态字段、状态枚举与"最近一次结果"取值属公共契约:修改必须同步调用方、测试与文档,并说明旧数据的读取行为;
- 字段增删遵循兼容优先:新增字段默认可选;删除或改义必须给出兼容路径与剥除时机;
- 时间语义变化(例如"什么算窗口内""跨天如何解释""下一触发点如何取")属行为变化,必须记录:旧行为 → 新行为 → 原因 → 影响范围 → 测试变化;
- 会话工作目录的决策(显式路径优先 / 继承创建者)只有一处实现,调用方不得复制该决策;显式路径的**存在性校验属接受该配置的保存端点**,运行期不重复校验——该不变式必须在实现处写明。

## 模块内部组织

- 文件结构不是架构契约,禁止为"看起来模块化"机械拆分;拆分依据只能是职责、变化原因、依赖边界与测试边界;
- 文件命名必须表达**稳定职责与所属维度**(例如同族概念保持一致的命名维度),名称失真的文件应立即重命名并同步全部引用;
- 不得创建 `utils.ts`、`helpers.ts`、`common.ts`、`shared.ts`、`misc.ts` 等无明确职责的聚合文件;
- 内部文件之间使用相对路径导入,公共入口不含实现。

## 修改前检查

1. 改动属于时间基元、窗口判定、触发计算、任务配置契约、持久化、决策引擎还是实例化?
2. 是否改变时间语义或触发/挂起/续跑决策?受影响的调用方与测试是哪些?
3. 是否新增/修改任务字段或运行态字段?兼容路径与测试是否同步?
4. 是否引入非确定性依赖(时钟 / 随机 / 环境时区 / IO / 全局状态)?
5. 是否产生第二份事实源(重复的时间算法、重复的字段收敛、重复的持久化形态)?
6. 是否越界进入其他模块职责(编排状态机、会话创建、HTTP 语义)?

## 验证

除根 `AGENTS.md` 的标准命令外,本模块改动必须额外确认:

- 时间相关断言全部使用**固定日期与时区**,不依赖运行机器的本地时区与当前时间;
- 窗口边界、跨天区间、跨日期扫描、无解配置(永不触发)等边界行为均有测试锁定;
- 决策路径的关键分支(触发 / 触发失败 / 并发跳过 / 窗口挂起 / 窗口续跑 / 用户接管 / 停用 / 重启游标)均有测试锁定;
- 重叠扫描、重复启动、终止后再启动等并发与生命周期行为有测试锁定;
- 公共入口导出面未意外扩大、无旧路径残留、无循环依赖、无重复实现。

## 核心原则

1. 时间语义确定且时区显式,纯计算不读时钟。
2. 窗口与触发两层语义固定,缺一不触发。
3. 收敛、校验、规范化各有唯一实现且顺序固定。
4. 扫描可重入安全,终止后不复活。
5. 外部能力经显式依赖缝注入,副作用不藏于纯函数。
6. 模块外只经公共入口协作,内部实现不外溢。
7. 语义变化必须显式记录并锁定测试。