@mulmoclaude/core 0.12.0 → 0.12.1

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.
@@ -69,6 +69,7 @@ See [Wiki](config/helps/wiki.md) for details on how it works.
69
69
  - [Sandbox](config/helps/sandbox.md) — how the Docker sandbox isolates the agent, what it can access, and how to disable it
70
70
  - [Error recovery](config/helps/error-recovery.md) — the lookup the agent reads on tool failures (gh/git/SSH inside the sandbox, Marp PDF, registry import, build/workspace, plugin runtime) before asking the user
71
71
  - [Telegram Bridge](config/helps/telegram.md) — how to talk to MulmoClaude from the Telegram app: creating a bot, starting the bridge, allowlisting chat IDs, commands, and troubleshooting
72
+ - [Remote host](config/helps/remote-host.md) — drive MulmoClaude from a phone at mulmoserver.web.app: Google sign-in connect, host online vs. offline (queued chats, 7-day expiry), photo attachments, and the security model
72
73
  - [Feeds](config/helps/feeds.md) — register a self-refreshing data feed (RSS/Atom/JSON) by authoring `feeds/<slug>/schema.json`: schema shape, the `ingest` block, raw-item field mapping, and `maxItems` retention
73
74
  - [GitHub repositories in the workspace](config/helps/github.md) — clone-destination rules under `github/<name>/` and how to handle existing directories with matching or different remotes
74
75
  - [Collection skills](config/helps/collection-skills.md) — build a data app (model + UI + relations + computed fields + action buttons) by authoring a `schema.json` collection skill: the DSL, field types, derived formulas, actions, records
