cron-up 26.10.31 → 26.10.33
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/README.md +16 -16
- package/package.json +1 -1
- package/src/args.mjs +1 -2
- package/src/cli.mjs +10 -16
- package/src/config.mjs +22 -19
- package/src/constants.mjs +19 -15
- package/src/cron.mjs +40 -13
- package/src/display.mjs +6 -5
- package/src/logs.mjs +2 -1
- package/src/patrol.mjs +388 -195
- package/src/registry.mjs +10 -13
- package/src/service.mjs +185 -46
- package/src/sessions.mjs +57 -16
- package/src/tasks.mjs +45 -84
package/src/tasks.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Claude Code 任务文件层:发现工作区、读任务、续期(permanent 标记)、
|
|
2
|
-
//
|
|
1
|
+
// Claude Code 任务文件层:发现工作区、读任务、续期(permanent 标记)、
|
|
2
|
+
// 归属盖印与原子写。任务文件是另一个程序写的外部输入,所有入口都
|
|
3
3
|
// 校验形状,坏数据读作「没有任务」而不是抛进巡检循环。
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
@@ -10,8 +10,6 @@ import { deps, isPlainObject } from './internals.mjs';
|
|
|
10
10
|
import { TASK_REL } from './paths.mjs';
|
|
11
11
|
import {
|
|
12
12
|
PRUNE_DIRS,
|
|
13
|
-
CC_JITTER,
|
|
14
|
-
AUTO_MIN_ID_TARGET_DELAY_MS,
|
|
15
13
|
MAX_SCAN_DEPTH,
|
|
16
14
|
} from './constants.mjs';
|
|
17
15
|
|
|
@@ -140,9 +138,9 @@ export function taskKey(t) {
|
|
|
140
138
|
return `anon:${createHash('sha1').update(taskSig(t)).digest('hex').slice(0, 16)}`;
|
|
141
139
|
}
|
|
142
140
|
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
141
|
+
// 任务的稳定内容签名:只含不会被我们与调度器改动的字段。盖印改
|
|
142
|
+
// createdBy*、执行改 lastFiredAt、续期加 permanent 都不改变签名;无 id
|
|
143
|
+
// 任务的 taskKey 用它生成内容摘要键。
|
|
146
144
|
export function taskSig(t) {
|
|
147
145
|
return JSON.stringify([
|
|
148
146
|
t?.cron ?? null,
|
|
@@ -152,9 +150,25 @@ export function taskSig(t) {
|
|
|
152
150
|
]);
|
|
153
151
|
}
|
|
154
152
|
|
|
153
|
+
// 按任务身份键分组的共享视图:重复键天然落在同一组,调用方不必各自
|
|
154
|
+
// 维护重复计数与键集合。非对象条目不参与盖印或巡检,和 readTasks/
|
|
155
|
+
// maintain 的既有口径一致。
|
|
156
|
+
export function groupTasksByKey(tasks) {
|
|
157
|
+
const groups = new Map();
|
|
158
|
+
for (const t of tasks) {
|
|
159
|
+
if (!isPlainObject(t)) continue;
|
|
160
|
+
const key = taskKey(t);
|
|
161
|
+
const group = groups.get(key) ?? [];
|
|
162
|
+
group.push(t);
|
|
163
|
+
groups.set(key, group);
|
|
164
|
+
}
|
|
165
|
+
return groups;
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
// 任务文件在「读到」与「写回」之间被别的写者改了(Claude Code 更新
|
|
156
|
-
// lastFiredAt
|
|
157
|
-
//
|
|
169
|
+
// lastFiredAt 或删除已触发任务):最终检查时能发现就抛 TaskFileChanged,
|
|
170
|
+
// 让下轮巡检基于更新的文档重试。检查与 rename 之间仍有极小竞态,不能把
|
|
171
|
+
// 这层检测当成与上游共用的互斥锁。
|
|
158
172
|
export class TaskFileChanged extends Error {
|
|
159
173
|
constructor(file) {
|
|
160
174
|
super(`任务文件已被其他写者改动:${file}`);
|
|
@@ -184,8 +198,8 @@ function tmpSibling(file) {
|
|
|
184
198
|
|
|
185
199
|
// 旁边写再 rename 的原子替换,保留原文件权限位与中文字面量。
|
|
186
200
|
// tmp 以 0600 创建(绝不更宽),写完再放宽到原 mode。expectedMtimeNs 是
|
|
187
|
-
//
|
|
188
|
-
//
|
|
201
|
+
// 乐观检测:最终检查时路径 mtime 已变说明别的写者抢先,抛 TaskFileChanged
|
|
202
|
+
// 而不是覆盖这次检查之前发生的更新。
|
|
189
203
|
export function atomicWriteJson(file, doc, expectedMtimeNs) {
|
|
190
204
|
let mode = 0o600;
|
|
191
205
|
try {
|
|
@@ -221,72 +235,17 @@ export function atomicWriteJson(file, doc, expectedMtimeNs) {
|
|
|
221
235
|
fs.renameSync(tmp, file);
|
|
222
236
|
}
|
|
223
237
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return parseInt(id, 16);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// autoMinId 的 id 数值上界:每日任务延迟 ≤ AUTO_MIN_ID_TARGET_DELAY_MS。
|
|
233
|
-
export function minTaskIdMaxN() {
|
|
234
|
-
return Math.floor(
|
|
235
|
-
AUTO_MIN_ID_TARGET_DELAY_MS
|
|
236
|
-
/ (CC_JITTER.recurringFrac * 86400_000) * 2 ** 32);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// 把显式周期任务里 id 数值超过上界(或形状不合法)的,改写成文件内未占用
|
|
240
|
-
// 的小号(00000001 起的 8 位十六进制),使其投递抖动降到目标延迟内。原地
|
|
241
|
-
// 修改 tasks 元素,返回 {changed, idChanges};idChanges 用 taskKey 口径
|
|
242
|
-
// 记录改前/改后键,巡检同轮把 state 条目跟着改键(state 以任务 id 为
|
|
243
|
-
// 键,不改键会把刚换 id 的任务误判为「旧任务删除 + 新任务」而错杀重
|
|
244
|
-
// 拉)。关键纪律:
|
|
245
|
-
// - 只碰 t.recurring truthy 的显式周期任务,一次性与无标志手写条目不碰;
|
|
246
|
-
// - 已达标的小号(含 00000000)原样保留,永不重排——多次巡检结果稳定,
|
|
247
|
-
// 第二轮起 0 改动、文件字节不动;
|
|
248
|
-
// - 空号在「文件内全部任务」(含一次性)已占用的 id 之外分配,保证同文
|
|
249
|
-
// 件唯一;跨文件重复无妨(各工作区独立加载)。
|
|
250
|
-
export function minifyIds(tasks) {
|
|
251
|
-
const maxN = minTaskIdMaxN();
|
|
252
|
-
const used = new Set();
|
|
253
|
-
for (const t of tasks) {
|
|
254
|
-
if (!isPlainObject(t)) continue;
|
|
255
|
-
const n = parseTaskIdN(t.id);
|
|
256
|
-
if (n !== null) used.add(n);
|
|
257
|
-
}
|
|
258
|
-
let next = 1;
|
|
259
|
-
const takeFree = () => {
|
|
260
|
-
while (used.has(next)) next += 1;
|
|
261
|
-
used.add(next);
|
|
262
|
-
return next;
|
|
263
|
-
};
|
|
264
|
-
let changed = 0;
|
|
265
|
-
const idChanges = [];
|
|
266
|
-
for (const t of tasks) {
|
|
267
|
-
if (!isPlainObject(t) || !t.recurring) continue;
|
|
268
|
-
const n = parseTaskIdN(t.id);
|
|
269
|
-
if (n !== null && n <= maxN) continue;
|
|
270
|
-
const from = taskKey(t);
|
|
271
|
-
const to = takeFree().toString(16).padStart(8, '0');
|
|
272
|
-
t.id = to;
|
|
273
|
-
changed += 1;
|
|
274
|
-
idChanges.push({ from, to });
|
|
275
|
-
}
|
|
276
|
-
return { changed, idChanges };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 一轮巡检对一个任务文件做的维护:renew(补 permanent)、minify(id 改
|
|
280
|
-
// 小)、reown(把任务归属盖给各自的预热会话)合并为同一次受 mtime 乐观
|
|
281
|
-
// 锁保护的原子写。renew/minify 无条件执行,没有开关。先取 mtimeNs、清残
|
|
282
|
-
// 留 .cron-up-tmp、loadTaskDoc round-trip 整文档;三项计数都为 0 时字节
|
|
283
|
-
// 级不动。返回 {renewed,minified,reowned,idChanges};null 表示文件缺失/
|
|
284
|
-
// 损坏;读到写间被改抛 TaskFileChanged。
|
|
238
|
+
// 一轮巡检对一个任务文件做的维护:renew(补 permanent)与 reown(把任务
|
|
239
|
+
// 归属盖给各自的预热会话)合并为同一次带 mtime 乐观检测的原子写。先取
|
|
240
|
+
// mtimeNs、清残留 .cron-up-tmp、loadTaskDoc round-trip 整文档;两项计数
|
|
241
|
+
// 都为 0 时字节级不动。返回 {renewed,reowned};null 表示文件缺失/损坏;
|
|
242
|
+
// 读到写间被改抛 TaskFileChanged。
|
|
285
243
|
//
|
|
286
|
-
// reown 形状:Map<
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
244
|
+
// reown 形状:Map<taskKey, {sessionId, pid, procStart}>。一轮内同一文件的
|
|
245
|
+
// 不同任务可盖向不同预热(一任务一会话);内容相同但 id 不同的任务互不
|
|
246
|
+
// 影响。同一个 taskKey 对应多条任务是病态输入,无法把 state/会话唯一对应
|
|
247
|
+
// 到条目,这些键全部跳过盖印。只盖 createdBySessionId 不等于目标的任务,
|
|
248
|
+
// 第二轮起字节级不动;pid/procStart 是登记里 claude 进程的 pid 与 lstart。
|
|
290
249
|
export function maintainWorkspace(taskfile, { reown = null } = {}) {
|
|
291
250
|
let mtimeNs;
|
|
292
251
|
try {
|
|
@@ -306,23 +265,25 @@ export function maintainWorkspace(taskfile, { reown = null } = {}) {
|
|
|
306
265
|
if (doc === null) return null;
|
|
307
266
|
let renewed = 0;
|
|
308
267
|
for (const t of doc.tasks) {
|
|
268
|
+
if (!isPlainObject(t)) continue;
|
|
309
269
|
// truthy 而非 === true:与 Claude Code 自己的加载器一致
|
|
310
270
|
// (...o.permanent && {permanent:true}),它已认作永久的任何值都算,
|
|
311
271
|
// 不再重写。只有显式周期任务有资格——绝不从 cron 形状推断周期性,
|
|
312
272
|
// 一次性任务不碰。
|
|
313
|
-
if (
|
|
273
|
+
if (t.recurring && !t.permanent) {
|
|
314
274
|
t.permanent = true;
|
|
315
275
|
renewed += 1;
|
|
316
276
|
}
|
|
317
277
|
}
|
|
318
|
-
const { changed: minified, idChanges } = minifyIds(doc.tasks);
|
|
319
278
|
let reowned = 0;
|
|
320
279
|
if (reown instanceof Map && reown.size > 0) {
|
|
321
|
-
|
|
280
|
+
// 只有确实需要盖印时才计算任务键;纯续期与常见空操作不付摘要成本。
|
|
281
|
+
for (const [key, group] of groupTasksByKey(doc.tasks)) {
|
|
282
|
+
if (group.length !== 1) continue;
|
|
283
|
+
const t = group[0];
|
|
284
|
+
const target = reown.get(key);
|
|
322
285
|
// 没有 owner 字段的外部条目同样盖——窗口内任务统一归各自预热,语义
|
|
323
286
|
// 不因条目来源留例外。
|
|
324
|
-
if (!isPlainObject(t)) continue;
|
|
325
|
-
const target = reown.get(taskSig(t));
|
|
326
287
|
if (target && t.createdBySessionId !== target.sessionId) {
|
|
327
288
|
t.createdBySessionId = target.sessionId;
|
|
328
289
|
t.createdByPid = target.pid;
|
|
@@ -332,11 +293,11 @@ export function maintainWorkspace(taskfile, { reown = null } = {}) {
|
|
|
332
293
|
}
|
|
333
294
|
}
|
|
334
295
|
}
|
|
335
|
-
if (renewed === 0 &&
|
|
336
|
-
return { renewed: 0,
|
|
296
|
+
if (renewed === 0 && reowned === 0) {
|
|
297
|
+
return { renewed: 0, reowned: 0 };
|
|
337
298
|
}
|
|
338
299
|
atomicWriteJson(taskfile, doc, mtimeNs);
|
|
339
|
-
return { renewed,
|
|
300
|
+
return { renewed, reowned };
|
|
340
301
|
}
|
|
341
302
|
|
|
342
303
|
Object.assign(deps, { discover, readTasks, loadTaskDoc });
|