@mulmoclaude/core 0.2.9 → 0.2.10
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/dist/collection/core/actionVisible.d.ts +34 -0
- package/dist/collection/core/calendarGrid.d.ts +120 -0
- package/dist/collection/core/deriveAll.d.ts +38 -0
- package/dist/collection/core/derivedFormula.d.ts +18 -0
- package/dist/collection/core/draft.d.ts +18 -0
- package/dist/collection/core/enumColors.d.ts +33 -0
- package/dist/collection/core/errorMessage.d.ts +4 -0
- package/dist/collection/core/itemLabel.d.ts +12 -0
- package/dist/collection/core/presentCollection.d.ts +13 -0
- package/dist/collection/core/promptSafety.d.ts +1 -0
- package/dist/collection/core/schema.d.ts +382 -0
- package/dist/collection/core/shortHexId.d.ts +8 -0
- package/dist/collection/core/sortItems.d.ts +29 -0
- package/dist/collection/core/uiTypes.d.ts +106 -0
- package/dist/collection/index.cjs +794 -0
- package/dist/collection/index.cjs.map +1 -0
- package/dist/collection/index.d.ts +14 -0
- package/dist/collection/paths.cjs +44 -0
- package/dist/collection/paths.cjs.map +1 -0
- package/dist/collection/server/atomic.d.ts +1 -0
- package/dist/collection/server/delete.d.ts +38 -0
- package/dist/collection/server/derive.d.ts +8 -0
- package/dist/collection/server/discoveredCollection.d.ts +18 -0
- package/dist/collection/server/discovery.d.ts +239 -0
- package/dist/collection/server/host.d.ts +77 -0
- package/dist/collection/server/index.cjs +51 -0
- package/dist/collection/server/io.d.ts +120 -0
- package/dist/collection/server/paths.d.ts +52 -0
- package/dist/collection/server/spawn.d.ts +55 -0
- package/dist/collection/server/util.d.ts +3 -0
- package/dist/collection/server/validate.d.ts +19 -0
- package/dist/collection/server/views.d.ts +20 -0
- package/dist/collection-watchers/config.d.ts +49 -0
- package/dist/collection-watchers/index.cjs +553 -0
- package/dist/collection-watchers/index.cjs.map +1 -0
- package/dist/collection-watchers/index.d.ts +3 -0
- package/dist/collection-watchers/reconciler.d.ts +33 -0
- package/dist/collection-watchers/watcher.d.ts +34 -0
- package/dist/deriveAll-VRWrs3SF.cjs +413 -0
- package/dist/deriveAll-VRWrs3SF.cjs.map +1 -0
- package/dist/feeds/index.cjs +8 -0
- package/dist/feeds/index.d.ts +1 -0
- package/dist/feeds/ingestTypes.d.ts +57 -0
- package/dist/feeds/paths.cjs +48 -0
- package/dist/feeds/paths.cjs.map +1 -0
- package/dist/feeds/paths.d.ts +22 -0
- package/dist/feeds/server/fetch/httpClient.d.ts +8 -0
- package/dist/feeds/server/fetch/rssParser.d.ts +12 -0
- package/dist/feeds/server/index.cjs +839 -0
- package/dist/feeds/server/index.cjs.map +1 -0
- package/dist/feeds/server/index.js +4 -5
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/feeds/server/pathResolver.d.ts +6 -0
- package/dist/feeds/server/projectItem.d.ts +8 -0
- package/dist/feeds/server/refreshResult.d.ts +17 -0
- package/dist/feeds/server/retrievers/httpJson.d.ts +3 -0
- package/dist/feeds/server/retrievers/index.d.ts +12 -0
- package/dist/feeds/server/retrievers/registerAll.d.ts +0 -0
- package/dist/feeds/server/retrievers/rss.d.ts +3 -0
- package/dist/feeds/server/scheduledRefresh.d.ts +1 -1
- package/dist/file-change/index.cjs +72 -0
- package/dist/file-change/index.cjs.map +1 -0
- package/dist/file-change/index.d.ts +43 -0
- package/dist/ingestTypes-DDOVhOYC.cjs +32 -0
- package/dist/ingestTypes-DDOVhOYC.cjs.map +1 -0
- package/dist/notifier/engine.d.ts +72 -0
- package/dist/notifier/index.cjs +21 -0
- package/dist/notifier/index.d.ts +3 -0
- package/dist/notifier/store.d.ts +18 -0
- package/dist/notifier/types.d.ts +118 -0
- package/dist/notifier/validate.d.ts +17 -0
- package/dist/notifier-bS8IEeLA.cjs +577 -0
- package/dist/notifier-bS8IEeLA.cjs.map +1 -0
- package/dist/rolldown-runtime-D6vf50IK.cjs +28 -0
- package/dist/scheduler/adapter.d.ts +48 -0
- package/dist/scheduler/index.cjs +352 -0
- package/dist/scheduler/index.cjs.map +1 -0
- package/dist/scheduler/index.d.ts +2 -0
- package/dist/scheduler/task-manager.d.ts +51 -0
- package/dist/server-Db-x7OXz.cjs +1993 -0
- package/dist/server-Db-x7OXz.cjs.map +1 -0
- package/dist/skill-bridge/index.cjs +88 -0
- package/dist/skill-bridge/index.cjs.map +1 -0
- package/dist/skill-bridge/index.d.ts +30 -0
- package/dist/whisper/client.cjs +241 -0
- package/dist/whisper/client.cjs.map +1 -0
- package/dist/whisper/client.d.ts +35 -0
- package/dist/whisper/ffmpeg.d.ts +6 -0
- package/dist/whisper/index.cjs +433 -0
- package/dist/whisper/index.cjs.map +1 -0
- package/dist/whisper/index.d.ts +5 -0
- package/dist/whisper/internal.d.ts +11 -0
- package/dist/whisper/models.d.ts +49 -0
- package/dist/whisper/sidecar.d.ts +8 -0
- package/dist/whisper/whisper.d.ts +28 -0
- package/dist/workspace-setup/assets.d.ts +10 -0
- package/dist/workspace-setup/index.d.ts +3 -0
- package/dist/workspace-setup/index.js +560 -0
- package/dist/workspace-setup/index.js.map +1 -0
- package/dist/workspace-setup/slug.d.ts +6 -0
- package/dist/workspace-setup/slug.js +13 -0
- package/dist/workspace-setup/slug.js.map +1 -0
- package/dist/workspace-setup/sync.d.ts +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
Object.defineProperty(exports, "__toESM", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: function() {
|
|
26
|
+
return __toESM;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { TaskExecutionState, TaskLogEntry, MISSED_RUN_POLICIES } from '@receptron/task-scheduler';
|
|
2
|
+
import { ITaskManager, TaskDefinition, SchedulerLogger } from './task-manager.js';
|
|
3
|
+
export interface SchedulerConfig {
|
|
4
|
+
/** Absolute workspace root — state.json + logs hang off it. */
|
|
5
|
+
workspaceRoot: string;
|
|
6
|
+
/** Host atomic file writer (used with `uniqueTmp` for the state file). */
|
|
7
|
+
writeFileAtomic: (filePath: string, content: string, opts: {
|
|
8
|
+
uniqueTmp: boolean;
|
|
9
|
+
}) => Promise<void>;
|
|
10
|
+
/** Optional logger. */
|
|
11
|
+
log?: SchedulerLogger;
|
|
12
|
+
}
|
|
13
|
+
/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */
|
|
14
|
+
export declare function configureScheduler(injected: SchedulerConfig): void;
|
|
15
|
+
export interface SystemTaskDef {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
schedule: TaskDefinition["schedule"];
|
|
20
|
+
missedRunPolicy: typeof MISSED_RUN_POLICIES.skip | typeof MISSED_RUN_POLICIES.runOnce | typeof MISSED_RUN_POLICIES.runAll;
|
|
21
|
+
run: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the scheduler adapter. Call once at server startup AFTER the
|
|
25
|
+
* task-manager is created but BEFORE `taskManager.start()`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function initScheduler(taskManager: ITaskManager, tasks: SystemTaskDef[]): Promise<void>;
|
|
28
|
+
/** Apply a schedule override to a running system task. Updates the
|
|
29
|
+
* in-memory task definition, the task-manager, and recalculates
|
|
30
|
+
* nextScheduledAt in persisted state. */
|
|
31
|
+
export declare function applyScheduleOverride(taskId: string, schedule: SystemTaskDef["schedule"]): Promise<boolean>;
|
|
32
|
+
/** Query execution logs — used by API routes. */
|
|
33
|
+
export declare function getSchedulerLogs(opts: {
|
|
34
|
+
since?: string;
|
|
35
|
+
taskId?: string;
|
|
36
|
+
limit?: number;
|
|
37
|
+
}): Promise<TaskLogEntry[]>;
|
|
38
|
+
/** Get all task states — used by API routes. */
|
|
39
|
+
export declare function getSchedulerTasks(): {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
schedule: TaskDefinition["schedule"];
|
|
44
|
+
missedRunPolicy: string;
|
|
45
|
+
state: TaskExecutionState;
|
|
46
|
+
}[];
|
|
47
|
+
/** Test-only: clear config + in-memory state. */
|
|
48
|
+
export declare function resetSchedulerForTesting(): void;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
5
|
+
let node_fs = require("node:fs");
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let _receptron_task_scheduler = require("@receptron/task-scheduler");
|
|
8
|
+
//#region src/scheduler/task-manager.ts
|
|
9
|
+
var ONE_SECOND_MS$1 = 1e3;
|
|
10
|
+
var ONE_MINUTE_MS = 60 * ONE_SECOND_MS$1;
|
|
11
|
+
var ONE_HOUR_MS = 60 * ONE_MINUTE_MS;
|
|
12
|
+
var NOOP_LOG$1 = {
|
|
13
|
+
info: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
error: () => {}
|
|
16
|
+
};
|
|
17
|
+
function isDue(now, schedule, tickMs) {
|
|
18
|
+
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval) {
|
|
19
|
+
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS$1;
|
|
20
|
+
return Math.floor(msSinceMidnight / tickMs) * tickMs % schedule.intervalMs === 0;
|
|
21
|
+
}
|
|
22
|
+
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.daily) {
|
|
23
|
+
const [hours, minutes] = schedule.time.split(":").map(Number);
|
|
24
|
+
const targetMs = hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;
|
|
25
|
+
const msSinceMidnight = now.getUTCHours() * ONE_HOUR_MS + now.getUTCMinutes() * ONE_MINUTE_MS + now.getUTCSeconds() * ONE_SECOND_MS$1;
|
|
26
|
+
return Math.floor(msSinceMidnight / tickMs) * tickMs === targetMs;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function createTaskManager(options) {
|
|
31
|
+
const tickMs = options?.tickMs ?? ONE_MINUTE_MS;
|
|
32
|
+
const now = options?.now ?? (() => /* @__PURE__ */ new Date());
|
|
33
|
+
const log = options?.log ?? NOOP_LOG$1;
|
|
34
|
+
const registry = /* @__PURE__ */ new Map();
|
|
35
|
+
let timer = null;
|
|
36
|
+
function collectDueTasks(currentTime) {
|
|
37
|
+
const independent = [];
|
|
38
|
+
const dependent = [];
|
|
39
|
+
for (const def of registry.values()) {
|
|
40
|
+
if (def.enabled === false) continue;
|
|
41
|
+
if (!isDue(currentTime, def.schedule, tickMs)) continue;
|
|
42
|
+
if (def.dependsOn) dependent.push(def);
|
|
43
|
+
else independent.push(def);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
independent,
|
|
47
|
+
dependent
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function runAndTrack(def, currentTime, succeeded) {
|
|
51
|
+
try {
|
|
52
|
+
await def.run({
|
|
53
|
+
taskId: def.id,
|
|
54
|
+
now: currentTime
|
|
55
|
+
});
|
|
56
|
+
succeeded.add(def.id);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.error("task failed", {
|
|
59
|
+
id: def.id,
|
|
60
|
+
error: String(err)
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function runDependentChain(dependent, currentTime, succeeded) {
|
|
65
|
+
let remaining = [...dependent];
|
|
66
|
+
let progress = true;
|
|
67
|
+
while (remaining.length > 0 && progress) {
|
|
68
|
+
progress = false;
|
|
69
|
+
const next = [];
|
|
70
|
+
for (const def of remaining) {
|
|
71
|
+
const dep = def.dependsOn;
|
|
72
|
+
if (!dep || !succeeded.has(dep)) {
|
|
73
|
+
next.push(def);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
await runAndTrack(def, currentTime, succeeded);
|
|
77
|
+
progress = true;
|
|
78
|
+
}
|
|
79
|
+
remaining = next;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function onTick() {
|
|
83
|
+
const currentTime = now();
|
|
84
|
+
const { independent, dependent } = collectDueTasks(currentTime);
|
|
85
|
+
const succeeded = /* @__PURE__ */ new Set();
|
|
86
|
+
await Promise.all(independent.map((def) => runAndTrack(def, currentTime, succeeded)));
|
|
87
|
+
await runDependentChain(dependent, currentTime, succeeded);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
async tick() {
|
|
91
|
+
await onTick();
|
|
92
|
+
},
|
|
93
|
+
registerTask(def) {
|
|
94
|
+
if (registry.has(def.id)) throw new Error(`[task-manager] Task "${def.id}" is already registered`);
|
|
95
|
+
registry.set(def.id, def);
|
|
96
|
+
log.info("registered", { id: def.id });
|
|
97
|
+
},
|
|
98
|
+
updateSchedule(taskId, schedule) {
|
|
99
|
+
const def = registry.get(taskId);
|
|
100
|
+
if (!def) return false;
|
|
101
|
+
def.schedule = schedule;
|
|
102
|
+
log.info("schedule updated", { id: taskId });
|
|
103
|
+
return true;
|
|
104
|
+
},
|
|
105
|
+
removeTask(taskId) {
|
|
106
|
+
if (registry.delete(taskId)) log.info("removed", { id: taskId });
|
|
107
|
+
},
|
|
108
|
+
start() {
|
|
109
|
+
if (timer) return;
|
|
110
|
+
timer = setInterval(onTick, tickMs);
|
|
111
|
+
log.info("started", { tickMs });
|
|
112
|
+
},
|
|
113
|
+
stop() {
|
|
114
|
+
if (timer) {
|
|
115
|
+
clearInterval(timer);
|
|
116
|
+
timer = null;
|
|
117
|
+
log.info("stopped");
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
listTasks() {
|
|
121
|
+
return [...registry.values()].map((taskDef) => ({
|
|
122
|
+
id: taskDef.id,
|
|
123
|
+
description: taskDef.description,
|
|
124
|
+
schedule: taskDef.schedule,
|
|
125
|
+
dependsOn: taskDef.dependsOn
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/scheduler/adapter.ts
|
|
132
|
+
var ONE_SECOND_MS = 1e3;
|
|
133
|
+
var SCHEDULER_CONFIG_DIR = "config/scheduler";
|
|
134
|
+
var SCHEDULER_DATA_DIR = "data/scheduler/logs";
|
|
135
|
+
var NOOP_LOG = {
|
|
136
|
+
info: () => {},
|
|
137
|
+
warn: () => {},
|
|
138
|
+
error: () => {}
|
|
139
|
+
};
|
|
140
|
+
var config = null;
|
|
141
|
+
/** Wire the adapter to a host. Call once at startup, before `initScheduler`. */
|
|
142
|
+
function configureScheduler(injected) {
|
|
143
|
+
config = injected;
|
|
144
|
+
}
|
|
145
|
+
function requireConfig() {
|
|
146
|
+
if (!config) throw new Error("scheduler: configureScheduler() not called");
|
|
147
|
+
return config;
|
|
148
|
+
}
|
|
149
|
+
function logger() {
|
|
150
|
+
return config?.log ?? NOOP_LOG;
|
|
151
|
+
}
|
|
152
|
+
function errorMessage(err) {
|
|
153
|
+
return err instanceof Error ? err.message : String(err);
|
|
154
|
+
}
|
|
155
|
+
function stateFilePath() {
|
|
156
|
+
return node_path.default.join(requireConfig().workspaceRoot, SCHEDULER_CONFIG_DIR, "state.json");
|
|
157
|
+
}
|
|
158
|
+
function logsDir() {
|
|
159
|
+
return node_path.default.join(requireConfig().workspaceRoot, SCHEDULER_DATA_DIR);
|
|
160
|
+
}
|
|
161
|
+
function stateDeps() {
|
|
162
|
+
return {
|
|
163
|
+
readFile: (filePath) => (0, node_fs_promises.readFile)(filePath, "utf-8"),
|
|
164
|
+
writeFileAtomic: (filePath, content) => requireConfig().writeFileAtomic(filePath, content, { uniqueTmp: true }),
|
|
165
|
+
exists: node_fs.existsSync
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
var logDeps = {
|
|
169
|
+
appendFile: (filePath, content) => (0, node_fs_promises.appendFile)(filePath, content),
|
|
170
|
+
readFile: (filePath) => (0, node_fs_promises.readFile)(filePath, "utf-8"),
|
|
171
|
+
exists: node_fs.existsSync,
|
|
172
|
+
ensureDir: (directoryPath) => (0, node_fs_promises.mkdir)(directoryPath, { recursive: true }).then(() => {})
|
|
173
|
+
};
|
|
174
|
+
var stateMap = /* @__PURE__ */ new Map();
|
|
175
|
+
var systemTasks = [];
|
|
176
|
+
var taskManagerRef = null;
|
|
177
|
+
/**
|
|
178
|
+
* Initialize the scheduler adapter. Call once at server startup AFTER the
|
|
179
|
+
* task-manager is created but BEFORE `taskManager.start()`.
|
|
180
|
+
*/
|
|
181
|
+
async function initScheduler(taskManager, tasks) {
|
|
182
|
+
await (0, node_fs_promises.mkdir)(node_path.default.dirname(stateFilePath()), { recursive: true });
|
|
183
|
+
await (0, node_fs_promises.mkdir)(logsDir(), { recursive: true });
|
|
184
|
+
stateMap = await (0, _receptron_task_scheduler.loadState)(stateFilePath(), stateDeps());
|
|
185
|
+
systemTasks.length = 0;
|
|
186
|
+
systemTasks.push(...tasks);
|
|
187
|
+
taskManagerRef = taskManager;
|
|
188
|
+
const plan = (0, _receptron_task_scheduler.computeCatchUpPlan)(tasks.map((taskDef) => ({
|
|
189
|
+
id: taskDef.id,
|
|
190
|
+
name: taskDef.name,
|
|
191
|
+
schedule: toCoreSchedule(taskDef.schedule),
|
|
192
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
193
|
+
enabled: true
|
|
194
|
+
})), stateMap, Date.now());
|
|
195
|
+
for (const skip of plan.skipped) {
|
|
196
|
+
logger().info("catch-up skipped", {
|
|
197
|
+
taskId: skip.taskId,
|
|
198
|
+
windows: skip.windowCount
|
|
199
|
+
});
|
|
200
|
+
await safeUpdateState(skip.taskId, { lastRunAt: skip.lastWindow });
|
|
201
|
+
}
|
|
202
|
+
if (plan.runs.length > 0) {
|
|
203
|
+
logger().info("catch-up enqueued", { runs: plan.runs.length });
|
|
204
|
+
for (const run of plan.runs) {
|
|
205
|
+
const task = tasks.find((taskDef) => taskDef.id === run.taskId);
|
|
206
|
+
if (!task) continue;
|
|
207
|
+
await executeAndLog(task, run.context.scheduledFor, _receptron_task_scheduler.TASK_TRIGGERS.catchUp);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const task of tasks) taskManager.registerTask({
|
|
211
|
+
id: task.id,
|
|
212
|
+
description: task.description,
|
|
213
|
+
schedule: task.schedule,
|
|
214
|
+
run: async () => {
|
|
215
|
+
await executeAndLog(task, computeCurrentWindow(task), _receptron_task_scheduler.TASK_TRIGGERS.scheduled);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
logger().info("initialized", {
|
|
219
|
+
tasks: tasks.map((taskDef) => taskDef.id),
|
|
220
|
+
stateEntries: stateMap.size
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** Apply a schedule override to a running system task. Updates the
|
|
224
|
+
* in-memory task definition, the task-manager, and recalculates
|
|
225
|
+
* nextScheduledAt in persisted state. */
|
|
226
|
+
async function applyScheduleOverride(taskId, schedule) {
|
|
227
|
+
const task = systemTasks.find((taskDef) => taskDef.id === taskId);
|
|
228
|
+
if (!task || !taskManagerRef) return false;
|
|
229
|
+
if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;
|
|
230
|
+
task.schedule = schedule;
|
|
231
|
+
await safeUpdateState(taskId, { nextScheduledAt: computeNextScheduled(task) });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
/** Query execution logs — used by API routes. */
|
|
235
|
+
async function getSchedulerLogs(opts) {
|
|
236
|
+
return (0, _receptron_task_scheduler.queryLog)(logsDir(), opts, logDeps);
|
|
237
|
+
}
|
|
238
|
+
/** Get all task states — used by API routes. */
|
|
239
|
+
function getSchedulerTasks() {
|
|
240
|
+
return systemTasks.map((taskDef) => ({
|
|
241
|
+
id: taskDef.id,
|
|
242
|
+
name: taskDef.name,
|
|
243
|
+
description: taskDef.description,
|
|
244
|
+
schedule: taskDef.schedule,
|
|
245
|
+
missedRunPolicy: taskDef.missedRunPolicy,
|
|
246
|
+
state: stateMap.get(taskDef.id) ?? (0, _receptron_task_scheduler.emptyState)(taskDef.id)
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
/** Test-only: clear config + in-memory state. */
|
|
250
|
+
function resetSchedulerForTesting() {
|
|
251
|
+
config = null;
|
|
252
|
+
stateMap = /* @__PURE__ */ new Map();
|
|
253
|
+
systemTasks.length = 0;
|
|
254
|
+
taskManagerRef = null;
|
|
255
|
+
}
|
|
256
|
+
async function executeAndLog(task, scheduledFor, trigger) {
|
|
257
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
+
const startMs = Date.now();
|
|
259
|
+
let errMsg = null;
|
|
260
|
+
try {
|
|
261
|
+
await task.run();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
errMsg = errorMessage(err);
|
|
264
|
+
logger().error("task failed", {
|
|
265
|
+
taskId: task.id,
|
|
266
|
+
error: errMsg
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
await safePersist(task, scheduledFor, startedAt, Date.now() - startMs, trigger, errMsg);
|
|
270
|
+
}
|
|
271
|
+
/** Best-effort persistence — state and log are independent. A failure in
|
|
272
|
+
* one does not block the other, and neither propagates upward. */
|
|
273
|
+
async function safePersist(task, scheduledFor, startedAt, durationMs, trigger, errMsg) {
|
|
274
|
+
const isSuccess = errMsg === null;
|
|
275
|
+
const currentState = stateMap.get(task.id);
|
|
276
|
+
try {
|
|
277
|
+
await (0, _receptron_task_scheduler.updateAndSave)(stateFilePath(), stateMap, task.id, {
|
|
278
|
+
lastRunAt: scheduledFor,
|
|
279
|
+
lastRunResult: isSuccess ? _receptron_task_scheduler.TASK_RESULTS.success : _receptron_task_scheduler.TASK_RESULTS.error,
|
|
280
|
+
lastRunDurationMs: durationMs,
|
|
281
|
+
lastErrorMessage: errMsg,
|
|
282
|
+
consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,
|
|
283
|
+
totalRuns: (currentState?.totalRuns ?? 0) + 1,
|
|
284
|
+
nextScheduledAt: computeNextScheduled(task)
|
|
285
|
+
}, stateDeps());
|
|
286
|
+
} catch (err) {
|
|
287
|
+
logger().warn("state persistence failed", {
|
|
288
|
+
taskId: task.id,
|
|
289
|
+
error: String(err)
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
await (0, _receptron_task_scheduler.appendLogEntry)(logsDir(), {
|
|
294
|
+
taskId: task.id,
|
|
295
|
+
taskName: task.name,
|
|
296
|
+
scheduledFor,
|
|
297
|
+
startedAt,
|
|
298
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
299
|
+
result: isSuccess ? _receptron_task_scheduler.TASK_RESULTS.success : _receptron_task_scheduler.TASK_RESULTS.error,
|
|
300
|
+
durationMs,
|
|
301
|
+
trigger,
|
|
302
|
+
...errMsg !== null && { errorMessage: errMsg }
|
|
303
|
+
}, logDeps);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
logger().warn("log persistence failed", {
|
|
306
|
+
taskId: task.id,
|
|
307
|
+
error: String(err)
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Safe state update — swallows errors. */
|
|
312
|
+
async function safeUpdateState(taskId, patch) {
|
|
313
|
+
try {
|
|
314
|
+
await (0, _receptron_task_scheduler.updateAndSave)(stateFilePath(), stateMap, taskId, patch, stateDeps());
|
|
315
|
+
} catch (err) {
|
|
316
|
+
logger().warn("state update failed", {
|
|
317
|
+
taskId,
|
|
318
|
+
error: String(err)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Compute the window boundary that the current tick belongs to. For
|
|
323
|
+
* scheduled runs, this is the epoch-aligned window — not the wall-clock
|
|
324
|
+
* time of execution. This keeps lastRunAt consistent with catch-up's
|
|
325
|
+
* window-based accounting. */
|
|
326
|
+
function computeCurrentWindow(task) {
|
|
327
|
+
const coreSchedule = toCoreSchedule(task.schedule);
|
|
328
|
+
const nowMs = Date.now();
|
|
329
|
+
const windowMs = (0, _receptron_task_scheduler.nextWindowAfter)(coreSchedule, nowMs - (coreSchedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval ? coreSchedule.intervalSec * ONE_SECOND_MS : 0));
|
|
330
|
+
return windowMs !== null && windowMs <= nowMs ? new Date(windowMs).toISOString() : new Date(nowMs).toISOString();
|
|
331
|
+
}
|
|
332
|
+
function computeNextScheduled(task) {
|
|
333
|
+
const next = (0, _receptron_task_scheduler.nextWindowAfter)(toCoreSchedule(task.schedule), Date.now() + 1);
|
|
334
|
+
return next !== null ? new Date(next).toISOString() : null;
|
|
335
|
+
}
|
|
336
|
+
function toCoreSchedule(schedule) {
|
|
337
|
+
if (schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval) return {
|
|
338
|
+
type: _receptron_task_scheduler.SCHEDULE_TYPES.interval,
|
|
339
|
+
intervalSec: Math.round(schedule.intervalMs / ONE_SECOND_MS)
|
|
340
|
+
};
|
|
341
|
+
return schedule;
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
exports.applyScheduleOverride = applyScheduleOverride;
|
|
345
|
+
exports.configureScheduler = configureScheduler;
|
|
346
|
+
exports.createTaskManager = createTaskManager;
|
|
347
|
+
exports.getSchedulerLogs = getSchedulerLogs;
|
|
348
|
+
exports.getSchedulerTasks = getSchedulerTasks;
|
|
349
|
+
exports.initScheduler = initScheduler;
|
|
350
|
+
exports.resetSchedulerForTesting = resetSchedulerForTesting;
|
|
351
|
+
|
|
352
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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 { 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/** Minimal logger the engine logs through. Absent one, runs silent. */\nexport interface SchedulerLogger {\n info: (message: string, data?: Record<string, unknown>) => void;\n warn: (message: string, data?: Record<string, unknown>) => void;\n error: (message: string, data?: Record<string, unknown>) => void;\n}\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 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: () => {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n dependsOn?: string;\n }[];\n}\n\nexport interface TaskManagerOptions {\n tickMs?: number; // default: ONE_MINUTE_MS\n now?: () => Date; // default: () => new Date()\n log?: SchedulerLogger; // default: noop\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 [hours, minutes] = schedule.time.split(\":\").map(Number);\n const targetMs = hours * ONE_HOUR_MS + minutes * ONE_MINUTE_MS;\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\nexport function createTaskManager(options?: TaskManagerOptions): ITaskManager {\n const tickMs = options?.tickMs ?? ONE_MINUTE_MS;\n const now = options?.now ?? (() => new Date());\n const log = options?.log ?? NOOP_LOG;\n const registry = new Map<string, TaskDefinition>();\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function collectDueTasks(currentTime: Date): {\n independent: TaskDefinition[];\n dependent: TaskDefinition[];\n } {\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\n async function runAndTrack(def: TaskDefinition, currentTime: Date, succeeded: Set<string>): 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\n async function runDependentChain(dependent: TaskDefinition[], currentTime: Date, succeeded: Set<string>): 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);\n progress = true;\n }\n remaining = next;\n }\n }\n\n async function onTick(): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime);\n\n // Per-invocation set — success does not leak across tick() calls.\n const succeeded = new Set<string>();\n\n await Promise.all(independent.map((def) => runAndTrack(def, currentTime, succeeded)));\n\n await runDependentChain(dependent, currentTime, succeeded);\n }\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 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 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 timer = setInterval(onTick, 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 [...registry.values()].map((taskDef) => ({\n id: taskDef.id,\n description: taskDef.description,\n schedule: taskDef.schedule,\n dependsOn: taskDef.dependsOn,\n }));\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\";\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\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\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 = computeNextScheduled(task);\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; taskId?: string; limit?: number }): 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/** 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 safePersist(task, scheduledFor, startedAt, durationMs, trigger, errMsg);\n}\n\n/** Best-effort persistence — state and log are independent. A failure in\n * one does not block the other, and neither propagates upward. */\nasync function safePersist(\n task: SystemTaskDef,\n scheduledFor: string,\n startedAt: string,\n durationMs: number,\n trigger: TaskTrigger,\n errMsg: string | null,\n): Promise<void> {\n const isSuccess = errMsg === null;\n const currentState = stateMap.get(task.id);\n try {\n await updateAndSave(\n stateFilePath(),\n stateMap,\n task.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: computeNextScheduled(task),\n },\n stateDeps(),\n );\n } catch (err) {\n logger().warn(\"state persistence failed\", { taskId: task.id, error: String(err) });\n }\n try {\n await appendLogEntry(\n logsDir(),\n {\n taskId: task.id,\n taskName: task.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 },\n logDeps,\n );\n } catch (err) {\n logger().warn(\"log persistence failed\", { taskId: task.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 computeNextScheduled(task: SystemTaskDef): string | null {\n const coreSchedule = toCoreSchedule(task.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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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { createTaskManager, type ITaskManager, type TaskDefinition, type TaskSchedule, type TaskRunContext, type TaskManagerOptions, type SchedulerLogger, } from './task-manager.js';
|
|
2
|
+
export { configureScheduler, initScheduler, applyScheduleOverride, getSchedulerLogs, getSchedulerTasks, resetSchedulerForTesting, type SchedulerConfig, type SystemTaskDef, } from './adapter.js';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SCHEDULE_TYPES } from '@receptron/task-scheduler';
|
|
2
|
+
/** Minimal logger the engine logs through. Absent one, runs silent. */
|
|
3
|
+
export interface SchedulerLogger {
|
|
4
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
5
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
6
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
7
|
+
}
|
|
8
|
+
export type TaskSchedule = {
|
|
9
|
+
type: typeof SCHEDULE_TYPES.interval;
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: typeof SCHEDULE_TYPES.daily;
|
|
13
|
+
time: string;
|
|
14
|
+
};
|
|
15
|
+
export interface TaskRunContext {
|
|
16
|
+
taskId: string;
|
|
17
|
+
now: Date;
|
|
18
|
+
}
|
|
19
|
+
export interface TaskDefinition {
|
|
20
|
+
id: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
schedule: TaskSchedule;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
/** If set, this task only fires after the named task has completed
|
|
25
|
+
* successfully in the current tick cycle. Enforces ordering like
|
|
26
|
+
* "news fetch → journal → memory extraction". */
|
|
27
|
+
dependsOn?: string;
|
|
28
|
+
run: (ctx: TaskRunContext) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export interface ITaskManager {
|
|
31
|
+
registerTask: (def: TaskDefinition) => void;
|
|
32
|
+
removeTask: (taskId: string) => void;
|
|
33
|
+
/** Update the schedule of an existing task. Returns false if not found. */
|
|
34
|
+
updateSchedule: (taskId: string, schedule: TaskSchedule) => boolean;
|
|
35
|
+
start: () => void;
|
|
36
|
+
stop: () => void;
|
|
37
|
+
/** Run one tick manually (for testing). */
|
|
38
|
+
tick: () => Promise<void>;
|
|
39
|
+
listTasks: () => {
|
|
40
|
+
id: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
schedule: TaskSchedule;
|
|
43
|
+
dependsOn?: string;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
export interface TaskManagerOptions {
|
|
47
|
+
tickMs?: number;
|
|
48
|
+
now?: () => Date;
|
|
49
|
+
log?: SchedulerLogger;
|
|
50
|
+
}
|
|
51
|
+
export declare function createTaskManager(options?: TaskManagerOptions): ITaskManager;
|