@@ -0,0 +1,122 @@
1
+ # Remote Host — Your MulmoClaude from Your Phone
2
+
3
+ The remote host feature lets you use your MulmoClaude from a phone browser at
4
+ **https://mulmoserver.web.app** — browse your collections, open mobile custom
5
+ views, edit records, and start chats with photos taken on the spot — while the
6
+ server keeps running privately on your own computer.
7
+
8
+ Nothing on your computer is exposed to the internet: no open ports, no public
9
+ URL, no tunnels. Phone and server talk through a **cloud message queue**
10
+ (Google Firestore): the phone drops a request into your private queue, your
11
+ server picks it up, does the work, and drops the answer back. Both directions
12
+ happen in real time.
13
+
14
+ ## Connecting
15
+
16
+ 1. On your computer, click the **phone icon** (`phonelink`) in the MulmoClaude
17
+ toolbar and press **Connect**. A Google sign-in popup opens — sign in with
18
+ your Google account. The icon turns green when the host is connected.
19
+ 2. On your phone, open **https://mulmoserver.web.app** and sign in with the
20
+ **same Google account**. The app shows your host as online and lists what it
21
+ can do.
22
+
23
+ The two sides find each other purely through the shared Google account — there
24
+ is no pairing code and no IP address to type.
25
+
26
+ The connection lives only as long as the server process: restarting MulmoClaude
27
+ drops it, and you reconnect with the same one click. **Disconnect** in the same
28
+ popover stops it immediately.
29
+
30
+ ## What You Can Do from the Phone
31
+
32
+ - **Browse collections** — list your collections and page through their records.
33
+ - **Mobile custom views** — open views authored for the phone
34
+ (`target: "mobile"` in a collection's `views[]`; see
35
+ [Remote custom views](config/helps/custom-view-remote.md)). Views can show
36
+ image thumbnails and, where the view allows it, **update or delete records**.
37
+ - **Start a chat** — send a message (optionally with a role and **photo
38
+ attachments**) that opens a visible chat session on your computer, as if you
39
+ had typed it there.
40
+ - **Browse feeds, shortcuts, and skills** — read-only listings of what the
41
+ workspace offers.
42
+
43
+ The phone app only shows actions your host actually supports — an older host
44
+ simply advertises a shorter menu, so you never tap a button that can't work.
45
+
46
+ ## When the Host Is Online
47
+
48
+ Everything above works live. The host announces itself once a minute, so the
49
+ phone reliably shows it as online and results come back within moments.
50
+
51
+ ## When the Host Is Offline
52
+
53
+ If your computer is asleep or MulmoClaude isn't connected, the phone shows the
54
+ host as **offline**. Browsing (collections, views, feeds) needs a live host and
55
+ is unavailable — but **starting a chat still works**:
56
+
57
+ - The message (and any attached photos) is **queued** in the cloud. The moment
58
+ your host reconnects, it drains the queue — oldest first — and starts the
59
+ chats as if they had just arrived.
60
+ - Queued messages **expire after 7 days**. An expired message is deleted
61
+ outright, along with its uploaded photos — it will not surprise you by
62
+ starting a week-old chat.
63
+ - The phone lists your pending messages, and you can delete one before the
64
+ host picks it up.
65
+
66
+ This makes the chat queue a fire-and-forget inbox: jot an idea or snap a
67
+ receipt on the go, and it is waiting for your MulmoClaude the next time it
68
+ comes online.
69
+
70
+ ## Photos and Attachments
71
+
72
+ Photos attached to a chat are uploaded to a private staging area (Firebase
73
+ Storage) under your account, downloaded into your workspace when the host
74
+ handles the message, and then **deleted from the cloud**. Photos belonging to
75
+ an expired queued message are deleted too. As a backstop, anything somehow left
76
+ behind is automatically swept after 14 days.
77
+
78
+ ## How Secure Is It?
79
+
80
+ **Your server stays private.** It only makes *outbound* connections to Google's
81
+ Firestore — it never listens on a public address, so there is nothing on your
82
+ machine for an attacker to scan or reach.
83
+
84
+ **Everything is scoped to your Google account.** Both the phone and the host
85
+ sign in as *you*, and the cloud's security rules restrict each signed-in user
86
+ to their own private area — no other user can read your data or send commands
87
+ to your host, and your host cannot touch anyone else's data.
88
+
89
+ **Sign-in is one-shot and in-memory.** Connect passes a short-lived Google
90
+ sign-in token from your browser to the server over `localhost` only; it is
91
+ used once, never written to a log or to disk. The resulting session lives in
92
+ server memory — restarting the server forgets it, and Disconnect ends it on
93
+ the spot.
94
+
95
+ **Mobile views are sandboxed.** A custom view rendered on the phone runs in a
96
+ locked-down frame with **all network access disabled** — data reaches it only
97
+ through a controlled message channel, so a view (which may be LLM-authored)
98
+ cannot leak your data to the outside. Record edits made from a view are
99
+ validated and policy-checked by *your host*, not trusted from the phone.
100
+
101
+ **What passes through the cloud.** Requests, results (record pages, view HTML,
102
+ thumbnails), and staged photo uploads transit — and are temporarily stored in —
103
+ your private area of a Google Firebase project while in flight. Access is
104
+ limited to your account, and the data is deleted as soon as it has been
105
+ delivered — the cloud is a relay, not a copy of your workspace.
106
+
107
+ **The one thing to protect is your Google account.** Anyone who can sign in to
108
+ it can command your host with the full capability list above (though never
109
+ beyond it — the host only executes its fixed, built-in set of operations).
110
+ Use a strong password and two-factor authentication, and Disconnect the host
111
+ when you don't need remote access.
112
+
113
+ ## Good to Know
114
+
115
+ - **Host shows offline right after a restart** — reconnecting is manual by
116
+ design (the sign-in lives in memory). Click the phone icon → Connect.
117
+ - **A queued chat used a role that no longer exists** — the message fails with
118
+ an error the phone can display, rather than silently starting the wrong chat.
119
+ - **Not the same thing as the MulmoBridge messenger bridges** — Telegram / LINE
120
+ bridges relay *conversations* through a bot; the remote host is a *control
121
+ channel* for collections, views, and chat-starting from the dedicated phone
122
+ app.
@@ -1,4 +1,4 @@
1
- import { TaskExecutionState, TaskLogEntry, MISSED_RUN_POLICIES } from '@receptron/task-scheduler';
1
+ import { TaskExecutionState, TaskLogEntry, TaskTrigger, MISSED_RUN_POLICIES } from '@receptron/task-scheduler';
2
2
  import { ITaskManager, TaskDefinition, SchedulerLogger } from './task-manager.js';
