@mulmoclaude/core 4.4.0 → 4.4.2
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/assets/helps/custom-view.md +69 -1
- package/dist/plugin-vue/index.cjs +201 -66
- package/dist/plugin-vue/index.cjs.map +1 -1
- package/dist/plugin-vue/index.js +201 -66
- package/dist/plugin-vue/index.js.map +1 -1
- package/dist/scheduler/index.cjs +116 -63
- package/dist/scheduler/index.cjs.map +1 -1
- package/dist/scheduler/index.js +117 -64
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/schedule-window.d.ts +35 -0
- package/dist/scheduler/task-manager.d.ts +2 -8
- package/package.json +6 -6
package/dist/scheduler/index.cjs
CHANGED
|
@@ -6,11 +6,86 @@ let node_path = require("node:path");
|
|
|
6
6
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
7
7
|
let node_fs_promises = require("node:fs/promises");
|
|
8
8
|
let _receptron_task_scheduler = require("@receptron/task-scheduler");
|
|
9
|
-
//#region src/scheduler/
|
|
9
|
+
//#region src/scheduler/schedule-window.ts
|
|
10
10
|
var ONE_SECOND_MS$1 = 1e3;
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
var ONE_DAY_MS = 86400 * ONE_SECOND_MS$1;
|
|
12
|
+
/** An interval this engine can never turn into a window. Zero divides by zero
|
|
13
|
+
* inside the library's `Math.ceil(afterMs / intervalMs)`; a negative or
|
|
14
|
+
* non-finite one poisons the arithmetic with NaN, which compares false
|
|
15
|
+
* everywhere and so hides itself. */
|
|
16
|
+
function isUsableInterval(intervalMs) {
|
|
17
|
+
return Number.isFinite(intervalMs) && intervalMs > 0;
|
|
18
|
+
}
|
|
19
|
+
/** Milliseconds past UTC midnight for a daily `HH:MM`, or null when the string
|
|
20
|
+
* does not name a time of day. Out-of-range values are rejected, not clamped
|
|
21
|
+
* and not wrapped: `"24:00"` would otherwise land on the NEXT day's midnight
|
|
22
|
+
* and `"-1:00"` on an hour before it, so a typo that used to be inert would
|
|
23
|
+
* start firing — at 00:00 daily, or 61 times a day. */
|
|
24
|
+
function dailyOffsetMs(time) {
|
|
25
|
+
const offsetMs = (0, _receptron_task_scheduler.parseTimeToMs)(time);
|
|
26
|
+
return Number.isFinite(offsetMs) && offsetMs >= 0 && offsetMs < ONE_DAY_MS ? offsetMs : null;
|
|
27
|
+
}
|
|
28
|
+
/** Convert to the library's schedule shape. Milliseconds are divided, never
|
|
29
|
+
* rounded: rounding a sub-second interval to `intervalSec: 0` is what made
|
|
30
|
+
* `nextWindowAfter` answer NaN and `new Date(NaN).toISOString()` throw. */
|
|
31
|
+
function toLibrarySchedule(schedule) {
|
|
32
|
+
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval) return {
|
|
33
|
+
type: _receptron_task_scheduler.SCHEDULE_TYPES.interval,
|
|
34
|
+
intervalSec: schedule.intervalMs / ONE_SECOND_MS$1
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
type: _receptron_task_scheduler.SCHEDULE_TYPES.daily,
|
|
38
|
+
time: schedule.time
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** True when a scheduled window falls inside the tick ending at `nowMs`.
|
|
42
|
+
* Windows are anchored to the epoch, not to the current UTC day, so an
|
|
43
|
+
* interval longer than 24h keeps its real period. */
|
|
44
|
+
function isScheduleDueAt(schedule, nowMs, tickMs) {
|
|
45
|
+
if (unfireableScheduleReason(schedule) !== null) return false;
|
|
46
|
+
return (0, _receptron_task_scheduler.isDueAt)(toLibrarySchedule(schedule), nowMs, tickMs);
|
|
47
|
+
}
|
|
48
|
+
/** The furthest instant `Date` can represent. A window past it is finite
|
|
49
|
+
* arithmetic but not a date, and `toISOString()` answers that with a throw —
|
|
50
|
+
* which, inside the state writer, silently loses the whole run record. */
|
|
51
|
+
var MAX_DATE_MS = 864e13;
|
|
52
|
+
/** A window as an ISO timestamp, or null when the number is not a date. */
|
|
53
|
+
function windowToIso(windowMs) {
|
|
54
|
+
if (windowMs === null || !Number.isFinite(windowMs) || Math.abs(windowMs) > MAX_DATE_MS) return null;
|
|
55
|
+
return new Date(windowMs).toISOString();
|
|
56
|
+
}
|
|
57
|
+
/** The most recent window at or before `nowMs`, or null when the schedule has
|
|
58
|
+
* none. `nextWindowAfter` only ever looks FORWARD, so asking it from
|
|
59
|
+
* `nowMs - one period` answers the previous window whenever `nowMs` sits
|
|
60
|
+
* exactly on a boundary, and a daily schedule read after its minute answers
|
|
61
|
+
* TOMORROW. Both schedule kinds have a fixed period, so stepping one period
|
|
62
|
+
* back off the next window is exact. */
|
|
63
|
+
function latestWindowAtOrBefore(schedule, nowMs) {
|
|
64
|
+
if (unfireableScheduleReason(schedule) !== null) return null;
|
|
65
|
+
const next = (0, _receptron_task_scheduler.nextWindowAfter)(toLibrarySchedule(schedule), nowMs);
|
|
66
|
+
if (next === null || !Number.isFinite(next)) return null;
|
|
67
|
+
if (next <= nowMs) return next;
|
|
68
|
+
return next - (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval ? schedule.intervalMs : ONE_DAY_MS);
|
|
69
|
+
}
|
|
70
|
+
/** Name the field that makes a schedule unfireable, or null when it is fine.
|
|
71
|
+
* Returns the offending value (not a boolean) so the caller can put it in the
|
|
72
|
+
* log — a task that silently never runs leaves no evidence to start from
|
|
73
|
+
* (#2765). */
|
|
74
|
+
function unfireableScheduleReason(schedule) {
|
|
75
|
+
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval) return isUsableInterval(schedule.intervalMs) ? null : {
|
|
76
|
+
field: "intervalMs",
|
|
77
|
+
value: String(schedule.intervalMs)
|
|
78
|
+
};
|
|
79
|
+
return dailyOffsetMs(schedule.time) !== null ? null : {
|
|
80
|
+
field: "time",
|
|
81
|
+
value: schedule.time
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/scheduler/task-manager.ts
|
|
86
|
+
var ONE_SECOND_MS = 1e3;
|
|
87
|
+
var ONE_MINUTE_MS = 60 * ONE_SECOND_MS;
|
|
88
|
+
var DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;
|
|
14
89
|
var MAX_STAGGER_FRACTION_OF_TICK = .5;
|
|
15
90
|
var realSleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
16
91
|
function staggerStepMs(staggerMs, tickMs, count) {
|
|
@@ -24,34 +99,7 @@ var NOOP_LOG$1 = {
|
|
|
24
99
|
error: () => {}
|
|
25
100
|
};
|
|
26
101
|
function isDue(now, schedule, tickMs) {
|
|
27
|
-
|
|
28
|
-
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS$1;
|
|
29
|
-
return Math.floor(msSinceMidnight / tickMs) * tickMs % schedule.intervalMs === 0;
|
|
30
|
-
}
|
|
31
|
-
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.daily) {
|
|
32
|
-
const targetMs = dailyTargetMs(schedule.time);
|
|
33
|
-
if (targetMs === null) return false;
|
|
34
|
-
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS$1;
|
|
35
|
-
return Math.floor(msSinceMidnight / tickMs) * tickMs === targetMs;
|
|
36
|
-
}
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
/** Milliseconds past UTC midnight for a `HH:MM` daily time, or null when the
|
|
40
|
-
* string isn't that shape. `TaskSchedule.time` is a bare `string` on a
|
|
41
|
-
* published package, so a host that skips its own validation must land on
|
|
42
|
-
* "never due" rather than on a garbage firing time. */
|
|
43
|
-
function dailyTargetMs(time) {
|
|
44
|
-
const [hours, minutes] = time.split(":").map(Number);
|
|
45
|
-
if (hours === void 0 || minutes === void 0) return null;
|
|
46
|
-
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
|
|
47
|
-
return hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;
|
|
48
|
-
}
|
|
49
|
-
/** The `time` of a daily schedule this manager can never fire, or null when the
|
|
50
|
-
* schedule is fine. Returning the offending value (not a boolean) is what lets
|
|
51
|
-
* the caller name it in the log. */
|
|
52
|
-
function unfireableDailyTime(schedule) {
|
|
53
|
-
if (schedule.type !== _receptron_task_scheduler.SCHEDULE_TYPES.daily) return null;
|
|
54
|
-
return dailyTargetMs(schedule.time) === null ? schedule.time : null;
|
|
102
|
+
return isScheduleDueAt(schedule, now.getTime(), tickMs);
|
|
55
103
|
}
|
|
56
104
|
/** Say out loud that this schedule can never fire. The task is still accepted —
|
|
57
105
|
* `isDue` answers false forever, which is the safe outcome — but silence here
|
|
@@ -59,11 +107,11 @@ function unfireableDailyTime(schedule) {
|
|
|
59
107
|
* evidence anywhere to start from (#2765). Deliberately not a throw: a
|
|
60
108
|
* consumer whose task has been quietly dead would get a boot crash instead. */
|
|
61
109
|
function reportUnfireable(log, taskId, schedule) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
log.error(
|
|
110
|
+
const reason = unfireableScheduleReason(schedule);
|
|
111
|
+
if (reason === null) return;
|
|
112
|
+
log.error(`schedule has an unusable ${reason.field} — this task will never run`, {
|
|
65
113
|
id: taskId,
|
|
66
|
-
|
|
114
|
+
[reason.field]: reason.value
|
|
67
115
|
});
|
|
68
116
|
}
|
|
69
117
|
/** Split the due tasks into those that may run immediately and those gated
|
|
@@ -206,7 +254,6 @@ function createTaskManager(options) {
|
|
|
206
254
|
}
|
|
207
255
|
//#endregion
|
|
208
256
|
//#region src/scheduler/adapter.ts
|
|
209
|
-
var ONE_SECOND_MS = 1e3;
|
|
210
257
|
var SCHEDULER_CONFIG_DIR = "config/scheduler";
|
|
211
258
|
var SCHEDULER_DATA_DIR = "data/scheduler/logs";
|
|
212
259
|
var NOOP_LOG = {
|
|
@@ -259,14 +306,7 @@ async function initScheduler(taskManager, tasks) {
|
|
|
259
306
|
systemTasks.length = 0;
|
|
260
307
|
systemTasks.push(...tasks);
|
|
261
308
|
taskManagerRef = taskManager;
|
|
262
|
-
const
|
|
263
|
-
id: taskDef.id,
|
|
264
|
-
name: taskDef.name,
|
|
265
|
-
schedule: toCoreSchedule(taskDef.schedule),
|
|
266
|
-
missedRunPolicy: taskDef.missedRunPolicy,
|
|
267
|
-
enabled: true
|
|
268
|
-
}));
|
|
269
|
-
const plan = (0, _receptron_task_scheduler.computeCatchUpPlan)(catchUpTasks, stateMap, Date.now());
|
|
309
|
+
const plan = (0, _receptron_task_scheduler.computeCatchUpPlan)(catchUpTasksFor(tasks), stateMap, Date.now());
|
|
270
310
|
for (const skip of plan.skipped) {
|
|
271
311
|
logger().info("catch-up skipped", {
|
|
272
312
|
taskId: skip.taskId,
|
|
@@ -286,8 +326,8 @@ async function initScheduler(taskManager, tasks) {
|
|
|
286
326
|
id: task.id,
|
|
287
327
|
description: task.description,
|
|
288
328
|
schedule: task.schedule,
|
|
289
|
-
run: async () => {
|
|
290
|
-
const windowIso = computeCurrentWindow(task);
|
|
329
|
+
run: async (ctx) => {
|
|
330
|
+
const windowIso = computeCurrentWindow(task, ctx.now.getTime());
|
|
291
331
|
await executeAndLog(task, windowIso, _receptron_task_scheduler.TASK_TRIGGERS.scheduled);
|
|
292
332
|
}
|
|
293
333
|
});
|
|
@@ -296,6 +336,28 @@ async function initScheduler(taskManager, tasks) {
|
|
|
296
336
|
stateEntries: stateMap.size
|
|
297
337
|
});
|
|
298
338
|
}
|
|
339
|
+
/** The catch-up input, with the tasks that can never fire left out. Their
|
|
340
|
+
* windows come back NaN rather than null, which `listMissedWindows` cannot
|
|
341
|
+
* see the end of — it fills to its cap and the first `new Date(NaN)` throws
|
|
342
|
+
* `RangeError`, taking every other task's catch-up down with it at startup.
|
|
343
|
+
* Registration still happens, so the task-manager reports the bad schedule. */
|
|
344
|
+
function catchUpTasksFor(tasks) {
|
|
345
|
+
return tasks.filter((taskDef) => {
|
|
346
|
+
const reason = unfireableScheduleReason(taskDef.schedule);
|
|
347
|
+
if (reason === null) return true;
|
|
348
|
+
logger().error(`schedule has an unusable ${reason.field} — skipping catch-up, this task will never run`, {
|
|
349
|
+
taskId: taskDef.id,
|
|
350
|
+
[reason.field]: reason.value
|
|
351
|
+
});
|
|
352
|
+
return false;
|
|
353
|
+
}).map((taskDef) => ({
|
|
354
|
+
id: taskDef.id,
|
|
355
|
+
name: taskDef.name,
|
|
356
|
+
schedule: toLibrarySchedule(taskDef.schedule),
|
|
357
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
358
|
+
enabled: true
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
299
361
|
/** Apply a schedule override to a running system task. Updates the
|
|
300
362
|
* in-memory task definition, the task-manager, and recalculates
|
|
301
363
|
* nextScheduledAt in persisted state. */
|
|
@@ -449,24 +511,15 @@ async function safeUpdateState(taskId, patch) {
|
|
|
449
511
|
/** Compute the window boundary that the current tick belongs to. For
|
|
450
512
|
* scheduled runs, this is the epoch-aligned window — not the wall-clock
|
|
451
513
|
* time of execution. This keeps lastRunAt consistent with catch-up's
|
|
452
|
-
* window-based accounting.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return windowMs !== null && windowMs <= nowMs ? new Date(windowMs).toISOString() : new Date(nowMs).toISOString();
|
|
514
|
+
* window-based accounting. `tickMs` is the tick's own clock, not
|
|
515
|
+
* `Date.now()`: the two drift apart by however long the tick took to reach
|
|
516
|
+
* this task, and under an injected clock they are unrelated. */
|
|
517
|
+
function computeCurrentWindow(task, tickMs) {
|
|
518
|
+
return windowToIso(latestWindowAtOrBefore(task.schedule, tickMs)) ?? new Date(tickMs).toISOString();
|
|
458
519
|
}
|
|
459
520
|
function computeNextScheduledFor(schedule) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
return next !== null ? new Date(next).toISOString() : null;
|
|
463
|
-
}
|
|
464
|
-
function toCoreSchedule(schedule) {
|
|
465
|
-
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval) return {
|
|
466
|
-
type: _receptron_task_scheduler.SCHEDULE_TYPES.interval,
|
|
467
|
-
intervalSec: Math.round(schedule.intervalMs / ONE_SECOND_MS)
|
|
468
|
-
};
|
|
469
|
-
return schedule;
|
|
521
|
+
if (unfireableScheduleReason(schedule) !== null) return null;
|
|
522
|
+
return windowToIso((0, _receptron_task_scheduler.nextWindowAfter)(toLibrarySchedule(schedule), Date.now() + 1));
|
|
470
523
|
}
|
|
471
524
|
//#endregion
|
|
472
525
|
Object.defineProperty(exports, "TASK_TRIGGERS", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { SCHEDULE_TYPES } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\nconst ONE_HOUR_MS = 60 * ONE_MINUTE_MS;\n\n// Gap between the START of each independently-due task within one tick. When\n// many tasks come due at the same minute (system journal + feed-refresh + a few\n// user tasks all at 20:00 UTC), firing every chat in the same event-loop turn\n// floods the machine; the mulmoclaude MCP broker each chat spawns then boots\n// under contention and can lose the startup race to the CLI's first tool call,\n// so that turn fails with `handlePermission not found` (#2057). Spacing the\n// launches by a second gives each broker room to connect. Far under one tick\n// (a handful of tasks spread over a few seconds), so nothing is delayed past\n// its window.\nconst DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;\n\n// The total stagger must stay well inside one tick, or the tail tasks start in a\n// LATER tick window and two ticks overlap. Cap all starts to this fraction of\n// `tickMs` regardless of task count or the configured gap — this keeps a debug\n// `tickMs === firingStaggerMs` (see server boot) from degenerating.\nconst MAX_STAGGER_FRACTION_OF_TICK = 0.5;\n\nconst realSleep = (delayMs: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delayMs));\n\n// Per-task stagger step: the configured gap, shrunk so the last of `count` tasks\n// still starts within MAX_STAGGER_FRACTION_OF_TICK of the tick. 0 disables it.\nfunction staggerStepMs(staggerMs: number, tickMs: number, count: number): number {\n if (staggerMs <= 0 || count <= 1) return 0;\n const maxStepMs = (tickMs * MAX_STAGGER_FRACTION_OF_TICK) / (count - 1);\n return Math.min(staggerMs, maxStepMs);\n}\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport type SchedulerLogger = MinimalLogger;\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string }; // time: \"HH:MM\" in UTC\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface TaskSummary {\n id: string;\n description?: string | undefined;\n schedule: TaskSchedule;\n dependsOn?: string | undefined;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => TaskSummary[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n /** Gap between the start of each independently-due task in a tick (#2057).\n * Default DEFAULT_FIRING_STAGGER_MS; 0 fires them all at once. */\n firingStaggerMs?: number;\n /** Injected so tests advance time without real timers. Default setTimeout. */\n sleep?: (delayMs: number) => Promise<void>;\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS;\n // Round down to tick boundary, then check if it aligns with the interval\n const rounded = Math.floor(msSinceMidnight / tickMs) * tickMs;\n return rounded % schedule.intervalMs === 0;\n }\n\n if (schedule.type === SCHEDULE_TYPES.daily) {\n const targetMs = dailyTargetMs(schedule.time);\n if (targetMs === null) return false;\n const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS;\n const rounded = Math.floor(msSinceMidnight / tickMs) * tickMs;\n return rounded === targetMs;\n }\n\n return false;\n}\n\n/** Milliseconds past UTC midnight for a `HH:MM` daily time, or null when the\n * string isn't that shape. `TaskSchedule.time` is a bare `string` on a\n * published package, so a host that skips its own validation must land on\n * \"never due\" rather than on a garbage firing time. */\nfunction dailyTargetMs(time: string): number | null {\n const [hours, minutes] = time.split(\":\").map(Number);\n if (hours === undefined || minutes === undefined) return null;\n if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;\n return hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;\n}\n\n/** The `time` of a daily schedule this manager can never fire, or null when the\n * schedule is fine. Returning the offending value (not a boolean) is what lets\n * the caller name it in the log. */\nfunction unfireableDailyTime(schedule: TaskSchedule): string | null {\n if (schedule.type !== SCHEDULE_TYPES.daily) return null;\n return dailyTargetMs(schedule.time) === null ? schedule.time : null;\n}\n\n/** Say out loud that this schedule can never fire. The task is still accepted —\n * `isDue` answers false forever, which is the safe outcome — but silence here\n * is what made \"the task I scheduled has never run once\" a bug with no\n * evidence anywhere to start from (#2765). Deliberately not a throw: a\n * consumer whose task has been quietly dead would get a boot crash instead. */\nfunction reportUnfireable(log: SchedulerLogger, taskId: string, schedule: TaskSchedule): void {\n const time = unfireableDailyTime(schedule);\n if (time === null) return;\n log.error(\"daily time is not HH:MM — this task will never run\", { id: taskId, time });\n}\n\n/** Split the due tasks into those that may run immediately and those gated\n * behind a `dependsOn` edge (resolved later in the same tick cycle). */\nexport function collectDueTasks(\n currentTime: Date,\n registry: Map<string, TaskDefinition>,\n tickMs: number,\n): { independent: TaskDefinition[]; dependent: TaskDefinition[] } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n}\n\nasync function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n}\n\nasync function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded, log);\n progress = true;\n }\n remaining = next;\n }\n}\n\ninterface TickConfig {\n tickMs: number;\n staggerMs: number;\n sleep: (delayMs: number) => Promise<void>;\n log: SchedulerLogger;\n}\n\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, cfg.tickMs);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n // Staggered start (#2057), still concurrent: each fires after its own capped\n // delay but the tick awaits them all, preserving the previous \"all due tasks\n // ran this tick\" contract.\n const stepMs = staggerStepMs(cfg.staggerMs, cfg.tickMs, independent.length);\n await Promise.all(\n independent.map(async (def, index) => {\n if (stepMs > 0 && index > 0) await cfg.sleep(index * stepMs);\n await runAndTrack(def, currentTime, succeeded, cfg.log);\n }),\n );\n\n await runDependentChain(dependent, currentTime, succeeded, cfg.log);\n}\n\nexport function listTaskSummaries(registry: Map<string, TaskDefinition>): TaskSummary[] {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n}\n\n// A tick runner that skips re-entry: if the previous tick is still draining its\n// staggered starts, drop this one so a slow tick + short tickMs can never run\n// two ticks concurrently.\nfunction makeGuardedTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): () => Promise<void> {\n let ticking = false;\n return async () => {\n if (ticking) return;\n ticking = true;\n try {\n await runTick(now, registry, cfg);\n } finally {\n ticking = false;\n }\n };\n}\n\nfunction resolveTickConfig(options?: TaskManagerOptions): { tickMs: number; now: () => Date; cfg: TickConfig } {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n return {\n tickMs,\n now: options?.now ?? (() => new Date()),\n cfg: {\n tickMs,\n staggerMs: options?.firingStaggerMs ?? DEFAULT_FIRING_STAGGER_MS,\n sleep: options?.sleep ?? realSleep,\n log: options?.log ?? NOOP_LOG,\n },\n };\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const { tickMs, now, cfg } = resolveTickConfig(options);\n const { log } = cfg;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n const onTick = makeGuardedTick(now, registry, cfg);\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n reportUnfireable(log, def.id, def.schedule);\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n // Checked here as well as at registration: an override applied at run\n // time (`applyScheduleOverride`) never passes through `registerTask`.\n reportUnfireable(log, taskId, schedule);\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n // `onTick` guards re-entry but not failure — it has a `finally`, no\n // `catch` — so handing it straight to setInterval let a rejected tick\n // escape as an unhandled rejection, which the host turns into a process\n // exit. The scheduler must outlive one bad tick.\n timer = setInterval(() => {\n onTick().catch((err: unknown) => log.error(\"tick failed\", { error: String(err) }));\n }, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return listTaskSummaries(registry);\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskSchedule,\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n SCHEDULE_TYPES,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\nimport { errorMessage } from \"../utils/errors.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n // Run catch-up\n const catchUpTasks: CatchUpTask[] = tasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toCoreSchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n const plan = computeCatchUpPlan(catchUpTasks, stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async () => {\n const windowIso = computeCurrentWindow(task);\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduledFor(task.schedule);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string | undefined; taskId?: string | undefined; limit?: number | undefined }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Read the persisted execution state for any task id (system, user, or\n * skill). Returns an empty state when the id has never run — used by the API\n * route to attach last-run / next-run / history state to user + skill tasks\n * it lists from other sources. */\nexport function getSchedulerTaskState(taskId: string): TaskExecutionState {\n return stateMap.get(taskId) ?? emptyState(taskId);\n}\n\n/** Record a run of an EXTERNAL (skill / user) task — one registered directly\n * on the task-manager rather than through `initScheduler`. Persists a log\n * entry + updates state so these tasks get the same history / last-run /\n * next-run as system tasks. Because they are fire-and-forget (`startChat`\n * spawns an async chat), `errorMessage` reflects whether the *dispatch*\n * succeeded, not the chat's eventual outcome; `chatSessionId` links the run\n * to the spawned session. */\nexport async function recordExternalRun(params: {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errorMessage: string | null;\n chatSessionId?: string | undefined;\n}): Promise<void> {\n await persistRun({\n meta: { id: params.id, name: params.name, schedule: params.schedule },\n scheduledFor: params.scheduledFor,\n startedAt: params.startedAt,\n durationMs: params.durationMs,\n trigger: params.trigger,\n errMsg: params.errorMessage,\n chatSessionId: params.chatSessionId,\n });\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await persistRun({ meta: { id: task.id, name: task.name, schedule: task.schedule }, scheduledFor, startedAt, durationMs, trigger, errMsg });\n}\n\n/** The minimum a run needs to persist state + a log entry — shared by system\n * tasks (`executeAndLog`) and external skill/user runs (`recordExternalRun`). */\ninterface TaskRunMeta {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n}\n\ninterface RunRecord {\n meta: TaskRunMeta;\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errMsg: string | null;\n chatSessionId?: string | undefined;\n}\n\n/** Best-effort persistence — state and log are independent, so one failing\n * never blocks the other and neither propagates upward. */\nasync function persistRun(run: RunRecord): Promise<void> {\n await writeRunState(run);\n await writeRunLog(run);\n}\n\nasync function writeRunState(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, durationMs, errMsg } = run;\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(meta.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n meta.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduledFor(meta.schedule),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\nasync function writeRunLog(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;\n const isSuccess = errMsg === null;\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: meta.id,\n taskName: meta.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n ...(chatSessionId !== undefined && { chatSessionId }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. */\nfunction computeCurrentWindow(task: SystemTaskDef): string {\n const coreSchedule = toCoreSchedule(task.schedule);\n // The window that just fired is the latest one at or before now.\n const nowMs = Date.now();\n const windowMs = nextWindowAfter(coreSchedule, nowMs - (coreSchedule.type === SCHEDULE_TYPES.interval ? coreSchedule.intervalSec * ONE_SECOND_MS : 0));\n return windowMs !== null && windowMs <= nowMs ? new Date(windowMs).toISOString() : new Date(nowMs).toISOString();\n}\n\nfunction computeNextScheduledFor(schedule: TaskDefinition[\"schedule\"]): string | null {\n const coreSchedule = toCoreSchedule(schedule);\n const next = nextWindowAfter(coreSchedule, Date.now() + 1);\n return next !== null ? new Date(next).toISOString() : null;\n}\n\nfunction toCoreSchedule(schedule: TaskDefinition[\"schedule\"]): TaskSchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return {\n type: SCHEDULE_TYPES.interval,\n intervalSec: Math.round(schedule.intervalMs / ONE_SECOND_MS),\n };\n }\n return schedule;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/scheduler/schedule-window.ts","../../src/scheduler/task-manager.ts","../../src/scheduler/adapter.ts"],"sourcesContent":["// One definition of \"when does this schedule fire\". The host expresses\n// intervals in milliseconds and the library (@receptron/task-scheduler) in\n// seconds, so every place that answers \"is it due now?\", \"when is the next\n// run?\" or \"which windows were missed?\" has to cross that boundary. Doing it\n// twice is what let the tick engine drift away from `nextWindowAfter`: its own\n// interval check counted from UTC midnight, so anything longer than a day\n// collapsed to \"every day at 00:00\" while the UI kept showing the real,\n// epoch-anchored next run (#2937).\n\nimport { SCHEDULE_TYPES, isDueAt, nextWindowAfter, parseTimeToMs, type TaskSchedule as LibrarySchedule } from \"@receptron/task-scheduler\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_DAY_MS = 24 * 60 * 60 * ONE_SECOND_MS;\n\n/** A schedule as the host states it: intervals in milliseconds, or a daily\n * `HH:MM` UTC time. */\nexport type TaskSchedule = { type: typeof SCHEDULE_TYPES.interval; intervalMs: number } | { type: typeof SCHEDULE_TYPES.daily; time: string };\n\n/** An interval this engine can never turn into a window. Zero divides by zero\n * inside the library's `Math.ceil(afterMs / intervalMs)`; a negative or\n * non-finite one poisons the arithmetic with NaN, which compares false\n * everywhere and so hides itself. */\nfunction isUsableInterval(intervalMs: number): boolean {\n return Number.isFinite(intervalMs) && intervalMs > 0;\n}\n\n/** Milliseconds past UTC midnight for a daily `HH:MM`, or null when the string\n * does not name a time of day. Out-of-range values are rejected, not clamped\n * and not wrapped: `\"24:00\"` would otherwise land on the NEXT day's midnight\n * and `\"-1:00\"` on an hour before it, so a typo that used to be inert would\n * start firing — at 00:00 daily, or 61 times a day. */\nfunction dailyOffsetMs(time: string): number | null {\n const offsetMs = parseTimeToMs(time);\n return Number.isFinite(offsetMs) && offsetMs >= 0 && offsetMs < ONE_DAY_MS ? offsetMs : null;\n}\n\n/** Convert to the library's schedule shape. Milliseconds are divided, never\n * rounded: rounding a sub-second interval to `intervalSec: 0` is what made\n * `nextWindowAfter` answer NaN and `new Date(NaN).toISOString()` throw. */\nexport function toLibrarySchedule(schedule: TaskSchedule): LibrarySchedule {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return { type: SCHEDULE_TYPES.interval, intervalSec: schedule.intervalMs / ONE_SECOND_MS };\n }\n return { type: SCHEDULE_TYPES.daily, time: schedule.time };\n}\n\n/** True when a scheduled window falls inside the tick ending at `nowMs`.\n * Windows are anchored to the epoch, not to the current UTC day, so an\n * interval longer than 24h keeps its real period. */\nexport function isScheduleDueAt(schedule: TaskSchedule, nowMs: number, tickMs: number): boolean {\n if (unfireableScheduleReason(schedule) !== null) return false;\n return isDueAt(toLibrarySchedule(schedule), nowMs, tickMs);\n}\n\n/** The furthest instant `Date` can represent. A window past it is finite\n * arithmetic but not a date, and `toISOString()` answers that with a throw —\n * which, inside the state writer, silently loses the whole run record. */\nconst MAX_DATE_MS = 8.64e15;\n\n/** A window as an ISO timestamp, or null when the number is not a date. */\nexport function windowToIso(windowMs: number | null): string | null {\n if (windowMs === null || !Number.isFinite(windowMs) || Math.abs(windowMs) > MAX_DATE_MS) return null;\n return new Date(windowMs).toISOString();\n}\n\n/** The most recent window at or before `nowMs`, or null when the schedule has\n * none. `nextWindowAfter` only ever looks FORWARD, so asking it from\n * `nowMs - one period` answers the previous window whenever `nowMs` sits\n * exactly on a boundary, and a daily schedule read after its minute answers\n * TOMORROW. Both schedule kinds have a fixed period, so stepping one period\n * back off the next window is exact. */\nexport function latestWindowAtOrBefore(schedule: TaskSchedule, nowMs: number): number | null {\n if (unfireableScheduleReason(schedule) !== null) return null;\n const next = nextWindowAfter(toLibrarySchedule(schedule), nowMs);\n if (next === null || !Number.isFinite(next)) return null;\n if (next <= nowMs) return next;\n const periodMs = schedule.type === SCHEDULE_TYPES.interval ? schedule.intervalMs : ONE_DAY_MS;\n return next - periodMs;\n}\n\n/** Name the field that makes a schedule unfireable, or null when it is fine.\n * Returns the offending value (not a boolean) so the caller can put it in the\n * log — a task that silently never runs leaves no evidence to start from\n * (#2765). */\nexport function unfireableScheduleReason(schedule: TaskSchedule): { field: string; value: string } | null {\n if (schedule.type === SCHEDULE_TYPES.interval) {\n return isUsableInterval(schedule.intervalMs) ? null : { field: \"intervalMs\", value: String(schedule.intervalMs) };\n }\n return dailyOffsetMs(schedule.time) !== null ? null : { field: \"time\", value: schedule.time };\n}\n","// Generic dependency-ordered cron tick engine. Host-agnostic: the only\n// host coupling (a logger) is injected via options. Schedules are either\n// fixed intervals or a daily UTC time; tasks may declare a `dependsOn`\n// edge so an ordering like \"news fetch → journal → memory extraction\"\n// runs in sequence within one tick.\n\nimport type { MinimalLogger } from \"@mulmoclaude/common\";\nimport { isScheduleDueAt, unfireableScheduleReason, type TaskSchedule } from \"./schedule-window.js\";\n\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\n\n// Gap between the START of each independently-due task within one tick. When\n// many tasks come due at the same minute (system journal + feed-refresh + a few\n// user tasks all at 20:00 UTC), firing every chat in the same event-loop turn\n// floods the machine; the mulmoclaude MCP broker each chat spawns then boots\n// under contention and can lose the startup race to the CLI's first tool call,\n// so that turn fails with `handlePermission not found` (#2057). Spacing the\n// launches by a second gives each broker room to connect. Far under one tick\n// (a handful of tasks spread over a few seconds), so nothing is delayed past\n// its window.\nconst DEFAULT_FIRING_STAGGER_MS = ONE_SECOND_MS;\n\n// The total stagger must stay well inside one tick, or the tail tasks start in a\n// LATER tick window and two ticks overlap. Cap all starts to this fraction of\n// `tickMs` regardless of task count or the configured gap — this keeps a debug\n// `tickMs === firingStaggerMs` (see server boot) from degenerating.\nconst MAX_STAGGER_FRACTION_OF_TICK = 0.5;\n\nconst realSleep = (delayMs: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, delayMs));\n\n// Per-task stagger step: the configured gap, shrunk so the last of `count` tasks\n// still starts within MAX_STAGGER_FRACTION_OF_TICK of the tick. 0 disables it.\nfunction staggerStepMs(staggerMs: number, tickMs: number, count: number): number {\n if (staggerMs <= 0 || count <= 1) return 0;\n const maxStepMs = (tickMs * MAX_STAGGER_FRACTION_OF_TICK) / (count - 1);\n return Math.min(staggerMs, maxStepMs);\n}\n\n/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport type SchedulerLogger = MinimalLogger;\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\n// Re-exported, not redefined: the schedule shape belongs with the window\n// arithmetic that interprets it, and hosts have always imported it from here.\nexport type { TaskSchedule } from \"./schedule-window.js\";\n\nexport interface TaskRunContext {\n taskId: string;\n now: Date;\n}\n\nexport interface TaskDefinition {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n enabled?: boolean; // default: true\n /** If set, this task only fires after the named task has completed\n * successfully in the current tick cycle. Enforces ordering like\n * \"news fetch → journal → memory extraction\". */\n dependsOn?: string;\n run: (ctx: TaskRunContext) => Promise<void>;\n}\n\nexport interface TaskSummary {\n id: string;\n description?: string | undefined;\n schedule: TaskSchedule;\n dependsOn?: string | undefined;\n}\n\nexport interface ITaskManager {\n registerTask: (def: TaskDefinition) => void;\n removeTask: (taskId: string) => void;\n /** Update the schedule of an existing task. Returns false if not found. */\n updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;\n start: () => void;\n stop: () => void;\n /** Run one tick manually (for testing). */\n tick: () => Promise<void>;\n listTasks: () => TaskSummary[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\n /** Gap between the start of each independently-due task in a tick (#2057).\n * Default DEFAULT_FIRING_STAGGER_MS; 0 fires them all at once. */\n firingStaggerMs?: number;\n /** Injected so tests advance time without real timers. Default setTimeout. */\n sleep?: (delayMs: number) => Promise<void>;\n}\n\nfunction isDue(now: Date, schedule: TaskSchedule, tickMs: number): boolean {\n return isScheduleDueAt(schedule, now.getTime(), tickMs);\n}\n\n/** Say out loud that this schedule can never fire. The task is still accepted —\n * `isDue` answers false forever, which is the safe outcome — but silence here\n * is what made \"the task I scheduled has never run once\" a bug with no\n * evidence anywhere to start from (#2765). Deliberately not a throw: a\n * consumer whose task has been quietly dead would get a boot crash instead. */\nfunction reportUnfireable(log: SchedulerLogger, taskId: string, schedule: TaskSchedule): void {\n const reason = unfireableScheduleReason(schedule);\n if (reason === null) return;\n log.error(`schedule has an unusable ${reason.field} — this task will never run`, { id: taskId, [reason.field]: reason.value });\n}\n\n/** Split the due tasks into those that may run immediately and those gated\n * behind a `dependsOn` edge (resolved later in the same tick cycle). */\nexport function collectDueTasks(\n currentTime: Date,\n registry: Map<string, TaskDefinition>,\n tickMs: number,\n): { independent: TaskDefinition[]; dependent: TaskDefinition[] } {\n const independent: TaskDefinition[] = [];\n const dependent: TaskDefinition[] = [];\n for (const def of registry.values()) {\n if (def.enabled === false) continue;\n if (!isDue(currentTime, def.schedule, tickMs)) continue;\n if (def.dependsOn) {\n dependent.push(def);\n } else {\n independent.push(def);\n }\n }\n return { independent, dependent };\n}\n\nasync function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n try {\n await def.run({ taskId: def.id, now: currentTime });\n succeeded.add(def.id);\n } catch (err) {\n log.error(\"task failed\", {\n id: def.id,\n error: String(err),\n });\n }\n}\n\nasync function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>, log: SchedulerLogger): Promise<void> {\n let remaining = [...dependent];\n let progress = true;\n while (remaining.length > 0 && progress) {\n progress = false;\n const next: TaskDefinition[] = [];\n for (const def of remaining) {\n const dep = def.dependsOn;\n if (!dep || !succeeded.has(dep)) {\n next.push(def);\n continue;\n }\n await runAndTrack(def, currentTime, succeeded, log);\n progress = true;\n }\n remaining = next;\n }\n}\n\ninterface TickConfig {\n tickMs: number;\n staggerMs: number;\n sleep: (delayMs: number) => Promise<void>;\n log: SchedulerLogger;\n}\n\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, cfg.tickMs);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n // Staggered start (#2057), still concurrent: each fires after its own capped\n // delay but the tick awaits them all, preserving the previous \"all due tasks\n // ran this tick\" contract.\n const stepMs = staggerStepMs(cfg.staggerMs, cfg.tickMs, independent.length);\n await Promise.all(\n independent.map(async (def, index) => {\n if (stepMs > 0 && index > 0) await cfg.sleep(index * stepMs);\n await runAndTrack(def, currentTime, succeeded, cfg.log);\n }),\n );\n\n await runDependentChain(dependent, currentTime, succeeded, cfg.log);\n}\n\nexport function listTaskSummaries(registry: Map<string, TaskDefinition>): TaskSummary[] {\n return [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\n}\n\n// A tick runner that skips re-entry: if the previous tick is still draining its\n// staggered starts, drop this one so a slow tick + short tickMs can never run\n// two ticks concurrently.\nfunction makeGuardedTick(now: () => Date, registry: Map<string, TaskDefinition>, cfg: TickConfig): () => Promise<void> {\n let ticking = false;\n return async () => {\n if (ticking) return;\n ticking = true;\n try {\n await runTick(now, registry, cfg);\n } finally {\n ticking = false;\n }\n };\n}\n\nfunction resolveTickConfig(options?: TaskManagerOptions): { tickMs: number; now: () => Date; cfg: TickConfig } {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n return {\n tickMs,\n now: options?.now ?? (() => new Date()),\n cfg: {\n tickMs,\n staggerMs: options?.firingStaggerMs ?? DEFAULT_FIRING_STAGGER_MS,\n sleep: options?.sleep ?? realSleep,\n log: options?.log ?? NOOP_LOG,\n },\n };\n}\n\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const { tickMs, now, cfg } = resolveTickConfig(options);\n const { log } = cfg;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n const onTick = makeGuardedTick(now, registry, cfg);\n\n return {\n async tick() {\n await onTick();\n },\n\n registerTask(def: TaskDefinition) {\n if (registry.has(def.id)) {\n throw new Error(`[task-manager] Task \"${def.id}\" is already registered`);\n }\n reportUnfireable(log, def.id, def.schedule);\n registry.set(def.id, def);\n log.info(\"registered\", { id: def.id });\n },\n\n updateSchedule(taskId: string, schedule: TaskSchedule): boolean {\n const def = registry.get(taskId);\n if (!def) return false;\n // Checked here as well as at registration: an override applied at run\n // time (`applyScheduleOverride`) never passes through `registerTask`.\n reportUnfireable(log, taskId, schedule);\n def.schedule = schedule;\n log.info(\"schedule updated\", { id: taskId });\n return true;\n },\n\n removeTask(taskId: string) {\n if (registry.delete(taskId)) {\n log.info(\"removed\", { id: taskId });\n }\n },\n\n start() {\n if (timer) return;\n // `onTick` guards re-entry but not failure — it has a `finally`, no\n // `catch` — so handing it straight to setInterval let a rejected tick\n // escape as an unhandled rejection, which the host turns into a process\n // exit. The scheduler must outlive one bad tick.\n timer = setInterval(() => {\n onTick().catch((err: unknown) => log.error(\"tick failed\", { error: String(err) }));\n }, tickMs);\n log.info(\"started\", { tickMs });\n },\n\n stop() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n log.info(\"stopped\");\n }\n },\n\n listTasks() {\n return listTaskSummaries(registry);\n },\n };\n}\n","// Adapter that wires the pure scheduler library (@receptron/task-scheduler)\n// to a host's task-manager + workspace. Registers system tasks, runs\n// catch-up on startup, and persists execution state + logs.\n//\n// Host-agnostic: the workspace root, the atomic file writer, and the\n// logger are injected via `configureScheduler`. The host supplies its OWN\n// system tasks (journal / feeds / user-cron in MulmoClaude) to\n// `initScheduler` — the package owns no task definitions. Deliberately\n// thin: all complex scheduling logic lives in @receptron/task-scheduler.\n\nimport { existsSync } from \"node:fs\";\nimport { readFile, appendFile, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport {\n type TaskExecutionState,\n type TaskLogEntry,\n type CatchUpTask,\n type TaskTrigger,\n emptyState,\n computeCatchUpPlan,\n nextWindowAfter,\n loadState,\n updateAndSave,\n appendLogEntry,\n queryLog,\n TASK_RESULTS,\n TASK_TRIGGERS,\n type MISSED_RUN_POLICIES,\n type StateMap,\n type StateDeps,\n type LogDeps,\n} from \"@receptron/task-scheduler\";\nimport type { ITaskManager, TaskDefinition, SchedulerLogger } from \"./task-manager.js\";\nimport { latestWindowAtOrBefore, toLibrarySchedule, unfireableScheduleReason, windowToIso } from \"./schedule-window.js\";\nimport { errorMessage } from \"../utils/errors.js\";\n\nconst SCHEDULER_CONFIG_DIR = \"config/scheduler\";\nconst SCHEDULER_DATA_DIR = \"data/scheduler/logs\";\n\n// ── Host injection ────────────────────────────────────────────────\n\nexport interface SchedulerConfig {\n /** Absolute workspace root — state.json + logs hang off it. */\n workspaceRoot: string;\n /** Host atomic file writer (used with `uniqueTmp` for the state file). */\n writeFileAtomic: (filePath: string, content: string, opts: { uniqueTmp: boolean }) => Promise<void>;\n /** Optional logger. */\n log?: SchedulerLogger;\n}\n\nconst NOOP_LOG: SchedulerLogger = { info: () => {}, warn: () => {}, error: () => {} };\n\nlet config: SchedulerConfig | null = null;\n\n/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */\nexport function configureScheduler(injected: SchedulerConfig): void {\n config = injected;\n}\n\nfunction requireConfig(): SchedulerConfig {\n if (!config) throw new Error(\"scheduler: configureScheduler() not called\");\n return config;\n}\n\nfunction logger(): SchedulerLogger {\n return config?.log ?? NOOP_LOG;\n}\n\n// ── Paths ─────────────────────────────────────────────────────────\n\nfunction stateFilePath(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, \"state.json\");\n}\n\nfunction logsDir(): string {\n return path.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);\n}\n\n// ── I/O deps (real filesystem) ────────────────────────────────────\n\nfunction stateDeps(): StateDeps {\n return {\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n writeFileAtomic: (filePath: string, content: string) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),\n exists: existsSync,\n };\n}\n\nconst logDeps: LogDeps = {\n appendFile: (filePath: string, content: string) => appendFile(filePath, content),\n readFile: (filePath: string) => readFile(filePath, \"utf-8\"),\n exists: existsSync,\n ensureDir: (directoryPath: string) => mkdir(directoryPath, { recursive: true }).then(() => {}),\n};\n\n// ── System task registry ──────────────────────────────────────────\n\nexport interface SystemTaskDef {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;\n run: () => Promise<void>;\n}\n\n// ── Public API ────────────────────────────────────────────────────\n\nlet stateMap: StateMap = new Map();\nconst systemTasks: SystemTaskDef[] = [];\nlet taskManagerRef: ITaskManager | null = null;\n\n/**\n * Initialize the scheduler adapter. Call once at server startup AFTER the\n * task-manager is created but BEFORE `taskManager.start()`.\n */\nexport async function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void> {\n await mkdir(path.dirname(stateFilePath()), { recursive: true });\n await mkdir(logsDir(), { recursive: true });\n\n stateMap = await loadState(stateFilePath(), stateDeps());\n systemTasks.length = 0;\n systemTasks.push(...tasks);\n taskManagerRef = taskManager;\n\n const plan = computeCatchUpPlan(catchUpTasksFor(tasks), stateMap, Date.now());\n\n for (const skip of plan.skipped) {\n logger().info(\"catch-up skipped\", { taskId: skip.taskId, windows: skip.windowCount });\n await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });\n }\n\n if (plan.runs.length > 0) {\n logger().info(\"catch-up enqueued\", { runs: plan.runs.length });\n for (const run of plan.runs) {\n const task = tasks.find((taskDef) => taskDef.id === run.taskId);\n if (!task) continue;\n await executeAndLog(task, run.context.scheduledFor, TASK_TRIGGERS.catchUp);\n }\n }\n\n // Register with task-manager for ongoing ticks\n for (const task of tasks) {\n taskManager.registerTask({\n id: task.id,\n description: task.description,\n schedule: task.schedule,\n run: async (ctx) => {\n const windowIso = computeCurrentWindow(task, ctx.now.getTime());\n await executeAndLog(task, windowIso, TASK_TRIGGERS.scheduled);\n },\n });\n }\n\n logger().info(\"initialized\", { tasks: tasks.map((taskDef) => taskDef.id), stateEntries: stateMap.size });\n}\n\n/** The catch-up input, with the tasks that can never fire left out. Their\n * windows come back NaN rather than null, which `listMissedWindows` cannot\n * see the end of — it fills to its cap and the first `new Date(NaN)` throws\n * `RangeError`, taking every other task's catch-up down with it at startup.\n * Registration still happens, so the task-manager reports the bad schedule. */\nfunction catchUpTasksFor(tasks: SystemTaskDef[]): CatchUpTask[] {\n return tasks\n .filter((taskDef) => {\n const reason = unfireableScheduleReason(taskDef.schedule);\n if (reason === null) return true;\n logger().error(`schedule has an unusable ${reason.field} — skipping catch-up, this task will never run`, {\n taskId: taskDef.id,\n [reason.field]: reason.value,\n });\n return false;\n })\n .map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n schedule: toLibrarySchedule(taskDef.schedule),\n missedRunPolicy: taskDef.missedRunPolicy,\n enabled: true,\n }));\n}\n\n/** Apply a schedule override to a running system task. Updates the\n * in-memory task definition, the task-manager, and recalculates\n * nextScheduledAt in persisted state. */\nexport async function applyScheduleOverride(taskId: string, schedule: SystemTaskDef[\"schedule\"]): Promise<boolean> {\n const task = systemTasks.find((taskDef) => taskDef.id === taskId);\n if (!task || !taskManagerRef) return false;\n if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;\n task.schedule = schedule;\n\n // Recalculate next window so the UI reflects the new schedule\n const nextScheduledAt = computeNextScheduledFor(task.schedule);\n await safeUpdateState(taskId, { nextScheduledAt });\n\n return true;\n}\n\n/** Query execution logs — used by API routes. */\nexport async function getSchedulerLogs(opts: { since?: string | undefined; taskId?: string | undefined; limit?: number | undefined }): Promise<TaskLogEntry[]> {\n return queryLog(logsDir(), opts, logDeps);\n}\n\n/** Get all task states — used by API routes. */\nexport function getSchedulerTasks(): {\n id: string;\n name: string;\n description: string;\n schedule: TaskDefinition[\"schedule\"];\n missedRunPolicy: string;\n state: TaskExecutionState;\n}[] {\n return systemTasks.map((taskDef) => ({\n id: taskDef.id,\n name: taskDef.name,\n description: taskDef.description,\n schedule: taskDef.schedule,\n missedRunPolicy: taskDef.missedRunPolicy,\n state: stateMap.get(taskDef.id) ?? emptyState(taskDef.id),\n }));\n}\n\n/** Read the persisted execution state for any task id (system, user, or\n * skill). Returns an empty state when the id has never run — used by the API\n * route to attach last-run / next-run / history state to user + skill tasks\n * it lists from other sources. */\nexport function getSchedulerTaskState(taskId: string): TaskExecutionState {\n return stateMap.get(taskId) ?? emptyState(taskId);\n}\n\n/** Record a run of an EXTERNAL (skill / user) task — one registered directly\n * on the task-manager rather than through `initScheduler`. Persists a log\n * entry + updates state so these tasks get the same history / last-run /\n * next-run as system tasks. Because they are fire-and-forget (`startChat`\n * spawns an async chat), `errorMessage` reflects whether the *dispatch*\n * succeeded, not the chat's eventual outcome; `chatSessionId` links the run\n * to the spawned session. */\nexport async function recordExternalRun(params: {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errorMessage: string | null;\n chatSessionId?: string | undefined;\n}): Promise<void> {\n await persistRun({\n meta: { id: params.id, name: params.name, schedule: params.schedule },\n scheduledFor: params.scheduledFor,\n startedAt: params.startedAt,\n durationMs: params.durationMs,\n trigger: params.trigger,\n errMsg: params.errorMessage,\n chatSessionId: params.chatSessionId,\n });\n}\n\n/** Test-only: clear config + in-memory state. */\nexport function resetSchedulerForTesting(): void {\n config = null;\n stateMap = new Map();\n systemTasks.length = 0;\n taskManagerRef = null;\n}\n\n// ── Internal ──────────────────────────────────────────────────────\n\nasync function executeAndLog(task: SystemTaskDef, scheduledFor: string, trigger: TaskTrigger): Promise<void> {\n const startedAt = new Date().toISOString();\n const startMs = Date.now();\n let errMsg: string | null = null;\n try {\n await task.run();\n } catch (err) {\n errMsg = errorMessage(err);\n logger().error(\"task failed\", { taskId: task.id, error: errMsg });\n }\n const durationMs = Date.now() - startMs;\n // Persistence is best-effort — never let disk failures propagate to the\n // tick loop or abort startup catch-up.\n await persistRun({ meta: { id: task.id, name: task.name, schedule: task.schedule }, scheduledFor, startedAt, durationMs, trigger, errMsg });\n}\n\n/** The minimum a run needs to persist state + a log entry — shared by system\n * tasks (`executeAndLog`) and external skill/user runs (`recordExternalRun`). */\ninterface TaskRunMeta {\n id: string;\n name: string;\n schedule: TaskDefinition[\"schedule\"];\n}\n\ninterface RunRecord {\n meta: TaskRunMeta;\n scheduledFor: string;\n startedAt: string;\n durationMs: number;\n trigger: TaskTrigger;\n errMsg: string | null;\n chatSessionId?: string | undefined;\n}\n\n/** Best-effort persistence — state and log are independent, so one failing\n * never blocks the other and neither propagates upward. */\nasync function persistRun(run: RunRecord): Promise<void> {\n await writeRunState(run);\n await writeRunLog(run);\n}\n\nasync function writeRunState(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, durationMs, errMsg } = run;\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(meta.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n meta.id,\n {\n lastRunAt: scheduledFor,\n lastRunResult: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n lastRunDurationMs: durationMs,\n lastErrorMessage: errMsg,\n consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,\n totalRuns: (currentState?.totalRuns ?? 0) + 1,\n nextScheduledAt: computeNextScheduledFor(meta.schedule),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\nasync function writeRunLog(run: RunRecord): Promise<void> {\n const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;\n const isSuccess = errMsg === null;\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: meta.id,\n taskName: meta.name,\n scheduledFor,\n startedAt,\n completedAt: new Date().toISOString(),\n result: isSuccess ? TASK_RESULTS.success : TASK_RESULTS.error,\n durationMs,\n trigger,\n ...(errMsg !== null && { errorMessage: errMsg }),\n ...(chatSessionId !== undefined && { chatSessionId }),\n },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: meta.id, error: String(err) });\n }\n}\n\n/** Safe state update — swallows errors. */\nasync function safeUpdateState(taskId: string, patch: Partial<TaskExecutionState>): Promise<void> {\n try {\n await updateAndSave(stateFilePath(), stateMap, taskId, patch, stateDeps());\n } catch (err) {\n logger().warn(\"state update failed\", { taskId, error: String(err) });\n }\n}\n\n/** Compute the window boundary that the current tick belongs to. For\n * scheduled runs, this is the epoch-aligned window — not the wall-clock\n * time of execution. This keeps lastRunAt consistent with catch-up's\n * window-based accounting. `tickMs` is the tick's own clock, not\n * `Date.now()`: the two drift apart by however long the tick took to reach\n * this task, and under an injected clock they are unrelated. */\nfunction computeCurrentWindow(task: SystemTaskDef, tickMs: number): string {\n return windowToIso(latestWindowAtOrBefore(task.schedule, tickMs)) ?? new Date(tickMs).toISOString();\n}\n\nfunction computeNextScheduledFor(schedule: TaskDefinition[\"schedule\"]): string | null {\n // The engine will never fire this one, so promising a next run in the UI\n // would be a lie — and the library still hands back a window for some of\n // them (`\"24:00\"` resolves to tomorrow's midnight).\n if (unfireableScheduleReason(schedule) !== null) return null;\n // `windowToIso` is what keeps a window that is not a date (NaN from a\n // degenerate schedule, or an interval so large the next window is past the\n // end of `Date`) from throwing here and taking the whole state write down.\n return windowToIso(nextWindowAfter(toLibrarySchedule(schedule), Date.now() + 1));\n}\n"],"mappings":"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"}
|