cron-up 26.10.33 → 26.10.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +61 -0
- package/README.md +228 -112
- package/docs/internals.md +190 -0
- package/docs/migration.md +47 -0
- package/docs/release.md +75 -0
- package/package.json +5 -3
- package/src/cli.mjs +8 -4
- package/src/constants.mjs +5 -7
- package/src/cron.mjs +3 -3
- package/src/display.mjs +2 -2
- package/src/list.mjs +1 -1
- package/src/overview.mjs +7 -7
- package/src/service.mjs +5 -4
- package/src/sessions.mjs +4 -4
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# 贡献指南
|
|
2
|
+
|
|
3
|
+
## 开发环境
|
|
4
|
+
|
|
5
|
+
- macOS,便于验证 launchd、`script(1)`、zsh 和进程行为
|
|
6
|
+
- Node.js 22 或更高版本
|
|
7
|
+
- 无第三方运行时依赖,测试直接使用 `node:test`
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone git@github.com:adaex/cron-up.git
|
|
11
|
+
cd cron-up
|
|
12
|
+
node --test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
CI 在 macOS 上分别使用 Node 22、24 运行全部测试。需要本机联调时可执行 `npm link`,它会把开发版链接成全局 `cron-up` 命令;只做测试时不需要这一步
|
|
16
|
+
|
|
17
|
+
## 代码结构
|
|
18
|
+
|
|
19
|
+
- `bin/cron-up.mjs`:CLI 入口与顶层异常处理
|
|
20
|
+
- `src/cli.mjs`、`src/args.mjs`:参数解析、帮助和命令分发
|
|
21
|
+
- `src/internals.mjs`:可替换的 `deps` 容器及系统级默认实现
|
|
22
|
+
- `src/` 其他模块:任务发现、cron、巡检、会话、登记、展示、日志与 launchd 服务
|
|
23
|
+
- `test/`、`test-support/`:行为测试和可注入依赖的夹具
|
|
24
|
+
|
|
25
|
+
业务模块向 `deps` 注册外部依赖,测试通过替换这些依赖隔离文件、进程、时钟和 launchd。修改行为时同步修改测试,不要只让旧断言适配新输出
|
|
26
|
+
|
|
27
|
+
## 文档同步要求
|
|
28
|
+
|
|
29
|
+
用户可观察行为变化时,至少检查以下位置,避免命令、README 和实现各自描述一套口径:
|
|
30
|
+
|
|
31
|
+
| 变化 | 需要同步的位置 |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| 产品范围、安装、日常使用、限制、卸载或数据保留 | `README.md` |
|
|
34
|
+
| 命令、选项、升级步骤或帮助文案 | `src/cli.mjs`,必要时包括 `src/overview.mjs` |
|
|
35
|
+
| npm 包定位 | `package.json` 的 `description` |
|
|
36
|
+
| 内部机制、生命周期常量、兼容性证据或投递算法 | `docs/internals.md` |
|
|
37
|
+
| 跨版本行为与旧安装收尾 | `docs/migration.md` |
|
|
38
|
+
| 版本号、工作流或发布验收 | `docs/release.md` |
|
|
39
|
+
|
|
40
|
+
维护文档中的常量表只写语义和派生关系,具体数值链接 `src/constants.mjs`,避免同时维护两份当前值;README 中面向用户的数值仍需随行为变化核对
|
|
41
|
+
|
|
42
|
+
文档中的兼容版本必须有实际核对依据。没有把某个版本验证过,就写“未验证”,不要用“以上均支持”替代证据
|
|
43
|
+
|
|
44
|
+
示例输出应与真实命令格式核对;时间、pid 等变化值可以标注为示例,不要伪造验证结果
|
|
45
|
+
|
|
46
|
+
## 提交前检查
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node --test
|
|
50
|
+
node bin/cron-up.mjs --help
|
|
51
|
+
npm pack --dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
同时检查:
|
|
55
|
+
|
|
56
|
+
- README 与分拆文档的相对链接有效
|
|
57
|
+
- 新命令或字段有对应测试
|
|
58
|
+
- 文档没有引入“保证一个不少”“绝不可能失败”等超过实现边界的承诺
|
|
59
|
+
- 涉及任务文件写入、权限、联网或卸载时,明确说明影响范围和不会处理的内容
|
|
60
|
+
|
|
61
|
+
发布是独立流程,见[发布流程](docs/release.md)
|
package/README.md
CHANGED
|
@@ -1,191 +1,307 @@
|
|
|
1
1
|
# cron-up
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
为 **Claude Code 的 durable 定时任务**在触发前按需预热交互会话,减少因原会话关闭、电脑上没有在线会话而造成的漏跑。每个需要 cron-up 预热且能唯一识别的任务使用独立后台会话,任务仍由 Claude Code 自己投递和执行
|
|
4
|
+
|
|
5
|
+
目前仅支持 macOS,依赖 launchd、`script(1)` 和 zsh;不是通用命令行 AI agent 的调度器。会话按任务需要创建,满足回收条件后退出,短周期任务也可能持续复用现有会话
|
|
4
6
|
|
|
5
7
|
> [!WARNING]
|
|
6
|
-
> cron-up
|
|
8
|
+
> cron-up 依赖 Claude Code 未公开且无稳定性承诺的内部实现。当前核心行为以 **2.1.302** 的本机核对结果为依据,其他版本不代表已经验证;不同机制的核对版本见[内部机制与兼容性](docs/internals.md)。升级 Claude Code 后,先运行 `cron-up` 查看告警,再确认任务能正常预热和投递
|
|
7
9
|
|
|
8
10
|
## 它解决什么问题
|
|
9
11
|
|
|
10
|
-
在 Claude Code
|
|
12
|
+
在 Claude Code 中用 CronCreate 创建 `durable: true` 的任务后,任务记录在当前工作目录的 `.claude/scheduled_tasks.json` 中。任务到点时,该目录需要有正在运行的交互 `claude` 会话:
|
|
13
|
+
|
|
14
|
+
- 原创建者会话还开着时,任务优先投递给它,cron-up 不为其预热或改写归属
|
|
15
|
+
- 原创建者会话已经关闭时,cron-up 在任务进入预热窗口后尝试启动专属后台会话,并改写该任务的归属字段
|
|
16
|
+
- 电脑休眠期间错过的周期任务不会补执行;一次性任务的补执行见[任务时间与执行](#任务时间与执行)
|
|
17
|
+
|
|
18
|
+
职责边界如下:
|
|
19
|
+
|
|
20
|
+
| 环节 | 负责方 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| 创建和保存任务 | Claude Code 的 CronCreate 工具 |
|
|
23
|
+
| 判断触发时刻、投递 prompt、执行任务 | Claude Code 交互会话 |
|
|
24
|
+
| 提前准备会话、调整任务归属、回收会话 | cron-up |
|
|
25
|
+
| 定时驱动巡检 | macOS launchd |
|
|
26
|
+
|
|
27
|
+
cron-up 不负责创作任务内容,也不能保证每个任务都执行成功。授权、模型、工具、任务脚本和上游调度仍可能失败
|
|
28
|
+
|
|
29
|
+
## 安装前须知
|
|
30
|
+
|
|
31
|
+
### 环境与兼容性
|
|
32
|
+
|
|
33
|
+
- **macOS**:使用 launchd、`script(1)` 和 zsh,暂不支持其他系统
|
|
34
|
+
- **Node.js 22 或更高版本**:cron-up 没有第三方运行时依赖
|
|
35
|
+
- **Claude Code**:核心行为已在 2.1.302 核对;更低或更高版本均应按未验证版本对待,不要仅凭版本号推断兼容
|
|
36
|
+
- **可用的 `claude` 命令**:已完成登录和所需配置,并且能从交互登录 zsh 中启动;可先用 `zsh -lic 'command -v claude'` 检查
|
|
37
|
+
- **目录信任与工具授权**:先在该工作目录手动启动一次 Claude Code,完成信任确认及无法在无人值守时回答的授权,否则预热可能停在提问界面,随后被判为卡死并进入失败流程
|
|
11
38
|
|
|
12
|
-
|
|
39
|
+
fnm、volta、Homebrew、官网 pkg 等 Node 安装方式均可用于提供 `node`。nvm 等把 Node 放在版本目录中的安装方式,在切换或删除版本后需要重新运行 `cron-up install`;这里说的是 Node 的启动链,不代表所有 Claude Code 安装和 shell 配置都经过验证
|
|
13
40
|
|
|
14
|
-
|
|
41
|
+
### 安全与自动修改
|
|
15
42
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
3. 本进程是任务记录的创建者进程(resume 形态)→ 投;
|
|
19
|
-
4. 其余情况 → 必须持锁、任务属于本项目、且创建者进程已死,才接管。
|
|
43
|
+
> [!CAUTION]
|
|
44
|
+
> `roots` 是**任务扫描与信任范围,不是沙箱**。后台会话通过 `claude --permission-mode bypassPermissions` 跳过工具权限确认,并以当前 macOS 用户身份运行;任务可以访问 roots 之外的位置,也可能通过模型、MCP 或任务工具联网
|
|
20
45
|
|
|
21
|
-
|
|
46
|
+
只扫描你信任其**整个工作区内容**的目录,而不只是检查任务文件。预热会在工作区启动完整的 Claude Code,任务、`CLAUDE.md`、`.claude` 中的项目指令与配置,以及任务可能调用的脚本,都可能影响无人值守会话
|
|
22
47
|
|
|
23
|
-
|
|
24
|
-
|---|---|
|
|
25
|
-
| 任务登记 | Claude Code 原生命令 CronCreate,写入工作目录下 `.claude` |
|
|
26
|
-
| 定时执行 | Claude Code 交互会话自身(包括补执行和抖动) |
|
|
27
|
-
| 保证每个窗口内死主任务有专属会话在场 | cron-up(按任务拉起、盖印、执行完回收) |
|
|
28
|
-
| 安装与升级 | npm:`~/Library/LaunchAgents/local.cron-up.plist` 与 `~/.local/share/cron-up/cron-up-service` |
|
|
48
|
+
目录归你所有并不等于内容可信:clone 到 roots 下的外部仓库、团队共享仓库或他人可写目录,都可能带来由他人编写的任务和项目指令,不能只审核 `.claude/scheduled_tasks.json`
|
|
29
49
|
|
|
30
|
-
|
|
50
|
+
安装后,每轮巡检会按需自动维护扫描到的任务文件:
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
1. 给所有显式周期任务补 `permanent: true`,使其绕开 Claude Code 的 7 天周期任务回收
|
|
53
|
+
2. 对已经进入预热窗口、原归属会话不在场的任务,改写 `createdBySessionId`、`createdByPid` 和 `createdByProcStart`
|
|
54
|
+
|
|
55
|
+
这些行为没有任务级开关。即使原创建者会话仍在线,自动续期仍会进行;手动删除 `permanent` 后,只要任务仍在扫描范围内,后续巡检还会补回。不希望某个工作区被修改时,应把它移出 roots;已经写入的字段不会自动恢复,详见[卸载与数据保留](#卸载与数据保留)
|
|
56
|
+
|
|
57
|
+
## 快速开始
|
|
58
|
+
|
|
59
|
+
### 1. 安装并选择扫描范围
|
|
33
60
|
|
|
34
61
|
```bash
|
|
35
62
|
npm i -g cron-up
|
|
36
63
|
cron-up install --roots ~/code,~/work
|
|
37
64
|
```
|
|
38
65
|
|
|
39
|
-
|
|
66
|
+
`--roots` 接受逗号分隔的目录列表,每次提供的是完整列表,不是在原列表上追加。第二步会写入配置、启动脚本和 LaunchAgent,并立即尝试首轮巡检
|
|
67
|
+
|
|
68
|
+
不带 `--roots` 时会沿用可读取且 roots 类型有效的现有配置;首次安装使用默认范围 `~/space`、`~/workspace`、`~/tasks`,不存在的目录会跳过。使用默认范围前,同样要确认其中现在及未来出现的任务和项目指令都可信
|
|
69
|
+
|
|
70
|
+
现有配置无法解读时也会回退默认范围;其写入风险和修复方法见[配置无法解读时](#配置无法解读时)
|
|
71
|
+
|
|
72
|
+
扫描范围按下面的路径深度计算:
|
|
73
|
+
|
|
74
|
+
- root 本身,以及 root 下的第 1、2、3 层目录都会检查任务文件
|
|
75
|
+
- 例如 `<root>/<repo>/<subdir>/<nested>` 是 root 下第 3 层,可以发现;再深一层不会发现
|
|
76
|
+
- 不深入符号链接形式的子目录,并跳过依赖目录和常见系统目录,完整名单见 `src/constants.mjs` 中的 `PRUNE_DIRS`
|
|
77
|
+
|
|
78
|
+
工作区指**包含 `.claude/scheduled_tasks.json` 的那个精确目录**,不自动等同于 Git 仓库根目录。若任务是在仓库子目录的 Claude Code 会话里创建,它属于该子目录
|
|
40
79
|
|
|
41
|
-
|
|
80
|
+
### 2. 创建第一个任务
|
|
42
81
|
|
|
43
|
-
|
|
82
|
+
进入 roots 覆盖的工作目录并启动 Claude Code,然后在 Claude Code 中提出请求,例如:
|
|
44
83
|
|
|
45
|
-
|
|
84
|
+
> 在当前工作目录用 CronCreate 创建一个 `durable: true` 的一次性任务,约 5 分钟后在会话里回复“cron-up 验证成功”;完成后执行 `/exit` 退出会话
|
|
46
85
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
86
|
+
CronCreate 是 Claude Code 内部工具,不是在终端里直接执行的 shell 命令。推荐让无人值守任务在完成并保存结果后执行 `/exit`,但是否执行该指令仍取决于任务本身
|
|
87
|
+
|
|
88
|
+
### 3. 验证发现和预热
|
|
89
|
+
|
|
90
|
+
依次用 `cron-up list` 确认任务已被发现并查看时间,用 `cron-up run` 立即巡检一次,再用 `cron-up` 查看服务、会话和告警。需要查看后台界面时,运行 `cron-up logs <目录名或任务id>`;完整命令和日志选项见[日常使用](#日常使用)
|
|
91
|
+
|
|
92
|
+
定时巡检每 5 分钟一轮。周期任务在设定时间前 6 分钟进入预热窗口;所有一次性任务都统一再增加 90 秒前瞻,最早可在设定时间前 7.5 分钟进入窗口。实际启动由窗口内的巡检触发,具体时间取决于巡检相位,并不是固定提前 6 或 7.5 分钟
|
|
93
|
+
|
|
94
|
+
其中,落在 `:00` 或 `:30` 的一次性任务按当前上游规则还可能提前至多 90 秒投递
|
|
95
|
+
|
|
96
|
+
任务还没到预热窗口时,`cron-up run` 不会为它提前常驻一个会话;若已经有一轮巡检在运行,后到的一轮会跳过
|
|
52
97
|
|
|
53
98
|
## 日常使用
|
|
54
99
|
|
|
55
100
|
```bash
|
|
56
|
-
cron-up #
|
|
57
|
-
cron-up list #
|
|
58
|
-
cron-up run #
|
|
59
|
-
cron-up logs #
|
|
60
|
-
cron-up logs team-space #
|
|
101
|
+
cron-up # 总览:服务、配置、最近任务、预热会话与告警
|
|
102
|
+
cron-up list # 逐任务列出时间、类型、摘要与预热状态
|
|
103
|
+
cron-up run # 立即手动巡检一次
|
|
104
|
+
cron-up logs # 查看最近 100 行巡检日志
|
|
105
|
+
cron-up logs team-space # 按文件名片段选择后台会话日志
|
|
61
106
|
cron-up logs team-space--00000002 -f
|
|
62
|
-
cron-up
|
|
107
|
+
cron-up --help # 查看命令帮助
|
|
63
108
|
```
|
|
64
109
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
### 自动维护(每轮无条件进行,没有开关)
|
|
110
|
+
会话日志按文件名片段匹配,可以输入目录名、任务 id 或两者组合;命中多个日志时会列出候选,请补充更长的片段
|
|
68
111
|
|
|
69
|
-
|
|
112
|
+
### 看懂总览和任务列表
|
|
70
113
|
|
|
71
|
-
|
|
72
|
-
- **自动任务级盖印**:把窗口内死主任务的三个归属字段盖成各自预热会话的身份。只改 `createdBySessionId/createdByPid/createdByProcStart`,不碰 `prompt`、`cron`、`id` 与 `createdInProject`;内容相同但 id 不同的任务仍逐条定位,不会共用盖印目标。
|
|
114
|
+
总览优先看三处:
|
|
73
115
|
|
|
74
|
-
|
|
116
|
+
- **服务与上次巡检**:launchd 已加载不等于巡检仍在成功运行,心跳可以帮助识别启动链失效
|
|
117
|
+
- **最近**:按预计投递时间排序,而不是只按 cron 的设定时间排序
|
|
118
|
+
- **需要留意**:配置、启动链、会话登记、预热失败和失去扫描范围的会话等告警
|
|
75
119
|
|
|
76
|
-
|
|
120
|
+
下面是 `cron-up list` 的输出格式示例,日期、时间和 pid 以本机实际输出为准:
|
|
77
121
|
|
|
78
|
-
|
|
122
|
+
```text
|
|
123
|
+
共 1 个工作区、2 个定时任务
|
|
79
124
|
|
|
80
|
-
|
|
125
|
+
/Users/you/code/my-project
|
|
126
|
+
10-24 19:52 一次性 52 19 24 10 * 验证 cron-up [预热中 12345]
|
|
127
|
+
10-25 09:00 → 09:30 周期·永久 0 9 * * * 每日汇总
|
|
128
|
+
```
|
|
81
129
|
|
|
82
|
-
|
|
130
|
+
常见状态和标签:
|
|
83
131
|
|
|
84
|
-
|
|
132
|
+
| 显示 | 含义 |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `设定时间 → 预计时间` | 前者是 cron 设定时间,后者是按已知上游规则估算的投递时间;没有偏差时只显示一个时间 |
|
|
135
|
+
| `启动中 <pid>` | 会话进程已启动,尚未在 Claude Code 会话登记中确认 |
|
|
136
|
+
| `预热中 <pid>` | 已确认后台会话登记且预热进程存活;归属是否已写回可结合巡检日志判断 |
|
|
137
|
+
| `待预热` | 已进入预热窗口,但当前还没有承接会话;结合总览告警判断原因 |
|
|
138
|
+
| `冷却中` / `失败N次` | 预热启动失败,正在退避或已经累计失败 |
|
|
139
|
+
| `终端接管` | 用户通过 `--resume` 接管了该会话,cron-up 不再另拉或结束它 |
|
|
140
|
+
| 标签为空 | 可能还没到预热窗口,也可能原创建者会话仍在线,需要结合时间列和总览判断 |
|
|
141
|
+
| `已错过` | 一次性任务的设定时间已经过去;有效 `createdAt` 是 cron-up 判断错过待补的必要字段 |
|
|
142
|
+
| `一年内无` | cron 可解析,但 366 天展示窗口内没有未来落点;二月三十日这类日历上永远不出现的表达式也归入此状态 |
|
|
143
|
+
| `cron 无效` | 表达式解析失败,或字段值域、区间导致合法值集合为空,巡检会跳过该任务 |
|
|
85
144
|
|
|
86
|
-
|
|
145
|
+
## 任务时间与执行
|
|
87
146
|
|
|
88
|
-
|
|
147
|
+
文档和命令输出中的时间均为 macOS 本机时间,cron-up 也用本地时区计算预热和展示。三个时间概念不要混用:
|
|
89
148
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- **孤儿认领**:state 记录丢失时,巡检能按会话日志路径从进程表里把仍在运行的预热重新认回来。
|
|
94
|
-
- **升级迁移**:从目录级单预热的旧版本升级时,首轮巡检会结束能核实身份的旧版预热,之后按任务重新拉起。缺少进程启动指纹的旧条目不会被贸然发信号;若对应进程仍在运行,巡检会明确告警,需人工核查,不会假装已经回收。
|
|
149
|
+
1. **设定时间**:cron 表达式对应的分钟
|
|
150
|
+
2. **预计投递时间**:计入已知上游抖动规则后的估算,可能早于或晚于设定时间
|
|
151
|
+
3. **实际执行时间**:Claude Code 会话实际收到并开始处理任务的时间,还受秒级轮询、会话状态和上游实现影响
|
|
95
152
|
|
|
96
|
-
|
|
153
|
+
周期任务通常在设定时间之后投递,每日任务使用原生随机 id 时经常接近固定的 30 分钟上限;一次性任务落在 `:00` 或 `:30` 时可能提前至多 90 秒。约 5 分钟周期且匹配特定表达式的上游任务还可能走提前 15 秒的分支,当前预测没有复刻该分支,完整规则和例外见[投递时间算法](docs/internals.md#投递时间算法)
|
|
97
154
|
|
|
98
|
-
|
|
155
|
+
因此,“设定时间到了但还没执行”不一定是故障,应先看 `cron-up list` 的预计时间和总览告警
|
|
99
156
|
|
|
100
|
-
|
|
101
|
-
claude --resume <sessionId> # 或 claude -r 在列表中选择
|
|
102
|
-
```
|
|
157
|
+
电脑休眠期间,launchd 醒来后只补跑一轮巡检:
|
|
103
158
|
|
|
104
|
-
|
|
159
|
+
- 错过的周期任务不会补执行
|
|
160
|
+
- 错过的一次性任务通常会在后台会话启动时由 Claude Code 补执行;手写任务缺少有效 `createdAt` 时,cron-up 无法据此识别错过待补
|
|
105
161
|
|
|
106
|
-
##
|
|
162
|
+
## 会话生命周期与恢复
|
|
107
163
|
|
|
108
|
-
|
|
164
|
+
预热会话按任务需要创建,周期任务不依靠跨周期累积上下文。复用与回收只在巡检轮次中按启发式条件判断,不等于对任务完成状态做强保证:
|
|
109
165
|
|
|
110
|
-
|
|
166
|
+
- 周期任务的预热窗口从设定时间前 6 分钟开始,一次性任务最早从设定时间前 7.5 分钟开始;具体启动时间取决于窗口内的巡检相位
|
|
167
|
+
- 新会话启动后,当轮每 3 秒重扫一次登记,最多等待 24 秒;这只是加速归属写回,不是启动判死时限
|
|
168
|
+
- 进程超过 3 分钟仍未完成登记时,由后续巡检判定卡死并终止;因为巡检每 5 分钟一轮,实际处置时间不是精确的启动后第 3 分钟
|
|
169
|
+
- 没有成功登记的启动连续失败 3 次后,该任务暂停重试 30 分钟;按任务分别计数,成功登记会清零,已死亡条目的失败记录跨预热窗口保留并在 48 小时后老化
|
|
170
|
+
- 回收除了要求界面静默超过 3 分钟,还要求距该任务下次需要超过 7 分钟;同样只在巡检时判断
|
|
171
|
+
- 距下次需要不超过 7 分钟时会继续复用现有会话,例如每 5 分钟执行的任务通常无法满足回收余量,其会话可能长期在线,不能按“窗口外没有会话”估算内存
|
|
172
|
+
- 同分钟大量任务会同时拉起多个 Claude Code 会话,每个会话可能占用数百 MB,实际以本机为准;正常回收后不再保留这些预热会话的内存开销
|
|
111
173
|
|
|
112
|
-
|
|
174
|
+
后台会话与普通会话一样保存对话记录,可以在终端恢复:
|
|
113
175
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- **会话日志可能包含任务输出**:后台会话的界面记录写到 `~/Library/Logs/cron-up/sessions/<工作区>--<任务id>.log`,权限 600、目录 700,每个任务独立文件、轮转保留一代;巡检日志在 `~/Library/Logs/cron-up/`。
|
|
118
|
-
- **扫描不深入符号链接的子目录**,默认还排除系统目录与依赖目录。
|
|
119
|
-
- **巡检、预热与你的 claude 会话之间不发起网络请求**,也不依赖 npm 注册表;网络活动全部来自被启动的会话本身。唯一的网络发生在安装/升级时访问 npm registry 拉包。
|
|
176
|
+
```bash
|
|
177
|
+
claude --resume <sessionId> # 或 claude -r 在列表中选择
|
|
178
|
+
```
|
|
120
179
|
|
|
121
|
-
|
|
180
|
+
对话记录位于 `~/.claude/projects/<目录编码>/<会话ID>.jsonl`。如果恢复后原后台进程已经退出、同一 sessionId 仍在终端运行,cron-up 会停止管理它,不结束终端进程;该终端会话关闭后,后续巡检可重新预热
|
|
122
181
|
|
|
123
|
-
|
|
124
|
-
- 每个窗口内的死主任务对应一个后台会话:同一分钟内有大量任务同时到期时,会同时存在相应数量的会话(每个数百 MB 内存)。时间错开的日常任务只在各自的 6 分钟窗口内占用(一次性任务至多 7.5 分钟)。
|
|
125
|
-
- 手工创建的任务若缺少 `createdAt`,错过补执行判定不生效。`cron-up list` 对缺字段的手写任务仍显示「已错过/设定时间」,但预热与补执行依赖 Claude Code 与 cron-up 的字段纪律。
|
|
126
|
-
- 工作区从巡检视野里消失(`.claude/scheduled_tasks.json` 被整个删除或移出 roots)后,仍在运行的预热不会被回收,总览会列出这类会话,不需要时手动结束。
|
|
127
|
-
- 任务文件的 mtime 乐观检测不是跨进程 CAS:另一个写者若恰好赶在最后一次检查之后、原子替换之前写入,仍可能被覆盖。这个窗口无法由 cron-up 单方面彻底关闭;需要绝对保证时,应由上游提供共享写锁或公开写接口。
|
|
128
|
-
- 预热依据的逐任务谓词、调度锁与三代进程结构是未公开内部实现:新版若调整归属/锁语义,任务级隔离的前提需要重新核对。cron-up 保留了会话登记格式的烟雾告警,格式漂移时在总览页明示。
|
|
182
|
+
## 自动维护的写入边界
|
|
129
183
|
|
|
130
|
-
|
|
184
|
+
同一轮中,同一批待处理的自动续期和归属改写会合并为带 mtime 乐观检测的原子写入;新会话在后续完成登记、需要补写归属时,可能另做一次原子写入。若检测到 Claude Code 同时更新了 `lastFiredAt`、删除了一次性任务或进行了其他修改,本轮对应写入会暂缓并在后续重试
|
|
131
185
|
|
|
132
|
-
|
|
133
|
-
|---|---|
|
|
134
|
-
| 总览提示「冷却中」 | 运行 `cron-up logs <目录>--<任务id>` 查看启动界面:停在信任确认就先在该目录手动走完信任;shell 初始化缺命令(command not found)按「启动脚本 PATH」段修 |
|
|
135
|
-
| 任务到点没有执行 | `cron-up list` 看任务与预热状态;预热在则看它的会话日志;总览的「需要留意」按任务给出原因 |
|
|
136
|
-
| 总览提示「启动脚本已失效 / PATH 找不到 node」 | 重跑 `cron-up install`(见下) |
|
|
137
|
-
| 总览提示「巡检已沉默」 | launchd 显示加载但心跳超时:先 `cron-up logs`,再重跑 install |
|
|
138
|
-
| `last exit code=2` | 配置损坏或字段类型不对:修好 `~/.local/share/cron-up/config.json`(只有 roots 一个字段)或重跑 install |
|
|
139
|
-
| 手动 `cron-up run` 正常、定时执行不正常 | 手动测试继承终端 PATH,定时由启动脚本驱动;换过 Node 安装方式后重跑 install |
|
|
140
|
-
| 任务表显示「已错过」 | 一次性任务落点已过:后台会话一启动就会补执行;周期任务没有补执行 |
|
|
141
|
-
| 日志里出现「卡死」 | 会话 3 分钟没完成登记:信任确认/工具授权提问没人应答,手动进目录走一遍授权 |
|
|
142
|
-
| 同目录多个任务互相干扰 | 升级后首轮会结束旧版目录级预热;如仍异常,确认所有预热会话都由新版本拉起(日志名含 `--任务id`) |
|
|
186
|
+
这只是降低并发覆盖概率,不是与 Claude Code 共用的互斥锁。最后一次检查与原子替换之间仍存在极小竞态,详见[内部机制](docs/internals.md#任务文件维护与并发)
|
|
143
187
|
|
|
144
|
-
|
|
188
|
+
卸载 cron-up 不会撤销这些已经写入任务文件的修改
|
|
145
189
|
|
|
146
|
-
|
|
147
|
-
- **升级**:`npm i -g cron-up@latest` 后重跑一次 `cron-up install`。下一轮巡检自然跑新版;Node 版本与包文件在 install 后自动跟随。fnm 切换默认版本同理。
|
|
148
|
-
- **版本号**:总览首行、心跳文件都带版本;发版收尾跑一次无参数 `cron-up` 确认首行版本、服务已加载且退出码 0、「需要留意」为空。
|
|
190
|
+
## 配置与产物
|
|
149
191
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
配置只有扫描范围一个字段:`~/.local/share/cron-up/config.json`
|
|
192
|
+
配置文件为 `~/.local/share/cron-up/config.json`,只有扫描范围一个字段:
|
|
153
193
|
|
|
154
194
|
```json
|
|
155
195
|
{ "roots": ["/Users/you/code", "/Users/you/work"] }
|
|
156
196
|
```
|
|
157
197
|
|
|
158
|
-
|
|
198
|
+
可以直接编辑 roots,通常最多等下一轮巡检生效;也可以运行:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
cron-up install --roots "$HOME/code,$HOME/work"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
重跑 install 会重写配置、启动脚本与 LaunchAgent,并移除旧版本遗留的废弃配置字段
|
|
205
|
+
|
|
206
|
+
巡检间隔、提前量、扫描深度、失败退避等均为固定行为,完整常量和生命周期规则见[内部机制](docs/internals.md#巡检与会话生命周期)
|
|
159
207
|
|
|
160
208
|
| 产物 | 位置 |
|
|
161
|
-
|
|
162
|
-
| 命令行与代码 | npm
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| 命令行与代码 | npm 全局目录,具体位置取决于 Node 与 npm 安装方式 |
|
|
163
211
|
| 配置文件 | `~/.local/share/cron-up/config.json` |
|
|
164
|
-
| 运行状态 |
|
|
165
|
-
|
|
|
212
|
+
| 运行状态 | `~/.local/share/cron-up/state.json`,按工作区与任务 id 记录预热 |
|
|
213
|
+
| 巡检心跳 | `~/.local/share/cron-up/heartbeat`,每轮成功完成后写入 |
|
|
166
214
|
| LaunchAgent | `~/Library/LaunchAgents/local.cron-up.plist` |
|
|
167
215
|
| 启动脚本 | `~/.local/share/cron-up/cron-up-service` |
|
|
168
216
|
| 巡检日志 | `~/Library/Logs/cron-up/launchd.{out,err}.log` |
|
|
169
|
-
| 各任务会话日志 | `~/Library/Logs/cron-up/sessions
|
|
217
|
+
| 各任务会话日志 | `~/Library/Logs/cron-up/sessions/<工作区折名>--<任务id>.log`,轮转保留一代 `.1` |
|
|
218
|
+
| Claude Code 对话记录 | `~/.claude/projects/<目录编码>/<会话ID>.jsonl`,由 Claude Code 管理 |
|
|
219
|
+
|
|
220
|
+
会话日志可能包含任务输出,文件权限为 0600、目录权限为 0700。日志文件名中的工作区折名由完整工作区路径生成,日常使用建议通过 `cron-up logs` 查找,不要手工猜文件名
|
|
221
|
+
|
|
222
|
+
cron-up 自身的巡检和会话管理不主动访问 npm registry 或其他网络服务;安装和升级时由 npm 访问 registry。被启动的 Claude Code 会话仍可能联网,cron-up 不对其网络活动作限制
|
|
170
223
|
|
|
171
|
-
|
|
224
|
+
### 配置无法解读时
|
|
225
|
+
|
|
226
|
+
不带 roots 重跑 install 时,只有可读取且 roots 类型有效的现有配置才会被沿用;配置无法解读时会提示并回退到默认范围 `~/space`、`~/workspace`、`~/tasks`,随后立即尝试首轮巡检,可能续期或改写其中的任务文件
|
|
227
|
+
|
|
228
|
+
修复时可以直接改正上述配置文件;需要通过 install 重建且不想使用默认范围时,必须显式传入完整的 `--roots`
|
|
229
|
+
|
|
230
|
+
## 升级与启动链维护
|
|
231
|
+
|
|
232
|
+
升级 cron-up:
|
|
172
233
|
|
|
173
234
|
```bash
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
235
|
+
npm i -g cron-up@latest --prefer-online
|
|
236
|
+
cron-up install
|
|
237
|
+
cron-up
|
|
177
238
|
```
|
|
178
239
|
|
|
179
|
-
|
|
240
|
+
最后一步确认首行版本、launchd 状态、上次退出码和“需要留意”。刚发布新版本时 registry 与本机缓存可能有延迟,维护者发版后的逐机升级使用显式版本号,见[发布后的逐机升级与验收](docs/release.md#每次发版后的逐机升级与验收)
|
|
241
|
+
|
|
242
|
+
launchd 的初始 PATH 很小,因此 install 会把 `node` 与 `cron-up` 所在 bin 目录写入 `~/.local/share/cron-up/cron-up-service`。预热中的 `claude` 则由 `/bin/zsh -lic` 启动,依赖登录交互 shell 的配置,而不是仅靠前述两个目录
|
|
243
|
+
|
|
244
|
+
出现以下情况后应重跑 `cron-up install`:
|
|
245
|
+
|
|
246
|
+
- 切换、删除或更换 Node 安装方式
|
|
247
|
+
- 修改 roots 或需要重建 LaunchAgent
|
|
248
|
+
- 总览提示启动脚本失效、PATH 找不到 node/cron-up,或巡检心跳沉默
|
|
249
|
+
|
|
250
|
+
若 `zsh -lic 'command -v claude'` 失败,先修复交互 zsh 中的 `claude` 命令及其环境,再重跑 install。手动 `cron-up run` 成功而 launchd 失败时,也应优先比较这两种环境的差异
|
|
251
|
+
|
|
252
|
+
## 卸载与数据保留
|
|
253
|
+
|
|
254
|
+
`cron-up uninstall` 管理的是本机服务和 cron-up 自有产物,不等于删除所有相关数据:
|
|
255
|
+
|
|
256
|
+
| 操作或数据 | `cron-up uninstall` | `cron-up uninstall --purge` |
|
|
257
|
+
| --- | --- | --- |
|
|
258
|
+
| 停止仍可安全跟踪的预热会话 | 是 | 是 |
|
|
259
|
+
| 移除 LaunchAgent 和 plist | 是 | 是 |
|
|
260
|
+
| 删除 `~/.local/share/cron-up` 中的配置、状态、心跳和启动脚本 | 否 | 是 |
|
|
261
|
+
| 删除 `~/Library/Logs/cron-up` 中的巡检与会话日志 | 否 | 是 |
|
|
262
|
+
| 卸载 npm 全局 `cron-up` 命令 | 否 | 否 |
|
|
263
|
+
| 删除 Claude Code 任务或 `~/.claude/projects` 对话记录 | 否 | 否 |
|
|
264
|
+
| 恢复任务文件中的 `permanent`、`createdBy*` 字段 | 否 | 否 |
|
|
265
|
+
|
|
266
|
+
完全移除命令行和 cron-up 自有产物:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
cron-up uninstall --purge
|
|
270
|
+
npm uninstall -g cron-up
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Claude Code 任务和对话记录需要在 Claude Code 侧另行处理。卸载后任务文件仍然存在,但 cron-up 不再提供预热、续期或会话回收;不要把这个状态理解为任务字段已经恢复原状
|
|
274
|
+
|
|
275
|
+
## 故障排查
|
|
276
|
+
|
|
277
|
+
| 现象 | 排查方法 |
|
|
278
|
+
| --- | --- |
|
|
279
|
+
| 总览提示“冷却中” | 运行 `cron-up logs <目录名>--<任务id>` 查看启动界面;常见原因是目录信任、工具授权、登录状态或 shell 环境,冷却结束不代表问题已经修复 |
|
|
280
|
+
| 设定时间到了但没有执行 | 先看 `cron-up list` 的预计投递时间,排除上游抖动;再看总览告警和对应会话日志,并确认当时电脑没有休眠 |
|
|
281
|
+
| 总览提示“启动脚本已失效 / PATH 找不到 node” | 按[升级与启动链维护](#升级与启动链维护)检查并重跑 `cron-up install` |
|
|
282
|
+
| 总览提示“巡检已沉默” | 先运行 `cron-up logs` 查看巡检和 stderr 提示,再检查启动链并重跑 install |
|
|
283
|
+
| `last exit code=2` | 通常是配置损坏或 roots 类型不对;直接修复 `~/.local/share/cron-up/config.json`,或按[配置无法解读时](#配置无法解读时)的说明显式传入完整 roots 重跑 install |
|
|
284
|
+
| 手动 `cron-up run` 正常、定时执行不正常 | 比较终端与 launchd/`zsh -lic` 的 PATH、Node 和 `claude` 环境;换过 Node 安装方式后重跑 install |
|
|
285
|
+
| 任务表显示“已错过” | 对一次性任务检查后台会话是否启动及其日志;手写任务缺少有效 `createdAt` 时,cron-up 不会按错过待补处理 |
|
|
286
|
+
| 日志里出现“卡死” | 表示超过登记宽限后由巡检终止,常见于人工作答界面,也可能是会话登记或启动形态变化;先看会话日志和总览的登记格式告警 |
|
|
287
|
+
| 从旧版升级后仍有目录级预热 | 查看[旧版迁移](docs/migration.md),核实是否因缺少进程指纹而需要人工处理 |
|
|
288
|
+
|
|
289
|
+
## 已知限制
|
|
290
|
+
|
|
291
|
+
- 仅支持 macOS,且依赖未公开的 Claude Code 内部实现;上游升级可能改变任务归属、登记格式、进程结构和投递算法
|
|
292
|
+
- 电脑休眠期间错过的周期任务不会补执行;一次性任务的补执行依赖上游机制和必要任务字段
|
|
293
|
+
- 每个进入窗口且原归属不在场的任务对应一个会话,同分钟大量任务可能带来明显内存压力
|
|
294
|
+
- 手写或第三方生成的任务文件不是推荐入口;任务应有唯一 id、显式 `recurring` 和符合上游语义的字段,一次性任务还需要毫秒时间戳 `createdAt` 才能由 cron-up 判断错过待补
|
|
295
|
+
- 工作区的任务文件被整体删除或目录移出 roots 后,仍在运行的预热可能不再被回收;总览会列出能识别的此类会话,需要人工处理
|
|
296
|
+
- mtime 乐观检测不能完全关闭跨进程写入竞态;需要绝对写入保证时,应由上游提供共享锁或公开写接口
|
|
297
|
+
- 即使预热和投递正常,任务仍可能因模型、工具、权限、网络或 prompt 本身而失败
|
|
180
298
|
|
|
181
|
-
##
|
|
299
|
+
## 更多文档
|
|
182
300
|
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
- **npm 传播延迟**:publish 成功后到 registry 可查通常两分钟、偶发七八分钟(带 provenance 要等透明日志与后端入库),本机元数据缓存还会再滞后,直接 `npm i -g cron-up` 可能装回旧版且毫无提示——用显式版本号加 `--prefer-online` 安装(`npm i -g cron-up@<新版本> --prefer-online`),装完以总览首行版本号为准。
|
|
188
|
-
- **trusted publishing**:npm 包设置页里已登记 GitHub 仓库(repository `adaex/cron-up`,workflow `release.yml`),一次性配置,没了见 Trusted Publisher 文档。
|
|
301
|
+
- [内部机制与兼容性](docs/internals.md):任务归属、生命周期常量、投递算法、并发与版本证据
|
|
302
|
+
- [旧版迁移](docs/migration.md):从 26.10.30 及更早版本升级
|
|
303
|
+
- [贡献指南](CONTRIBUTING.md):开发、测试和文档同步要求
|
|
304
|
+
- [发布流程](docs/release.md):版本号、签名 tag、trusted publishing 与逐机升级
|
|
189
305
|
|
|
190
306
|
## License
|
|
191
307
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# 内部机制与兼容性
|
|
2
|
+
|
|
3
|
+
本文面向需要排查兼容问题或修改实现的维护者。这里记录的是当前实现依据和行为边界,不是 Claude Code 的公开契约;普通使用从 [README](../README.md) 开始
|
|
4
|
+
|
|
5
|
+
## 版本证据与验证口径
|
|
6
|
+
|
|
7
|
+
cron-up 依赖的上游机制没有稳定性承诺,不能把“2.1 以上”视为完整的兼容声明。当前仓库中已有的核对记录如下:
|
|
8
|
+
|
|
9
|
+
| 机制 | 已有核对依据 | 使用时的含义 |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| 7 天周期任务回收与任务归属字段 | Claude Code 2.1.300 起有核对记录 | 更早版本不能据此推断可用 |
|
|
12
|
+
| 逐任务调度谓词与目录级调度锁 | Claude Code 2.1.302 二进制 | 一任务一会话的核心依据 |
|
|
13
|
+
| `script → zsh → claude` 三代进程结构 | 本机 Claude Code 2.1.302 | 会话认亲和进程回收依赖这一结构及祖先链识别 |
|
|
14
|
+
| 任务文件与 `~/.claude/sessions/` 会话登记 | 当前实现以 2.1.302 为本机核对基线 | 登记格式漂移时只能提供烟雾告警,不能证明所有语义仍兼容 |
|
|
15
|
+
| 投递抖动默认值与算法 | Claude Code 2.1.285,darwin-arm64,2026-09-30 安装的二进制 | 不能写成已经在 2.1.302 重新验证;远程配置还可能覆盖默认值 |
|
|
16
|
+
|
|
17
|
+
这张表记录已有证据,不是“只有列出的版本才能运行”的断言。低于或高于核心基线的版本都按未验证处理;升级后应:
|
|
18
|
+
|
|
19
|
+
1. 运行 `cron-up`,查看会话登记、启动链和预热告警
|
|
20
|
+
2. 用低风险任务验证发现、预热、投递和回收的完整链路
|
|
21
|
+
3. 若预计投递与实际投递持续出现系统性偏差,重新核对上游算法,而不是只调整本地显示常量
|
|
22
|
+
|
|
23
|
+
`src/constants.mjs` 中的 `CC_JITTER` 注释还记录了上游远程配置键 `tengu_kairos_cron_config`。运行时的远程配置可能改变投递行为,因此本地逆向常量只能用于估算
|
|
24
|
+
|
|
25
|
+
## 工作区与任务身份
|
|
26
|
+
|
|
27
|
+
- 工作区是包含 `.claude/scheduled_tasks.json` 的精确目录,不自动等同于 Git 根目录
|
|
28
|
+
- 扫描从 roots 开始,root 本身及其下第 1 至第 3 层都检查候选文件;符号链接形式的子目录不递归,排除名单由 `PRUNE_DIRS` 定义
|
|
29
|
+
- 原生任务优先使用唯一 `id` 作为状态键;缺少 id 的手写任务使用稳定内容摘要形成 `anon:` 键
|
|
30
|
+
- 同一个状态键对应多条任务时,无法唯一映射会话,巡检跳过这些条目的预热与归属改写并给出告警
|
|
31
|
+
- 推荐入口是 Claude Code 的 CronCreate(`durable: true`);手写或第三方生成文件只是兼容性用法,字段必须遵循上游语义
|
|
32
|
+
|
|
33
|
+
## 逐任务归属与调度谓词
|
|
34
|
+
|
|
35
|
+
Claude Code 2.1.302 中核对到的投递判定按以下优先级理解:
|
|
36
|
+
|
|
37
|
+
1. 任务没有归属会话:只有持目录调度锁的会话投递
|
|
38
|
+
2. 任务归属当前会话:当前会话投递,不需要目录锁
|
|
39
|
+
3. 当前进程是任务记录的创建者进程(resume 形态):可以投递
|
|
40
|
+
4. 其他情况:会话必须持锁、任务属于本项目,并且记录中的创建者进程已死,才可接管
|
|
41
|
+
|
|
42
|
+
cron-up 为进入窗口且原归属不在场的任务分别启动会话,认亲后把任务归属写到对应 sessionId 和进程指纹。这样,同目录同时到期的不同任务不必共享一个交互会话
|
|
43
|
+
|
|
44
|
+
“原创建者在线时不碰任务”只适用于**不预热、不改写归属**;自动续期仍会修改显式周期任务的 `permanent` 字段
|
|
45
|
+
|
|
46
|
+
某个预热中途退出后,其他符合上游规则的活会话仍可能按第 4 条接管,但这不是 cron-up 对执行结果的额外保证
|
|
47
|
+
|
|
48
|
+
## 巡检与会话生命周期
|
|
49
|
+
|
|
50
|
+
主要常量集中在 [`src/constants.mjs`](../src/constants.mjs),数值以源码为准;本文解释其语义和派生关系。修改常量时应同时核对状态机,而不是只改文档
|
|
51
|
+
|
|
52
|
+
| 常量 | 含义 |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `INTERVAL_SECONDS` | launchd 相邻巡检轮次的目标间隔,休眠时可能漏轮 |
|
|
55
|
+
| `LEAD_SECONDS` | 周期任务的预热前瞻量 |
|
|
56
|
+
| `MAX_SCAN_DEPTH` | root 下最多再下降的层数,最大深度本身仍检查任务文件 |
|
|
57
|
+
| `WARMUP_POLL_MS` | 新会话启动后,当轮重扫会话登记的间隔 |
|
|
58
|
+
| `WARMUP_WAIT_MS` | 当轮等待认亲的上限,不是判死时限 |
|
|
59
|
+
| `WARMUP_GRACE_SECONDS` | 未登记进程超过该年龄后,才在跨轮状态机中判卡死 |
|
|
60
|
+
| `FAIL_LIMIT` | 没有成功登记的启动失败达到该次数后进入冷却 |
|
|
61
|
+
| `COOLDOWN_SECONDS` | 单任务暂停重试的时间 |
|
|
62
|
+
| `FAIL_TTL_SECONDS` | 已死亡条目的失败记录跨预热窗口保留后的老化时限;活条目和冷却条目按各自状态保留 |
|
|
63
|
+
| `SESSION_RECYCLE_IDLE_SECONDS` | 与 `WARMUP_GRACE_SECONDS` 同值;回收判断要求的界面静默时长 |
|
|
64
|
+
| `SESSION_FIRE_MARGIN_SECONDS` | 除一个巡检间隔外,为下次触发预留的余量 |
|
|
65
|
+
| `SESSION_RECYCLE_MARGIN_SECONDS` | 等于 `SESSION_FIRE_MARGIN_SECONDS + INTERVAL_SECONDS`;回收要求距下次需要超过该余量 |
|
|
66
|
+
| `HEARTBEAT_STALE_FACTOR` | 心跳超过该倍数的巡检间隔未更新时提示沉默 |
|
|
67
|
+
| `PATROL_LOG_ROTATE_BYTES` | 单个巡检日志超过该大小后,在轮初轮转 |
|
|
68
|
+
| `DISPLAY_SEARCH_DAYS` | `list` 和总览寻找未来落点的展示窗口 |
|
|
69
|
+
|
|
70
|
+
一次性任务的预热前瞻统一额外增加 `CC_JITTER.oneShotMaxMs`,窗口前沿为 `落点 - LEAD_SECONDS - CC_JITTER.oneShotMaxMs`。**所有一次性任务都使用该保守窗口**,不是只有 `:00`/`:30` 网格任务;额外前瞻用于覆盖网格任务可能提前投递的相位,非网格任务也采用同一口径。实际启动仍取决于巡检相位和运行状态
|
|
71
|
+
|
|
72
|
+
### 一轮巡检的主要阶段
|
|
73
|
+
|
|
74
|
+
1. 获取巡检互斥锁,读取配置、状态、心跳和 Claude Code 会话登记
|
|
75
|
+
2. 发现工作区,按任务逐组处理;单个工作区出错不应阻断后续工作区
|
|
76
|
+
3. 已有条目先尝试认亲,成功后回填 sessionId、Claude pid 和进程启动指纹,并清零失败计数
|
|
77
|
+
4. 依次处理原创建者在线、终端 resume 接管、执行后回收、预热窗口、卡死退休、冷却和重新启动
|
|
78
|
+
5. 合并自动续期与归属改写;新启动的会话在当轮等待认亲后再尝试补写归属
|
|
79
|
+
6. 回收任务已经删除的会话,保存仍有效的状态和本轮心跳
|
|
80
|
+
|
|
81
|
+
### 启动失败与卡死不是同一计时器
|
|
82
|
+
|
|
83
|
+
- `WARMUP_WAIT_MS` 的当轮等待结束只会停止等待,不增加失败计数,也不直接杀死仍存活的进程
|
|
84
|
+
- 未登记进程年龄超过 `WARMUP_GRACE_SECONDS` 后,由后续巡检执行卡死退休,而不是由到达阈值就精确触发的独立定时器处置
|
|
85
|
+
- 启动即失败、启动后死亡和卡死退休都会进入失败流程;成功认亲会清零失败计数
|
|
86
|
+
- 已死亡条目的失败记录按任务保留,可跨稀疏任务的多个预热窗口累计,并按 `FAIL_TTL_SECONDS` 老化;因此 `FAIL_LIMIT` 统计的不是“连续若干个巡检轮次”
|
|
87
|
+
|
|
88
|
+
### 回收是启发式判断
|
|
89
|
+
|
|
90
|
+
周期任务的会话回收需要同时满足:
|
|
91
|
+
|
|
92
|
+
- `lastFiredAt` 表明该任务在本代会话启动后已经触发
|
|
93
|
+
- 会话界面日志静默超过 `SESSION_RECYCLE_IDLE_SECONDS`
|
|
94
|
+
- 距该任务下次需要会话超过 `SESSION_RECYCLE_MARGIN_SECONDS`
|
|
95
|
+
|
|
96
|
+
这些条件只说明“可以按当前模型回收”,不证明 prompt 的所有副作用都已成功完成。任一条件不满足时会继续复用现有会话;高频周期任务可能因始终不满足回收余量而长期复用同一会话
|
|
97
|
+
|
|
98
|
+
任务被删除或一次性任务执行后从文件中消失时,还会走对应的条目清理路径
|
|
99
|
+
|
|
100
|
+
若工作区整个离开扫描视野,巡检无法继续按该任务状态做回收;总览会尽量报告仍能识别的运行中会话
|
|
101
|
+
|
|
102
|
+
## 任务文件维护与并发
|
|
103
|
+
|
|
104
|
+
每轮维护分两类:
|
|
105
|
+
|
|
106
|
+
- **续期**:只要 `recurring` 为真且 `permanent` 尚未为真,就写入 `permanent: true`,不依赖任务是否进入预热窗口
|
|
107
|
+
- **归属改写**:只针对进入窗口且需要预热的唯一任务键,写入 `createdBySessionId`、`createdByPid`、`createdByProcStart`
|
|
108
|
+
|
|
109
|
+
归属改写不以修改 `prompt`、`cron`、`id`、`createdInProject` 为目标。内容相同但 id 不同的任务分别定位;重复 id 或重复匿名键无法唯一映射时跳过
|
|
110
|
+
|
|
111
|
+
同一批续期和已有条目的归属改写会合并写入;本轮新启动的会话在等待期认亲后,可能通过补盖流程另做一次归属写入。两条路径都使用同目录临时文件和原子替换:
|
|
112
|
+
|
|
113
|
+
- 临时文件先以 0600 创建,再尽量保留原任务文件权限
|
|
114
|
+
- 读取与写入之间使用 `mtimeNs` 做乐观检测,检测到其他写者改动时放弃本轮写入
|
|
115
|
+
- 这不是跨进程 CAS,也不是与 Claude Code 共用的锁;最后检查之后、`rename` 之前仍有一个无法由 cron-up 单方面关闭的竞态窗口
|
|
116
|
+
|
|
117
|
+
因此,文档不能把原子替换表述为绝不覆盖并发更新,卸载也不会回滚已经完成的续期或归属改写
|
|
118
|
+
|
|
119
|
+
## 投递时间算法
|
|
120
|
+
|
|
121
|
+
以下算法用于 `cron-up list`、总览的“最近”和预热尾窗估算。它是上游内部实现的复刻,不是 cron-up 自己决定的调度策略
|
|
122
|
+
|
|
123
|
+
### 任务 id 的归一化值
|
|
124
|
+
|
|
125
|
+
当前实现对原生十六进制任务 id 取前 8 位,解析后除以 `2^32`,得到 `[0, 1)` 范围内的固定值,记作 `h`。同一 id 每次计算相同,不是每次触发重新随机
|
|
126
|
+
|
|
127
|
+
手写非标准 id 的解析不应被当作稳定兼容接口;推荐让 CronCreate 生成 id
|
|
128
|
+
|
|
129
|
+
### 周期任务
|
|
130
|
+
|
|
131
|
+
对某个 cron 落点,先计算它到下一落点的实际间隔 `period`,再估算:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
delay = min(h × 0.5 × period, 30 分钟)
|
|
135
|
+
预计投递 = 本落点 + delay
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
每日任务周期较长,很多原生随机 id 会触及 30 分钟上限。落点已经过去、预计投递尚未到来时,仍属于需要会话在场的尾窗,不能按“下一次未来落点”提前回收
|
|
139
|
+
|
|
140
|
+
代码已记录但**没有复刻**一个上游例外:相邻落点间隔约在 `[300000, 315000)` ms、且表达式匹配上游内部步进规则时,存在 `cacheLeadMs` 提前 15 秒的分支,例如部分约 5 分钟周期任务。对这类任务:
|
|
141
|
+
|
|
142
|
+
- “周期任务一律在落点后投递”的说法不成立
|
|
143
|
+
- 当前 `list` 的预计时间可能与上游实际调度存在约 15 秒或更多的系统性差异
|
|
144
|
+
- 是否补齐该分支需要单独实现和测试,不能只改文档宣称已经支持
|
|
145
|
+
|
|
146
|
+
### 一次性任务
|
|
147
|
+
|
|
148
|
+
- 落点分钟不属于 `:00`/`:30` 网格时,当前估算不增加投递提前量
|
|
149
|
+
- 落点属于网格时,按 `h × 90 秒` 提前,且不早于当前时刻
|
|
150
|
+
- 无论是否属于网格,巡检的预热前瞻都统一额外增加 90 秒,原因见生命周期章节
|
|
151
|
+
|
|
152
|
+
会话内部还存在秒级轮询尾差,因此“预计投递时间”不等于对实际开始执行时刻的保证
|
|
153
|
+
|
|
154
|
+
### 展示边界
|
|
155
|
+
|
|
156
|
+
未来落点的展示搜索范围由 `DISPLAY_SEARCH_DAYS` 定义。“一年内无”既包括下一落点超出展示窗口的表达式,例如部分闰日任务,也包括字段可以解析、但日历上永远无法匹配的表达式,例如 `0 0 30 2 *`
|
|
157
|
+
|
|
158
|
+
“cron 无效”只用于表达式解析失败,或字段值域、区间导致合法值集合为空;它与“一年内无”不是同一状态
|
|
159
|
+
|
|
160
|
+
## 会话登记、认亲与接管
|
|
161
|
+
|
|
162
|
+
预热进程的典型结构为:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
/usr/bin/script(cron-up state 记录这一层)
|
|
166
|
+
└─ /bin/zsh -lic
|
|
167
|
+
└─ claude(会话登记记录这一层)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
巡检读取 `~/.claude/sessions/*.json`,结合进程存活、cwd 和祖先链,把 script 进程与登记的 sessionId 对应起来。登记是上游输出,不是稳定 API;读取器包含三类烟雾告警:
|
|
171
|
+
|
|
172
|
+
- 有登记文件但识别不到 interactive 会话
|
|
173
|
+
- 存活登记进程都不是可识别的 `claude`
|
|
174
|
+
- interactive 会话缺少有效 pid 或工作目录
|
|
175
|
+
|
|
176
|
+
这些告警只能覆盖已知漂移形态,不能证明上游所有内部语义均未变化
|
|
177
|
+
|
|
178
|
+
其他恢复机制:
|
|
179
|
+
|
|
180
|
+
- **终端接管**:script 层已退出但同一 sessionId 仍在终端存活时,不再另拉或结束该会话
|
|
181
|
+
- **孤儿认领**:state 丢失时,按任务专属日志路径从进程表重新识别仍可确认的预热进程
|
|
182
|
+
- **停止保护**:发信号前核对进程启动指纹,降低 pid 复用时误伤其他进程组的概率
|
|
183
|
+
|
|
184
|
+
## 日志与隐私
|
|
185
|
+
|
|
186
|
+
- 巡检日志位于 `~/Library/Logs/cron-up/launchd.{out,err}.log`,默认 `cron-up logs` 展示当前 stdout 日志最近 100 行
|
|
187
|
+
- 会话界面日志按 `<工作区折名>--<任务id>.log` 隔离,启动新代时轮转保留一代 `.1`
|
|
188
|
+
- 工作区折名会转义下划线并把路径分隔符折叠为 `_`,避免不同路径轻易撞名
|
|
189
|
+
- 会话日志可能包含任务输出,文件按 0600、目录按 0700 管理
|
|
190
|
+
- Claude Code 自己仍在 `~/.claude/projects/` 保存对话记录,cron-up 的 `--purge` 不删除这些记录
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 旧版迁移
|
|
2
|
+
|
|
3
|
+
本文适用于从 **26.10.30 及更早版本**升级到 **26.10.31 或更高版本**。从 26.10.31 开始,预热模型由“每个目录一个会话”改为“每个进入窗口的任务一个会话”,配置也只保留 roots
|
|
4
|
+
|
|
5
|
+
## 主要变化
|
|
6
|
+
|
|
7
|
+
| 方面 | 26.10.30 及更早 | 26.10.31 起 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| 预热粒度 | 每个工作区一个目录级会话 | 每个进入窗口的任务一个专属会话 |
|
|
10
|
+
| 会话日志 | 目录级命名 | `<工作区折名>--<任务id>.log` |
|
|
11
|
+
| 状态记录 | 扁平的目录级条目 | 按工作区、任务 id 两级记录 |
|
|
12
|
+
| 配置 | 可能留有旧行为开关和字段 | 只保留 `roots`,其他行为常量化 |
|
|
13
|
+
|
|
14
|
+
## 升级步骤
|
|
15
|
+
|
|
16
|
+
通用安装、启动链重建和总览验收按 [README 的升级步骤](../README.md#升级与启动链维护)执行。刚发布、需要避免 registry 或本机缓存带回旧版时,按[发布后的逐机升级与验收](release.md#每次发版后的逐机升级与验收)使用显式版本号
|
|
17
|
+
|
|
18
|
+
重跑 install 会移除旧版废弃配置字段;配置无法解读时的默认 roots 回退与首轮写入风险见 [README 的配置恢复说明](../README.md#配置无法解读时)
|
|
19
|
+
|
|
20
|
+
迁移后还要检查预热日志名称。新一代任务级日志应包含 `--<任务id>`;`cron-up logs <目录名>` 命中多个任务时,按候选提示补充任务 id
|
|
21
|
+
|
|
22
|
+
## 旧目录级会话如何收尾
|
|
23
|
+
|
|
24
|
+
升级后的首轮巡检会结束能够核实身份的旧目录级预热,再按任务重新启动。迁移不会把旧的目录级状态直接伪装成新任务级状态
|
|
25
|
+
|
|
26
|
+
为防止 pid 复用导致误杀,缺少进程启动指纹的旧条目不会被贸然发信号:
|
|
27
|
+
|
|
28
|
+
- 对应进程已经退出时,不再需要停止它
|
|
29
|
+
- 对应进程仍在运行且无法安全核实时,巡检会明确告警,不会把“未停止”记成“已回收”
|
|
30
|
+
- 出现这类告警时,先用 `cron-up logs` 找到“旧版迁移”条目,记录其中的 pid、工作区或日志路径;旧版扁平状态不一定会出现在总览中
|
|
31
|
+
- 再把下列命令中的 12345 换成实际 pid,核对进程启动时间、父进程和完整启动命令:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
/bin/ps -p 12345 -o pid=,ppid=,lstart=,args=
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
确认它确为旧版预热后再决定如何收尾;无法确认身份时,不要对该进程或其进程组直接发信号
|
|
38
|
+
|
|
39
|
+
## 不会自动回滚的内容
|
|
40
|
+
|
|
41
|
+
升级和卸载都不会把任务文件恢复到首次使用 cron-up 之前:
|
|
42
|
+
|
|
43
|
+
- 已写入的 `permanent` 会保留
|
|
44
|
+
- 已改写的 `createdBySessionId`、`createdByPid`、`createdByProcStart` 不会自动还原
|
|
45
|
+
- Claude Code 的任务和 `~/.claude/projects` 下的对话记录不由迁移流程删除
|
|
46
|
+
|
|
47
|
+
如果工作区不再纳入 roots,后续巡检不会再维护它,但既有字段和已经运行且失去扫描视野的会话仍需分别核查
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 发布流程
|
|
2
|
+
|
|
3
|
+
本文面向维护者,记录 `adaex/cron-up` 的版本号、tag、npm trusted publishing 和发版后逐机验收。普通升级见 [README](../README.md#升级与启动链维护)
|
|
4
|
+
|
|
5
|
+
## 版本号
|
|
6
|
+
|
|
7
|
+
格式为 `YY.MM.serial`,例如 `26.10.33`:
|
|
8
|
+
|
|
9
|
+
- `YY` 是两位年份,`MM` 是两位月份
|
|
10
|
+
- `serial` 是累计序号,包含 0.x 时代,跨月份不归零
|
|
11
|
+
- 工程上的最终校验口径是**远端 `v*` tag 数量**,不是 npm 成功发布次数;下一序号等于发布前该数量加一
|
|
12
|
+
|
|
13
|
+
发布前同步并计算:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git fetch --prune --prune-tags --tags
|
|
17
|
+
git tag -l 'v*' | wc -l
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
计数结果加一就是下一序号。release 工作流会重新直接统计远端 tag,防止本地状态与远端不一致;被删除的远端 tag 不再计数。已推送但尚未 publish、且 tag 内容无需改变的失败运行,优先重跑同一工作流,而不是递增序号;需要修复 tag 内容时按下文处理
|
|
21
|
+
|
|
22
|
+
## 发布步骤
|
|
23
|
+
|
|
24
|
+
1. 确认 `main` 已包含本次发布内容,测试通过
|
|
25
|
+
2. 按上述规则修改 `package.json` 的 `version`,并将发布提交合入 `main`
|
|
26
|
+
3. 在发布提交上创建同名签名注解 tag 并推送:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git tag -s v<version> <发布提交> -m "发布 <version>"
|
|
30
|
+
git push origin v<version>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. 等待 `.github/workflows/release.yml` 完成。工作流会:
|
|
34
|
+
- 在 macOS 上用 Node 22、24 运行测试
|
|
35
|
+
- 校验 `package.json` 版本与 tag 完全一致
|
|
36
|
+
- 校验 tag 序号与远端 `v*` tag 总数一致
|
|
37
|
+
- 执行 `npm pack --dry-run`
|
|
38
|
+
- 通过 `npm publish --provenance` 发布
|
|
39
|
+
|
|
40
|
+
发布失败时先确认失败发生在 publish 之前,并区分两种情况:
|
|
41
|
+
|
|
42
|
+
- **tag 内容无需改变**:例如 runner 或 registry 的临时故障,可以直接对同一个 tag 重跑 release 工作流,不消耗新序号
|
|
43
|
+
- **必须修改仓库内容**:例如测试、版本号或工作流本身有误,只修复 main 后重跑旧 tag 仍会检出旧内容。应先把修复提交合入 main,再将同名签名 tag 重新签在修复提交上,并按仓库 tag 保护规则替换远端同名 tag,使新运行检出修复后的内容;同名 tag 不增加远端计数,因此不消耗新序号
|
|
44
|
+
|
|
45
|
+
只有尚未成功 publish 的版本才能按后一种方式替换 tag。已经成功 publish 的版本不要再移动 tag,后续问题通过新的发布修复
|
|
46
|
+
|
|
47
|
+
## Trusted publishing 与凭据边界
|
|
48
|
+
|
|
49
|
+
npm 通过 GitHub OIDC 校验发布工作流身份,仓库中不保存长期 `NPM_TOKEN`。这降低了长期 npm 令牌泄漏的风险,但不等于所有凭据、工作流或账号安全风险都不存在
|
|
50
|
+
|
|
51
|
+
npm 包设置中需要一次性登记:
|
|
52
|
+
|
|
53
|
+
- repository:`adaex/cron-up`
|
|
54
|
+
- workflow:`release.yml`(仓库中的 `.github/workflows/release.yml`)
|
|
55
|
+
- 发布触发:同名 `v*` tag 对应的 GitHub Actions 运行
|
|
56
|
+
|
|
57
|
+
配置缺失或需要重建时,按 npm 官方 [Trusted Publishers 文档](https://docs.npmjs.com/trusted-publishers/)核对,同时确认 workflow 仍具有 `id-token: write`,且发布环境使用满足要求的 npm 版本
|
|
58
|
+
|
|
59
|
+
## 每次发版后的逐机升级与验收
|
|
60
|
+
|
|
61
|
+
按 26.10.32 的实际发布记录,publish 成功后 registry 通常约两分钟可查,偶发需要七八分钟;带 provenance 时还要等待透明日志与后端入库,本机元数据缓存可能进一步滞后
|
|
62
|
+
|
|
63
|
+
对每一台已有安装的机器分别执行:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm i -g cron-up@<新版本> --prefer-online
|
|
67
|
+
cron-up install
|
|
68
|
+
cron-up
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- 第一条必须使用显式新版本号和 `--prefer-online`,不要用不带版本的模糊安装命令
|
|
72
|
+
- 第二条重建配置、启动脚本与 LaunchAgent,并尝试首轮巡检
|
|
73
|
+
- 最后以总览首行的实际版本号为准,同时确认服务已加载、上次退出码为 0、“需要留意”为空;不能用 npm 命令退出码或 registry 页面代替本机核验
|
|
74
|
+
|
|
75
|
+
未安装的机器不借本次升级新装;无法连接的机器明确记为未完成。npm publish 成功只代表包已发布,不代表各台机器已经升级或巡检健康
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cron-up",
|
|
3
|
-
"version": "26.10.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "26.10.34",
|
|
4
|
+
"description": "为 Claude Code 定时任务在触发前按需预热交互会话",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cron-up": "bin/cron-up.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"docs",
|
|
13
|
+
"CONTRIBUTING.md"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
14
16
|
"test": "node --test"
|
package/src/cli.mjs
CHANGED
|
@@ -102,17 +102,21 @@ export function parseCli(argv) {
|
|
|
102
102
|
return result;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const USAGE = `cron-up ——
|
|
105
|
+
const USAGE = `cron-up —— 为 Claude Code 定时任务按需预热交互会话(一任务一会话)
|
|
106
106
|
|
|
107
107
|
用法:
|
|
108
108
|
cron-up 查看总览(服务、配置、任务、预热会话)
|
|
109
109
|
cron-up install 安装配置与 LaunchAgent(可用 --roots 指定扫描目录)
|
|
110
|
-
cron-up uninstall
|
|
111
|
-
cron-up list
|
|
110
|
+
cron-up uninstall 停止预热并移除 LaunchAgent(--purge 另删配置与日志)
|
|
111
|
+
cron-up list 逐个列出任务的设定/预计投递时间与预热状态
|
|
112
112
|
cron-up run 执行一轮巡检(launchd 入口,也可手动立即跑一轮)
|
|
113
113
|
cron-up logs [目录片段] 查看巡检日志或后台会话记录(-f 跟踪)
|
|
114
114
|
|
|
115
|
-
升级:npm i -g cron-up@latest
|
|
115
|
+
升级:npm i -g cron-up@latest --prefer-online
|
|
116
|
+
cron-up install
|
|
117
|
+
|
|
118
|
+
完全移除:cron-up uninstall --purge
|
|
119
|
+
npm uninstall -g cron-up`;
|
|
116
120
|
|
|
117
121
|
export async function main(argv = process.argv) {
|
|
118
122
|
const parsed = parseCli(argv.slice(2));
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export const DEFAULT_CONFIG = {
|
|
6
6
|
// 默认只扫本人的三个任务制容器,不扫整个家目录——他人可写仓库里的周期
|
|
7
7
|
// 任务会被自动续期永久化,默认范围必须是用户完全掌控的领地(见 README
|
|
8
|
-
//
|
|
8
|
+
// 「安全与自动修改」)。其他布局用 cron-up install --roots 覆盖;目录不存在则跳过。
|
|
9
9
|
roots: ['~/space', '~/workspace', '~/tasks'],
|
|
10
10
|
};
|
|
11
11
|
|
|
@@ -45,9 +45,8 @@ export const WARMUP_GRACE_SECONDS = 180;
|
|
|
45
45
|
// 帽与墙钟帽双出口,高负载拖慢 setTimeout 也不会把一轮撑过间隔。
|
|
46
46
|
export const WARMUP_WAIT_MS = 24_000;
|
|
47
47
|
export const WARMUP_POLL_MS = 3_000;
|
|
48
|
-
// window
|
|
49
|
-
//
|
|
50
|
-
// WARMUP_GRACE 同值,结构上对齐「3 分钟定生死」的既有心智。
|
|
48
|
+
// window 回收判据一:界面静默超过此时长,作为本代触发后空闲的启发式,
|
|
49
|
+
// 不是任务完成状态的证明。取 WARMUP_GRACE 同值,结构上对齐既有心智。
|
|
51
50
|
export const SESSION_RECYCLE_IDLE_SECONDS = WARMUP_GRACE_SECONDS;
|
|
52
51
|
// window 回收判据二:距该任务下次「需要会话在场」至少还有这么多秒。回收
|
|
53
52
|
// 后下轮(≤INTERVAL)发现并 spawn,进程即在场,盖印在该轮等待期内完成;
|
|
@@ -73,7 +72,7 @@ export const PRUNE_DIRS = new Set([
|
|
|
73
72
|
]);
|
|
74
73
|
|
|
75
74
|
// Claude Code 会话投递定时任务时的抖动参数,两处使用:list/总览预测
|
|
76
|
-
//
|
|
75
|
+
// 「预计投递时间」;巡检把「落点之后、延迟投递点之前」这段投递尾窗
|
|
77
76
|
// 也算作需要会话在场,避免在任务真正投递前把预热回收。逆向自
|
|
78
77
|
// Claude Code v2.1.285
|
|
79
78
|
// (darwin-arm64 原生二进制,2026-09-30 安装)内部 createCronScheduler 的
|
|
@@ -81,8 +80,7 @@ export const PRUNE_DIRS = new Set([
|
|
|
81
80
|
// recurringCapMs);一次性任务在 :00/:30 网格点提前 0~oneShotMaxMs。
|
|
82
81
|
// 这些值没有稳定性承诺——版本升级可能改,运行时还可能被远程配置键
|
|
83
82
|
// tengu_kairos_cron_config(60 秒刷新,校验失败回退默认)覆盖。预测与
|
|
84
|
-
// 实际系统性对不上时,按
|
|
85
|
-
// claude-code-scheduled-task-jitter 的排查步骤复核新版本。
|
|
83
|
+
// 实际系统性对不上时,按 docs/internals.md 的版本证据与复核步骤核对新版本。
|
|
86
84
|
export const CC_JITTER = {
|
|
87
85
|
recurringFrac: 0.5,
|
|
88
86
|
recurringCapMs: 30 * 60 * 1000,
|
package/src/cron.mjs
CHANGED
|
@@ -260,7 +260,7 @@ export function taskWanted(task, now, leadMs) {
|
|
|
260
260
|
// 两个口径供展示视图复用(同一个已解析 cron 顺带回答巡检问题)。
|
|
261
261
|
export { wantedNow, wantedWithin };
|
|
262
262
|
|
|
263
|
-
// ---- Claude Code
|
|
263
|
+
// ---- Claude Code 投递抖动(预计投递时间)----
|
|
264
264
|
// 复刻 v2.1.285 内部的 M():任务 id 前 8 位十六进制除以 2^32,得 [0,1) 的
|
|
265
265
|
// 固定哈希——同一任务每次触发的抖动完全相同,不是随机数。id 缺失或前 8
|
|
266
266
|
// 位都不是十六进制时 parseInt 得 NaN,上游回退 0;任务文件由别的程序书
|
|
@@ -271,7 +271,7 @@ export function taskIdHash(taskId) {
|
|
|
271
271
|
return Number.isFinite(n) ? n : 0;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
//
|
|
274
|
+
// 任务在展示语境里的「设定时间 → 预计投递」对:一次性任务是落点加网格
|
|
275
275
|
// 提前量;周期任务在投递尾窗内属于上一落点(即将到来的这次延迟投递,
|
|
276
276
|
// 不是 nextAfter 给出的下个落点——后者严格在未来,会把展示拐到明天),
|
|
277
277
|
// 否则是下一落点 + 按 id 的抖动。先于窗口检查尾窗:2 月 29 日这类长周期
|
|
@@ -326,7 +326,7 @@ function recurringDelayMs(cron, slot, taskId, withinDays) {
|
|
|
326
326
|
|
|
327
327
|
// 周期任务的「投递尾窗」判定:上一落点已过、按 id 算的延迟投递时刻未
|
|
328
328
|
// 到。返回 [落点, 投递时刻 ms];不在尾窗、找不到上一或下一落点返回
|
|
329
|
-
// null。wanted
|
|
329
|
+
// null。wanted(会话需求判定)与 nextDelivery(展示)共用这一口径,两边不会
|
|
330
330
|
// 各说各话;窗口取 TAIL_WINDOW_DAYS,理由见 constants。
|
|
331
331
|
function recurringTailWindow(cron, taskId, now) {
|
|
332
332
|
const prev = cron.prevAtOrBefore(now, TAIL_WINDOW_DAYS);
|
package/src/display.mjs
CHANGED
|
@@ -110,7 +110,7 @@ export function taskView(task, now, leadMs) {
|
|
|
110
110
|
// wanted 恒为 null 或布尔:没传 leadMs(不问)与 cron 无效(问不了)
|
|
111
111
|
// 都是 null,键的形状不随参数变化。
|
|
112
112
|
wanted: leadMs !== undefined ? wantedWithin(cron, task, now, leadMs) : null,
|
|
113
|
-
// 计入 Claude Code
|
|
113
|
+
// 计入 Claude Code 投递抖动后的预计投递时刻;已错过的一次性任务
|
|
114
114
|
// 没有未来触发,为 null;尾窗内的周期任务是即将到来的这次延迟投递。
|
|
115
115
|
// 与 nxt 同为 Date 或 null。
|
|
116
116
|
fire: missed ? null : fire,
|
|
@@ -135,7 +135,7 @@ export function pad2(n) {
|
|
|
135
135
|
return String(n).padStart(2, '0');
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
//
|
|
138
|
+
// 「设定时间 → 预计投递」文案。抖动四舍五入到秒(调度器秒级轮询):
|
|
139
139
|
// 同日省略第二个日期;预计值带非零秒时显示 HH:MM:SS(如 05:15:37);落
|
|
140
140
|
// 在同一分钟(无抖动)则只显示落点,不画无意义的箭头。
|
|
141
141
|
export function fmtFireRange(nxt, fire) {
|
package/src/list.mjs
CHANGED
package/src/overview.mjs
CHANGED
|
@@ -77,7 +77,7 @@ function overviewTasks(cfg, cols, details, state) {
|
|
|
77
77
|
bad += 1;
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
|
-
//
|
|
80
|
+
// 「最近」按计入抖动后的预计投递时间排,而非 cron 落点。
|
|
81
81
|
const fire = v.fire ?? v.nxt;
|
|
82
82
|
if (fire && (!soonest || fire.getTime() < soonest[0].getTime())) {
|
|
83
83
|
soonest = [fire, ws, v.summary, v.nxt];
|
|
@@ -189,18 +189,18 @@ function overviewSessions(cols, state, details, suppressCooling, discovered) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
const COMMANDS = [
|
|
192
|
-
['list', '
|
|
192
|
+
['list', '逐个列出任务的内容摘要、设定/预计投递时间与预热状态'],
|
|
193
193
|
['logs [目录片段] [-f]', '查看巡检日志或后台会话记录'],
|
|
194
194
|
['run', '立即手动巡检一轮'],
|
|
195
|
-
['install --roots ~/a,~/b', '
|
|
195
|
+
['install --roots ~/a,~/b', '更新扫描目录并重新安装(不带参数沿用有效配置)'],
|
|
196
196
|
];
|
|
197
197
|
|
|
198
198
|
export async function cmdOverview(args) {
|
|
199
199
|
const cols = deps.terminalWidth();
|
|
200
200
|
const v = packageVersion();
|
|
201
201
|
deps.print(v === null
|
|
202
|
-
? 'cron-up ——
|
|
203
|
-
: `cron-up ${v} ——
|
|
202
|
+
? 'cron-up —— 为 Claude Code 定时任务按需预热交互会话'
|
|
203
|
+
: `cron-up ${v} —— 为 Claude Code 定时任务按需预热交互会话(一任务一会话)`);
|
|
204
204
|
|
|
205
205
|
const cfgPath = args?.config ?? deps.paths.configPath;
|
|
206
206
|
let cfg = null;
|
|
@@ -232,8 +232,8 @@ export async function cmdOverview(args) {
|
|
|
232
232
|
} else if (cfg === null) {
|
|
233
233
|
deps.print('配置:缺失,运行 cron-up install 生成');
|
|
234
234
|
} else {
|
|
235
|
-
deps.print(`巡检:每 ${INTERVAL_SECONDS}
|
|
236
|
-
+ '
|
|
235
|
+
deps.print(`巡检:每 ${INTERVAL_SECONDS} 秒一轮,周期任务提前 ${LEAD_SECONDS} `
|
|
236
|
+
+ '秒进入预热窗口');
|
|
237
237
|
if (info && info.interval !== null && info.interval !== undefined
|
|
238
238
|
&& info.interval !== INTERVAL_SECONDS) {
|
|
239
239
|
alerts.push(`launchd 实际间隔 ${info.interval} 秒与当前版本的 `
|
package/src/service.mjs
CHANGED
|
@@ -360,11 +360,12 @@ export async function cmdInstall(args) {
|
|
|
360
360
|
validateConfig(cfg, (m) => deps.print(m));
|
|
361
361
|
if (JSON.stringify(cfg.roots)
|
|
362
362
|
=== JSON.stringify(normalizeRoots(DEFAULT_CONFIG.roots))) {
|
|
363
|
-
// 与 README
|
|
363
|
+
// 与 README「安全与自动修改」同一口径,在配置落定时再提示一次。
|
|
364
364
|
deps.print(`提示:未指定 --roots,默认扫描 ${DEFAULT_CONFIG.roots.join('、')}`
|
|
365
365
|
+ '(不存在的目录跳过);今后 clone 进这些容器的仓库也会进入巡检,'
|
|
366
|
-
+ '
|
|
367
|
-
+ '--roots
|
|
366
|
+
+ '其中的周期任务会被自动续期永久化,任务、CLAUDE.md 与项目配置都会'
|
|
367
|
+
+ '影响无人值守会话。目录布局不同或需收窄时用 --roots 指定'
|
|
368
|
+
+ '(见 README「安全与自动修改」)');
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
const launcher = deps.resolveLauncher();
|
|
@@ -515,7 +516,7 @@ export async function cmdUninstall(args) {
|
|
|
515
516
|
// npm 管的文件不能手删:命令行本身交给 npm 卸载。
|
|
516
517
|
deps.print('命令行本身请运行 npm uninstall -g cron-up 卸载');
|
|
517
518
|
} else {
|
|
518
|
-
deps.print('配置与日志已保留(--purge
|
|
519
|
+
deps.print('配置与日志已保留(--purge 可删除 cron-up 自有配置与日志)');
|
|
519
520
|
}
|
|
520
521
|
} finally {
|
|
521
522
|
deps.releaseRunLock(lock);
|
package/src/sessions.mjs
CHANGED
|
@@ -119,11 +119,11 @@ function canSignalProcessGroup(pid) {
|
|
|
119
119
|
// 为一个任务拉起一个 detached 的 pty TUI,返回 {pid, procStart}。
|
|
120
120
|
export function spawnSession(ws, taskId) {
|
|
121
121
|
const logPath = sessionLogPath(ws, taskId);
|
|
122
|
-
// 会话日志目录 700(README
|
|
122
|
+
// 会话日志目录 700(README 权限约定):每任务独立文件,目录只在此处创建。
|
|
123
123
|
ensurePrivateDir(path.dirname(logPath));
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
124
|
+
// script(1) 启动即截断:每次新拉起前无条件保留上一代(.1),避免上次
|
|
125
|
+
// 启动以来的界面记录被抹掉。总量上界两代,活着的一代增长再快也只影响
|
|
126
|
+
// 自身,不会经轮转放大。
|
|
127
127
|
try {
|
|
128
128
|
if (fs.existsSync(logPath)) rotateOnce(logPath);
|
|
129
129
|
} catch {
|