3
3
  export interface SchedulerConfig {
4
4
  /** Absolute workspace root — state.json + logs hang off it. */
@@ -44,5 +44,28 @@ export declare function getSchedulerTasks(): {
44
44
  missedRunPolicy: string;
45
45
  state: TaskExecutionState;
46
46
  }[];
47
+ /** Read the persisted execution state for any task id (system, user, or
48
+ * skill). Returns an empty state when the id has never run — used by the API
49
+ * route to attach last-run / next-run / history state to user + skill tasks
50
+ * it lists from other sources. */
51
+ export declare function getSchedulerTaskState(taskId: string): TaskExecutionState;
52
+ /** Record a run of an EXTERNAL (skill / user) task — one registered directly
53
+ * on the task-manager rather than through `initScheduler`. Persists a log
54
+ * entry + updates state so these tasks get the same history / last-run /
55
+ * next-run as system tasks. Because they are fire-and-forget (`startChat`
56
+ * spawns an async chat), `errorMessage` reflects whether the *dispatch*
57
+ * succeeded, not the chat's eventual outcome; `chatSessionId` links the run
58
+ * to the spawned session. */
59
+ export declare function recordExternalRun(params: {
60
+ id: string;
61
+ name: string;
62
+ schedule: TaskDefinition["schedule"];
63
+ scheduledFor: string;
64
+ startedAt: string;
65
+ durationMs: number;
66
+ trigger: TaskTrigger;
67
+ errorMessage: string | null;
68
+ chatSessionId?: string;
69
+ }): Promise<void>;
47
70
  /** Test-only: clear config + in-memory state. */
48
71
  export declare function resetSchedulerForTesting(): void;
@@ -27,65 +27,76 @@ function isDue(now, schedule, tickMs) {
27
27
  }
28
28
  return false;
29
29
  }
30
+ /** Split the due tasks into those that may run immediately and those gated
31
+ * behind a `dependsOn` edge (resolved later in the same tick cycle). */
32
+ function collectDueTasks(currentTime, registry, tickMs) {
33
+ const independent = [];
34
+ const dependent = [];
35
+ for (const def of registry.values()) {
36
+ if (def.enabled === false) continue;
37
+ if (!isDue(currentTime, def.schedule, tickMs)) continue;
38
+ if (def.dependsOn) dependent.push(def);
39
+ else independent.push(def);
40
+ }
41
+ return {
42
+ independent,
43
+ dependent
44
+ };
45
+ }
46
+ async function runAndTrack(def, currentTime, succeeded, log) {
47
+ try {
48
+ await def.run({
49
+ taskId: def.id,
50
+ now: currentTime
51
+ });
52
+ succeeded.add(def.id);
53
+ } catch (err) {
54
+ log.error("task failed", {
55
+ id: def.id,
56
+ error: String(err)
57
+ });
58
+ }
59
+ }
60
+ async function runDependentChain(dependent, currentTime, succeeded, log) {
61
+ let remaining = [...dependent];
62
+ let progress = true;
63
+ while (remaining.length > 0 && progress) {
64
+ progress = false;
65
+ const next = [];
66
+ for (const def of remaining) {
67
+ const dep = def.dependsOn;
68
+ if (!dep || !succeeded.has(dep)) {
69
+ next.push(def);
70
+ continue;
71
+ }
72
+ await runAndTrack(def, currentTime, succeeded, log);
73
+ progress = true;
74
+ }
75
+ remaining = next;
76
+ }
77
+ }
78
+ async function runTick(now, registry, tickMs, log) {
79
+ const currentTime = now();
80
+ const { independent, dependent } = collectDueTasks(currentTime, registry, tickMs);
81
+ const succeeded = /* @__PURE__ */ new Set();
82
+ await Promise.all(independent.map((def) => runAndTrack(def, currentTime, succeeded, log)));
83
+ await runDependentChain(dependent, currentTime, succeeded, log);
84
+ }
85
+ function listTaskSummaries(registry) {
86
+ return [...registry.values()].map((taskDef) => ({
87
+ id: taskDef.id,
88
+ description: taskDef.description,
89
+ schedule: taskDef.schedule,
90
+ dependsOn: taskDef.dependsOn
91
+ }));
92
+ }
30
93
  function createTaskManager(options) {
31
94
  const tickMs = options?.tickMs ?? ONE_MINUTE_MS;
32
95
  const now = options?.now ?? (() => /* @__PURE__ */ new Date());
33
96
  const log = options?.log ?? NOOP_LOG$1;
34
97
  const registry = /* @__PURE__ */ new Map();
35
98
  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
- }
99
+ const onTick = () => runTick(now, registry, tickMs, log);
89
100
  return {
90
101
  async tick() {
91
102
  await onTick();
@@ -118,12 +129,7 @@ function createTaskManager(options) {
118
129
  }
119
130
  },
120
131
  listTasks() {
121
- return [...registry.values()].map((taskDef) => ({
122
- id: taskDef.id,
123
- description: taskDef.description,
124
- schedule: taskDef.schedule,
125
- dependsOn: taskDef.dependsOn
126
- }));
132
+ return listTaskSummaries(registry);
127
133
  }
128
134
  };
129
135
  }
