cron-up 26.10.34 → 26.10.35
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 +2 -1
- package/docs/migration.md +1 -1
- package/docs/release.md +1 -1
- package/package.json +1 -1
- package/src/config.mjs +3 -2
- package/src/cron.mjs +4 -2
- package/src/internals.mjs +17 -0
- package/src/list.mjs +5 -2
- package/src/logs.mjs +4 -1
- package/src/overview.mjs +2 -2
- package/src/patrol.mjs +46 -37
- package/src/service.mjs +4 -1
- package/src/tasks.mjs +11 -3
package/CONTRIBUTING.md
CHANGED
|
@@ -26,11 +26,12 @@ CI 在 macOS 上分别使用 Node 22、24 运行全部测试。需要本机联
|
|
|
26
26
|
|
|
27
27
|
## 文档同步要求
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
用户可观察行为或维护者操作边界变化时,至少检查以下位置,避免命令、文档和实现各自描述一套口径:
|
|
30
30
|
|
|
31
31
|
| 变化 | 需要同步的位置 |
|
|
32
32
|
| --- | --- |
|
|
33
33
|
| 产品范围、安装、日常使用、限制、卸载或数据保留 | `README.md` |
|
|
34
|
+
| 维护者可操作的机器范围或自动化执行边界 | 根目录 `CLAUDE.md`;相关流程只引用该节,不复制机器名单 |
|
|
34
35
|
| 命令、选项、升级步骤或帮助文案 | `src/cli.mjs`,必要时包括 `src/overview.mjs` |
|
|
35
36
|
| npm 包定位 | `package.json` 的 `description` |
|
|
36
37
|
| 内部机制、生命周期常量、兼容性证据或投递算法 | `docs/internals.md` |
|
package/docs/migration.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
为防止 pid 复用导致误杀,缺少进程启动指纹的旧条目不会被贸然发信号:
|
|
27
27
|
|
|
28
28
|
- 对应进程已经退出时,不再需要停止它
|
|
29
|
-
-
|
|
29
|
+
- 对应进程仍在运行且无法安全核实时,巡检会明确告警并保留旧状态,在后续轮次重试,不会把“未停止”记成“已回收”
|
|
30
30
|
- 出现这类告警时,先用 `cron-up logs` 找到“旧版迁移”条目,记录其中的 pid、工作区或日志路径;旧版扁平状态不一定会出现在总览中
|
|
31
31
|
- 再把下列命令中的 12345 换成实际 pid,核对进程启动时间、父进程和完整启动命令:
|
|
32
32
|
|
package/docs/release.md
CHANGED
|
@@ -60,7 +60,7 @@ npm 包设置中需要一次性登记:
|
|
|
60
60
|
|
|
61
61
|
按 26.10.32 的实际发布记录,publish 成功后 registry 通常约两分钟可查,偶发需要七八分钟;带 provenance 时还要等待透明日志与后端入库,本机元数据缓存可能进一步滞后
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
先按仓库根目录 `CLAUDE.md` 的「使用边界」确定本次允许操作的机器,再对范围内每一台已有安装的机器分别执行:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
66
|
npm i -g cron-up@<新版本> --prefer-online
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isPlainObject,
|
|
11
11
|
isDecInt,
|
|
12
12
|
ensurePrivateDir,
|
|
13
|
+
setOwn,
|
|
13
14
|
} from './internals.mjs';
|
|
14
15
|
import { PID_T_MAX, HEARTBEAT_STALE_FACTOR } from './constants.mjs';
|
|
15
16
|
import { expandHome, isDir } from './tasks.mjs';
|
|
@@ -175,9 +176,9 @@ export function loadState() {
|
|
|
175
176
|
const cleanBucket = {};
|
|
176
177
|
for (const [taskId, raw] of Object.entries(bucket)) {
|
|
177
178
|
const ent = cleanEntry(raw, ws, taskId);
|
|
178
|
-
if (ent) cleanBucket
|
|
179
|
+
if (ent) setOwn(cleanBucket, taskId, ent);
|
|
179
180
|
}
|
|
180
|
-
if (Object.keys(cleanBucket).length > 0) out
|
|
181
|
+
if (Object.keys(cleanBucket).length > 0) setOwn(out, ws, cleanBucket);
|
|
181
182
|
}
|
|
182
183
|
return out;
|
|
183
184
|
}
|
package/src/cron.mjs
CHANGED
|
@@ -48,7 +48,9 @@ export function parseCronField(expr, lo, hi) {
|
|
|
48
48
|
start = lo;
|
|
49
49
|
end = hi;
|
|
50
50
|
} else if (rng.includes('-')) {
|
|
51
|
-
const
|
|
51
|
+
const bounds = rng.split('-');
|
|
52
|
+
if (bounds.length !== 2) throw new Error(`cron 区间格式无效:${rng}`);
|
|
53
|
+
const [a, b] = bounds;
|
|
52
54
|
start = strictInt(a);
|
|
53
55
|
end = strictInt(b);
|
|
54
56
|
} else {
|
|
@@ -173,7 +175,7 @@ export class Cron {
|
|
|
173
175
|
// CronCreate 产生的一次性任务形状像 'M H DoM Mon *',作为没有 recurring
|
|
174
176
|
// 标志时的兜底识别法。
|
|
175
177
|
function cronLooksOneshot(expr) {
|
|
176
|
-
const parts = expr.split(/\s+/);
|
|
178
|
+
const parts = expr.trim().split(/\s+/);
|
|
177
179
|
return parts.length === 5 && parts[4] === '*'
|
|
178
180
|
&& parts.slice(0, 4).every((p) => /^\d+$/.test(p));
|
|
179
181
|
}
|
package/src/internals.mjs
CHANGED
|
@@ -26,6 +26,23 @@ export function isPlainObject(v) {
|
|
|
26
26
|
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// state 的桶键来自外部任务 id。普通对象的 __proto__ 赋值会改原型而不是
|
|
30
|
+
// 建条目,读取还会把继承来的 Object.prototype 当成 entry;所有外部键都
|
|
31
|
+
// 通过自有属性访问,保持 JSON 形状不变,也无需特殊的任务 id 黑名单。
|
|
32
|
+
export function getOwn(obj, key) {
|
|
33
|
+
return obj != null && Object.hasOwn(obj, key) ? obj[key] : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setOwn(obj, key, value) {
|
|
37
|
+
Object.defineProperty(obj, key, {
|
|
38
|
+
value,
|
|
39
|
+
writable: true,
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
});
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
// 严格十进制整数(可选负号 + 纯数字):parseInt("5x")、Number("0x10") 都会
|
|
30
47
|
// 静默给出错误结果,来自外部字符串的整数一律先过这个谓词再 parseInt。
|
|
31
48
|
export function isDecInt(s) {
|
package/src/list.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// `cron-up list`:逐工作区列出任务的下次触发、节奏、类型、摘要与该任务
|
|
2
2
|
// 的预热状态。
|
|
3
3
|
|
|
4
|
-
import { deps } from './internals.mjs';
|
|
4
|
+
import { deps, getOwn } from './internals.mjs';
|
|
5
5
|
import { LEAD_SECONDS } from './constants.mjs';
|
|
6
6
|
import { loadConfig, loadState, isCooling } from './config.mjs';
|
|
7
7
|
import { taskKey } from './tasks.mjs';
|
|
@@ -70,7 +70,7 @@ export async function cmdList(args) {
|
|
|
70
70
|
return {
|
|
71
71
|
v,
|
|
72
72
|
t,
|
|
73
|
-
tag: warmTag(t, v, state[ws]
|
|
73
|
+
tag: warmTag(t, v, getOwn(state[ws], taskKey(t)), liveSids, ws, cur),
|
|
74
74
|
};
|
|
75
75
|
});
|
|
76
76
|
// 错过待补执行排最前,然后按下次时间升序,一年内无安排的垫底,无效
|
|
@@ -96,6 +96,9 @@ export async function cmdList(args) {
|
|
|
96
96
|
|
|
97
97
|
blocks.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
98
98
|
deps.print(`共 ${blocks.length} 个工作区、${total} 个定时任务`);
|
|
99
|
+
if (unreadable > 0) {
|
|
100
|
+
deps.print(`另有 ${unreadable} 个任务文件读不出,其中的定时任务不会执行`);
|
|
101
|
+
}
|
|
99
102
|
|
|
100
103
|
// 列宽从全部实际文案动态算;cadence 是截断列,取固定上限。
|
|
101
104
|
const statusW = Math.max(11,
|
package/src/logs.mjs
CHANGED
|
@@ -77,5 +77,8 @@ export function cmdLogs(args) {
|
|
|
77
77
|
tailArgs.push(file);
|
|
78
78
|
const r = deps.spawnSync('/usr/bin/tail', tailArgs, { stdio: 'inherit' });
|
|
79
79
|
if (r.error) throw r.error;
|
|
80
|
-
if (r.
|
|
80
|
+
if (r.signal) {
|
|
81
|
+
throw new ExitError(1, `日志查看进程被信号 ${r.signal} 终止`);
|
|
82
|
+
}
|
|
83
|
+
if (r.status !== 0) throw new ExitError(r.status ?? 1);
|
|
81
84
|
}
|
package/src/overview.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
import { deps, packageVersion } from './internals.mjs';
|
|
8
|
+
import { deps, packageVersion, getOwn } from './internals.mjs';
|
|
9
9
|
import {
|
|
10
10
|
INTERVAL_SECONDS,
|
|
11
11
|
LEAD_SECONDS,
|
|
@@ -87,7 +87,7 @@ function overviewTasks(cfg, cols, details, state) {
|
|
|
87
87
|
// 承接,都不是异常;否则按 entry 的冷却/失败状态给一条可执行的告
|
|
88
88
|
// 警。
|
|
89
89
|
const key = taskKey(t);
|
|
90
|
-
const ent = state[ws]
|
|
90
|
+
const ent = getOwn(state[ws], key);
|
|
91
91
|
if (deps.creatorSessionAlive(t, liveSids) || entryHolds(ent, details, ws)) {
|
|
92
92
|
continue;
|
|
93
93
|
}
|
package/src/patrol.mjs
CHANGED
|
@@ -10,7 +10,13 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
deps,
|
|
15
|
+
packageVersion,
|
|
16
|
+
isPlainObject,
|
|
17
|
+
getOwn,
|
|
18
|
+
setOwn,
|
|
19
|
+
} from './internals.mjs';
|
|
14
20
|
import {
|
|
15
21
|
FAIL_LIMIT,
|
|
16
22
|
COOLDOWN_SECONDS,
|
|
@@ -41,6 +47,7 @@ import {
|
|
|
41
47
|
TaskFileChanged,
|
|
42
48
|
taskFileExists,
|
|
43
49
|
groupTasksByKey,
|
|
50
|
+
taskOwnedBy,
|
|
44
51
|
} from './tasks.mjs';
|
|
45
52
|
import { taskWanted, parseCronOrNone, wantedNow } from './cron.mjs';
|
|
46
53
|
import {
|
|
@@ -381,7 +388,7 @@ export async function patrolWorkspace(
|
|
|
381
388
|
for (const [key, group] of taskGroups) {
|
|
382
389
|
if (group.length !== 1) continue;
|
|
383
390
|
const t = group[0];
|
|
384
|
-
let ent = bucket
|
|
391
|
+
let ent = getOwn(bucket, key) ?? null;
|
|
385
392
|
const logPath = sessionLogPath(ws, key);
|
|
386
393
|
const ownerIsOurs = entryClaimed(ent)
|
|
387
394
|
&& typeof t.createdBySessionId === 'string'
|
|
@@ -476,9 +483,9 @@ export async function patrolWorkspace(
|
|
|
476
483
|
// 盖印(跨轮老条目):已认亲且任务归属还没盖到本代预热(活创建者
|
|
477
484
|
// 已在 (a) 排除)。当轮新 spawn 的会话在第三步之后的轮内等待里认
|
|
478
485
|
// 亲,由 cmdRun 的 stampClaimed 补盖,不走这里。
|
|
479
|
-
if (entryClaimed(ent)
|
|
480
|
-
|
|
481
|
-
stamps.set(key,
|
|
486
|
+
if (entryClaimed(ent)) {
|
|
487
|
+
const target = reownTarget(ent);
|
|
488
|
+
if (!taskOwnedBy(t, target)) stamps.set(key, target);
|
|
482
489
|
}
|
|
483
490
|
|
|
484
491
|
// warm 未到回收 / spawning 宽限内:本轮无事。
|
|
@@ -496,15 +503,15 @@ export async function patrolWorkspace(
|
|
|
496
503
|
}
|
|
497
504
|
|
|
498
505
|
if (fails >= FAIL_LIMIT) {
|
|
499
|
-
bucket
|
|
506
|
+
const next = setOwn(bucket, key, {
|
|
500
507
|
...ent,
|
|
501
508
|
ws,
|
|
502
509
|
taskId: key,
|
|
503
510
|
pid: null,
|
|
504
511
|
fails,
|
|
505
512
|
log: ent?.log ?? logPath,
|
|
506
|
-
};
|
|
507
|
-
enterCooldown(
|
|
513
|
+
});
|
|
514
|
+
enterCooldown(next, ws, key, fails, cur, stats);
|
|
508
515
|
continue;
|
|
509
516
|
}
|
|
510
517
|
|
|
@@ -520,7 +527,7 @@ export async function patrolWorkspace(
|
|
|
520
527
|
scriptProcLogPath(procArgs) === logPath && deps.alive(procPid));
|
|
521
528
|
if (adopted) {
|
|
522
529
|
const [procPid] = adopted;
|
|
523
|
-
bucket
|
|
530
|
+
setOwn(bucket, key, {
|
|
524
531
|
ws,
|
|
525
532
|
taskId: key,
|
|
526
533
|
pid: procPid,
|
|
@@ -528,7 +535,7 @@ export async function patrolWorkspace(
|
|
|
528
535
|
procStart: deps.procStartedAt(procPid),
|
|
529
536
|
fails: 0,
|
|
530
537
|
log: logPath,
|
|
531
|
-
};
|
|
538
|
+
});
|
|
532
539
|
note('adopt', `接管 ${ws} 任务 ${key} pid=${procPid}:state 记录缺失,`
|
|
533
540
|
+ '但进程表里仍有本该任务的预热会话,重新纳入跟踪');
|
|
534
541
|
pending.push({ ws, key, pid: procPid });
|
|
@@ -542,25 +549,25 @@ export async function patrolWorkspace(
|
|
|
542
549
|
if (spawned?.pid == null) {
|
|
543
550
|
const newFails = fails + 1;
|
|
544
551
|
if (newFails >= FAIL_LIMIT) {
|
|
545
|
-
bucket
|
|
552
|
+
const next = setOwn(bucket, key, {
|
|
546
553
|
ws, taskId: key, pid: null, fails: newFails, log: logPath,
|
|
547
|
-
};
|
|
548
|
-
enterCooldown(
|
|
554
|
+
});
|
|
555
|
+
enterCooldown(next, ws, key, newFails, cur, stats);
|
|
549
556
|
continue;
|
|
550
557
|
}
|
|
551
|
-
bucket
|
|
558
|
+
setOwn(bucket, key, {
|
|
552
559
|
ws,
|
|
553
560
|
taskId: key,
|
|
554
561
|
pid: null,
|
|
555
562
|
startedAt: cur,
|
|
556
563
|
fails: newFails,
|
|
557
564
|
log: logPath,
|
|
558
|
-
};
|
|
565
|
+
});
|
|
559
566
|
note('fail', `启动失败 ${ws} 任务 ${key}:会话进程没有拉起`
|
|
560
567
|
+ `(连续失败计数 ${newFails})`);
|
|
561
568
|
continue;
|
|
562
569
|
}
|
|
563
|
-
bucket
|
|
570
|
+
setOwn(bucket, key, {
|
|
564
571
|
ws,
|
|
565
572
|
taskId: key,
|
|
566
573
|
pid: spawned.pid,
|
|
@@ -568,7 +575,7 @@ export async function patrolWorkspace(
|
|
|
568
575
|
procStart: spawned.procStart,
|
|
569
576
|
fails,
|
|
570
577
|
log: logPath,
|
|
571
|
-
};
|
|
578
|
+
});
|
|
572
579
|
note('spawn', `已启动 ${ws} 任务 ${key} pid=${spawned.pid}`
|
|
573
580
|
+ `(连续失败计数 ${fails})`);
|
|
574
581
|
pending.push({ ws, key, pid: spawned.pid });
|
|
@@ -588,8 +595,9 @@ export async function patrolWorkspace(
|
|
|
588
595
|
}
|
|
589
596
|
throw e;
|
|
590
597
|
}
|
|
591
|
-
// 与 readTasks
|
|
592
|
-
|
|
598
|
+
// 与 readTasks 不一致的极端竞态:维护保守不动,但本轮新 spawn 仍要进入
|
|
599
|
+
// 等待认亲与补盖流程;丢掉 pending 会把同轮收尾无故推迟到下一轮。
|
|
600
|
+
if (result === null) return pending;
|
|
593
601
|
if (result.renewed) {
|
|
594
602
|
deps.log(`已续期 ${ws}:${result.renewed} 个周期任务标记为 permanent,`
|
|
595
603
|
+ '不再受 7 天过期限制');
|
|
@@ -628,7 +636,7 @@ async function awaitWarmups(pending, state, cur, stats) {
|
|
|
628
636
|
const recognize = () => {
|
|
629
637
|
const rest = [];
|
|
630
638
|
for (const p of pending) {
|
|
631
|
-
const ent = state[p.ws]
|
|
639
|
+
const ent = getOwn(state[p.ws], p.key);
|
|
632
640
|
if (!ent || ent.pid !== p.pid) continue;
|
|
633
641
|
if (!deps.trackedAlive(ent)) {
|
|
634
642
|
// 启动即崩的典型窗口:真拉起过却死掉,计一次失败并置空 pid,条
|
|
@@ -696,10 +704,12 @@ function stampClaimed(claimedByWs, taskfiles, stats) {
|
|
|
696
704
|
// 拉);state.json 已丢的旧孤儿 script(日志是旧命名,不含任务键段)从
|
|
697
705
|
// 进程表兜底清扫。只在 loadLegacyEntries 非空(确在跨版本)时执行。
|
|
698
706
|
async function retireLegacy(legacy, stats) {
|
|
707
|
+
let retired = true;
|
|
699
708
|
for (const ent of legacy) {
|
|
700
709
|
if (deps.trackedAlive(ent)) {
|
|
701
710
|
const stopped = await deps.stopSession(ent);
|
|
702
711
|
if (!stopped) {
|
|
712
|
+
retired = false;
|
|
703
713
|
bump(stats, 'warn');
|
|
704
714
|
deps.log(`旧版迁移暂缓 ${ent.ws} pid=${ent.pid}:记录的进程仍存活,`
|
|
705
715
|
+ '本次无法安全停止目录级预热会话;本次不记为已回收');
|
|
@@ -721,6 +731,7 @@ async function retireLegacy(legacy, stats) {
|
|
|
721
731
|
if (!deps.trackedAlive(ent)) continue;
|
|
722
732
|
const stopped = await deps.stopSession(ent);
|
|
723
733
|
if (!stopped) {
|
|
734
|
+
retired = false;
|
|
724
735
|
bump(stats, 'warn');
|
|
725
736
|
deps.log(`旧版迁移暂缓 pid=${pid}:记录的进程仍存活,本次无法安全停止`
|
|
726
737
|
+ `目录级预热孤儿会话(日志 ${file});本次不记为已回收`);
|
|
@@ -730,6 +741,7 @@ async function retireLegacy(legacy, stats) {
|
|
|
730
741
|
+ '升级后按任务重新预热');
|
|
731
742
|
}
|
|
732
743
|
}
|
|
744
|
+
return retired;
|
|
733
745
|
}
|
|
734
746
|
|
|
735
747
|
// 跑一轮巡检;抢不到锁返回 false。
|
|
@@ -793,8 +805,9 @@ export async function cmdRun(args) {
|
|
|
793
805
|
deps.log(`告警:${registryAlert}`);
|
|
794
806
|
}
|
|
795
807
|
|
|
796
|
-
// 旧版收尾必须先于一切 spawn
|
|
797
|
-
|
|
808
|
+
// 旧版收尾必须先于一切 spawn 决策。停止失败时轮末保留原 state,下轮
|
|
809
|
+
// 才能继续重试;本轮新会话可由任务专属日志认领,不会因未落盘而重拉。
|
|
810
|
+
const legacyRetired = legacy.length === 0 || await retireLegacy(legacy, stats);
|
|
798
811
|
|
|
799
812
|
// 轮初把上轮末仍相信活着的条目分成三组:活、resume 脱离(script 已
|
|
800
813
|
// 死但 sid 还在,不报退出)、真死(立即报)。带 deadSince 的旧报条
|
|
@@ -865,7 +878,7 @@ export async function cmdRun(args) {
|
|
|
865
878
|
for (const d of died) {
|
|
866
879
|
bump(stats, 'exit');
|
|
867
880
|
deps.log(exitLine(d.ws, d.key,
|
|
868
|
-
{ ...state[d.ws]
|
|
881
|
+
{ ...getOwn(state[d.ws], d.key), pid: d.pid }));
|
|
869
882
|
}
|
|
870
883
|
stampClaimed(claimedByWs, taskfiles, stats);
|
|
871
884
|
} catch (e) {
|
|
@@ -894,27 +907,23 @@ export async function cmdRun(args) {
|
|
|
894
907
|
for (const [ws, bucket] of Object.entries(state)) {
|
|
895
908
|
const out = {};
|
|
896
909
|
for (const [taskId, ent] of Object.entries(bucket)) {
|
|
897
|
-
if (deps.trackedAlive(ent)
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
&& sidInDetails(ws, ent.sessionId)) {
|
|
903
|
-
out[taskId] = ent;
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
if (isCooling(ent, cur)) {
|
|
907
|
-
out[taskId] = ent;
|
|
910
|
+
if (deps.trackedAlive(ent)
|
|
911
|
+
|| (typeof ent.sessionId === 'string'
|
|
912
|
+
&& sidInDetails(ws, ent.sessionId))
|
|
913
|
+
|| isCooling(ent, cur)) {
|
|
914
|
+
setOwn(out, taskId, ent);
|
|
908
915
|
continue;
|
|
909
916
|
}
|
|
910
917
|
if ((ent.fails ?? 0) > 0) {
|
|
911
918
|
ent.deadSince ??= cur;
|
|
912
|
-
if (cur - ent.deadSince < FAIL_TTL_SECONDS)
|
|
919
|
+
if (cur - ent.deadSince < FAIL_TTL_SECONDS) {
|
|
920
|
+
setOwn(out, taskId, ent);
|
|
921
|
+
}
|
|
913
922
|
}
|
|
914
923
|
}
|
|
915
|
-
if (Object.keys(out).length > 0) kept
|
|
924
|
+
if (Object.keys(out).length > 0) setOwn(kept, ws, out);
|
|
916
925
|
}
|
|
917
|
-
saveState(kept);
|
|
926
|
+
if (legacyRetired) saveState(kept);
|
|
918
927
|
|
|
919
928
|
// 轮末汇总:一行交代这轮看见了什么、动了什么、花了多久,随后更新心跳。
|
|
920
929
|
const actions = EVENT_LABELS
|
package/src/service.mjs
CHANGED
|
@@ -292,7 +292,10 @@ function scriptPathDirs(scriptPath) {
|
|
|
292
292
|
export function launcherHealth() {
|
|
293
293
|
if (!fs.existsSync(deps.paths.plistPath)) return null;
|
|
294
294
|
const args = readPlistProgramArgs();
|
|
295
|
-
if (!args || args.length !== 1)
|
|
295
|
+
if (!args || args.length !== 1) {
|
|
296
|
+
return `LaunchAgent ${deps.paths.plistPath} 的 ProgramArguments 无法解读,`
|
|
297
|
+
+ '重跑 cron-up install 覆盖重生成';
|
|
298
|
+
}
|
|
296
299
|
const [scriptPath] = args;
|
|
297
300
|
if (!fs.existsSync(scriptPath)) {
|
|
298
301
|
return `launchd 配置中的启动脚本已失效(${path.basename(scriptPath)} `
|
package/src/tasks.mjs
CHANGED
|
@@ -235,6 +235,14 @@ export function atomicWriteJson(file, doc, expectedMtimeNs) {
|
|
|
235
235
|
fs.renameSync(tmp, file);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 任务归属是否已完全等于盖印目标。巡检预判与实际写入共用同一判据,
|
|
239
|
+
// 避免一层只看 sessionId、另一层比较三字段而逐渐漂移。
|
|
240
|
+
export function taskOwnedBy(t, target) {
|
|
241
|
+
return t.createdBySessionId === target.sessionId
|
|
242
|
+
&& t.createdByPid === target.pid
|
|
243
|
+
&& t.createdByProcStart === target.procStart;
|
|
244
|
+
}
|
|
245
|
+
|
|
238
246
|
// 一轮巡检对一个任务文件做的维护:renew(补 permanent)与 reown(把任务
|
|
239
247
|
// 归属盖给各自的预热会话)合并为同一次带 mtime 乐观检测的原子写。先取
|
|
240
248
|
// mtimeNs、清残留 .cron-up-tmp、loadTaskDoc round-trip 整文档;两项计数
|
|
@@ -244,8 +252,8 @@ export function atomicWriteJson(file, doc, expectedMtimeNs) {
|
|
|
244
252
|
// reown 形状:Map<taskKey, {sessionId, pid, procStart}>。一轮内同一文件的
|
|
245
253
|
// 不同任务可盖向不同预热(一任务一会话);内容相同但 id 不同的任务互不
|
|
246
254
|
// 影响。同一个 taskKey 对应多条任务是病态输入,无法把 state/会话唯一对应
|
|
247
|
-
//
|
|
248
|
-
//
|
|
255
|
+
// 到条目,这些键全部跳过盖印。归属三字段未完全等于目标时才写,第二轮起
|
|
256
|
+
// 字节级不动;pid/procStart 是登记里 claude 进程的 pid 与 lstart。
|
|
249
257
|
export function maintainWorkspace(taskfile, { reown = null } = {}) {
|
|
250
258
|
let mtimeNs;
|
|
251
259
|
try {
|
|
@@ -284,7 +292,7 @@ export function maintainWorkspace(taskfile, { reown = null } = {}) {
|
|
|
284
292
|
const target = reown.get(key);
|
|
285
293
|
// 没有 owner 字段的外部条目同样盖——窗口内任务统一归各自预热,语义
|
|
286
294
|
// 不因条目来源留例外。
|
|
287
|
-
if (target && t
|
|
295
|
+
if (target && !taskOwnedBy(t, target)) {
|
|
288
296
|
t.createdBySessionId = target.sessionId;
|
|
289
297
|
t.createdByPid = target.pid;
|
|
290
298
|
// ps lstart 原文(含「Oct 8」双空格),原样取自会话登记。
|