@@ -228,7 +234,7 @@ async function applyScheduleOverride(taskId, schedule) {
228
234
  if (!task || !taskManagerRef) return false;
229
235
  if (!taskManagerRef.updateSchedule(taskId, schedule)) return false;
230
236
  task.schedule = schedule;
231
- await safeUpdateState(taskId, { nextScheduledAt: computeNextScheduled(task) });
237
+ await safeUpdateState(taskId, { nextScheduledAt: computeNextScheduledFor(task.schedule) });
232
238
  return true;
233
239
  }
234
240
  /** Query execution logs — used by API routes. */
@@ -246,6 +252,35 @@ function getSchedulerTasks() {
246
252
  state: stateMap.get(taskDef.id) ?? (0, _receptron_task_scheduler.emptyState)(taskDef.id)
247
253
  }));
248
254
  }
255
+ /** Read the persisted execution state for any task id (system, user, or
256
+ * skill). Returns an empty state when the id has never run — used by the API
257
+ * route to attach last-run / next-run / history state to user + skill tasks
258
+ * it lists from other sources. */
259
+ function getSchedulerTaskState(taskId) {
260
+ return stateMap.get(taskId) ?? (0, _receptron_task_scheduler.emptyState)(taskId);
261
+ }
262
+ /** Record a run of an EXTERNAL (skill / user) task — one registered directly
263
+ * on the task-manager rather than through `initScheduler`. Persists a log
264
+ * entry + updates state so these tasks get the same history / last-run /
265
+ * next-run as system tasks. Because they are fire-and-forget (`startChat`
266
+ * spawns an async chat), `errorMessage` reflects whether the *dispatch*
267
+ * succeeded, not the chat's eventual outcome; `chatSessionId` links the run
268
+ * to the spawned session. */
269
+ async function recordExternalRun(params) {
270
+ await persistRun({
271
+ meta: {
272
+ id: params.id,
273
+ name: params.name,
274
+ schedule: params.schedule
275
+ },
276
+ scheduledFor: params.scheduledFor,
277
+ startedAt: params.startedAt,
278
+ durationMs: params.durationMs,
279
+ trigger: params.trigger,
280
+ errMsg: params.errorMessage,
281
+ chatSessionId: params.chatSessionId
282
+ });
283
+ }
249
284
  /** Test-only: clear config + in-memory state. */
250
285
  function resetSchedulerForTesting() {
251
286
  config = null;
@@ -266,44 +301,66 @@ async function executeAndLog(task, scheduledFor, trigger) {
266
301
  error: errMsg
267
302
  });
268
303
  }
269
- await safePersist(task, scheduledFor, startedAt, Date.now() - startMs, trigger, errMsg);
304
+ const durationMs = Date.now() - startMs;
305
+ await persistRun({
306
+ meta: {
307
+ id: task.id,
308
+ name: task.name,
309
+ schedule: task.schedule
310
+ },
311
+ scheduledFor,
312
+ startedAt,
313
+ durationMs,
314
+ trigger,
315
+ errMsg
316
+ });
270
317
  }
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) {
318
+ /** Best-effort persistence — state and log are independent, so one failing
319
+ * never blocks the other and neither propagates upward. */
320
+ async function persistRun(run) {
321
+ await writeRunState(run);
322
+ await writeRunLog(run);
323
+ }
324
+ async function writeRunState(run) {
325
+ const { meta, scheduledFor, durationMs, errMsg } = run;
274
326
  const isSuccess = errMsg === null;
275
- const currentState = stateMap.get(task.id);
327
+ const currentState = stateMap.get(meta.id);
276
328
  try {
277
- await (0, _receptron_task_scheduler.updateAndSave)(stateFilePath(), stateMap, task.id, {
329
+ await (0, _receptron_task_scheduler.updateAndSave)(stateFilePath(), stateMap, meta.id, {
278
330
  lastRunAt: scheduledFor,
279
331
  lastRunResult: isSuccess ? _receptron_task_scheduler.TASK_RESULTS.success : _receptron_task_scheduler.TASK_RESULTS.error,
280
332
  lastRunDurationMs: durationMs,
281
333
  lastErrorMessage: errMsg,
282
334
  consecutiveFailures: isSuccess ? 0 : (currentState?.consecutiveFailures ?? 0) + 1,
283
335
  totalRuns: (currentState?.totalRuns ?? 0) + 1,
284
- nextScheduledAt: computeNextScheduled(task)
336
+ nextScheduledAt: computeNextScheduledFor(meta.schedule)
285
337
  }, stateDeps());
286
338
  } catch (err) {
287
339
  logger().warn("state persistence failed", {
288
- taskId: task.id,
340
+ taskId: meta.id,
289
341
  error: String(err)
290
342
  });
291
343
  }
344
+ }
345
+ async function writeRunLog(run) {
346
+ const { meta, scheduledFor, startedAt, durationMs, trigger, errMsg, chatSessionId } = run;
347
+ const isSuccess = errMsg === null;
292
348
  try {
293
349
  await (0, _receptron_task_scheduler.appendLogEntry)(logsDir(), {
294
- taskId: task.id,
295
- taskName: task.name,
350
+ taskId: meta.id,
351
+ taskName: meta.name,
296
352
  scheduledFor,
297
353
  startedAt,
298
354
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
299
355
  result: isSuccess ? _receptron_task_scheduler.TASK_RESULTS.success : _receptron_task_scheduler.TASK_RESULTS.error,
300
356
  durationMs,
301
357
  trigger,
302
- ...errMsg !== null && { errorMessage: errMsg }
358
+ ...errMsg !== null && { errorMessage: errMsg },
359
+ ...chatSessionId !== void 0 && { chatSessionId }
303
360
  }, logDeps);
304
361
  } catch (err) {
305
362
  logger().warn("log persistence failed", {
306
- taskId: task.id,
363
+ taskId: meta.id,
307
364
  error: String(err)
308
365
  });
309
366
  }
@@ -329,8 +386,8 @@ function computeCurrentWindow(task) {
329
386
  const windowMs = (0, _receptron_task_scheduler.nextWindowAfter)(coreSchedule, nowMs - (coreSchedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval ? coreSchedule.intervalSec * ONE_SECOND_MS : 0));
330
387
  return windowMs !== null && windowMs <= nowMs ? new Date(windowMs).toISOString() : new Date(nowMs).toISOString();
331
388
  }
332
- function computeNextScheduled(task) {
333
- const next = (0, _receptron_task_scheduler.nextWindowAfter)(toCoreSchedule(task.schedule), Date.now() + 1);
389
+ function computeNextScheduledFor(schedule) {
390
+ const next = (0, _receptron_task_scheduler.nextWindowAfter)(toCoreSchedule(schedule), Date.now() + 1);
334
391
  return next !== null ? new Date(next).toISOString() : null;
335
392
  }
336
393
  function toCoreSchedule(schedule) {
@@ -341,12 +398,20 @@ function toCoreSchedule(schedule) {
341
398
  return schedule;
342
399
  }
343
400
  //#endregion
401
+ Object.defineProperty(exports, "TASK_TRIGGERS", {
402
+ enumerable: true,
403
+ get: function() {
404
+ return _receptron_task_scheduler.TASK_TRIGGERS;
405
+ }
406
+ });
344
407
  exports.applyScheduleOverride = applyScheduleOverride;
345
408
  exports.configureScheduler = configureScheduler;
346
409
  exports.createTaskManager = createTaskManager;
347
410
  exports.getSchedulerLogs = getSchedulerLogs;
411
+ exports.getSchedulerTaskState = getSchedulerTaskState;
348
412
  exports.getSchedulerTasks = getSchedulerTasks;
349
413
  exports.initScheduler = initScheduler;
414
+ exports.recordExternalRun = recordExternalRun;
350
415
  exports.resetSchedulerForTesting = resetSchedulerForTesting;
351
416
 
352
417
  //# sourceMappingURL=index.cjs.map
@@ -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 { 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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"}
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 TaskSummary {\n id: string;\n description?: string;\n schedule: TaskSchedule;\n dependsOn?: string;\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}\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\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\nasync function runTick(now: () => Date, registry: Map<string, TaskDefinition>, tickMs: number, log: SchedulerLogger): Promise<void> {\n const currentTime = now();\n const { independent, dependent } = collectDueTasks(currentTime, registry, tickMs);\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, log)));\n\n await runDependentChain(dependent, currentTime, succeeded, 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\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 const onTick = () => runTick(now, registry, tickMs, log);\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 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\";\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 = 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; 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/** 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;\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;\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,2 +1,3 @@
1
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';
2
+ export { configureScheduler, initScheduler, applyScheduleOverride, getSchedulerLogs, getSchedulerTasks, getSchedulerTaskState, recordExternalRun, resetSchedulerForTesting, type SchedulerConfig, type SystemTaskDef, } from './adapter.js';
3
+ export { TASK_TRIGGERS, type TaskTrigger } from '@receptron/task-scheduler';